Share what a LangGraph run produced
A graph finishes. The final state has a report key holding several thousand
lines of HTML, and the run returns it to whatever called invoke.
If that caller was a script, the report is in a variable. If it was a LangGraph Platform deployment, it is in a response body on a server you do not shell into. Either way, the analyst who needed it has nothing.
Put the publish in a node
The graph already models "then this happens" as an edge. Publishing is a node
like any other — the last one before END:
def publish(state: State) -> dict:
r = httpx.post(
"https://commareports.com/api/v1/reports",
headers={"Authorization": f"Bearer {os.environ['COMMA_API_TOKEN']}"},
json={
"title": state["title"],
"html": state["report_html"],
},
).json()
return {"report_url": r["url"], "report_id": r["id"], "report_html": ""}
graph.add_node("publish", publish)
graph.add_edge("analyze", "publish")
graph.add_edge("publish", END)
Note the last line of the return. Clearing report_html once it has an
address is the whole trick: LangGraph checkpoints state after every node, so a
large document in state gets serialized again on each step, and gets replayed
into context if the graph resumes. Swapping the document for its URL is the
same move as
context budgeting for AI reports.
Interrupts get something to interrupt for
Human-in-the-loop is the case where this stops being convenience. interrupt()
suspends the graph and waits for a human value, which means the human has to
form an opinion about work they cannot see.
Publish first, interrupt second:
def review_gate(state: State):
decision = interrupt({
"message": "Review the findings and approve or send back.",
"report_url": state["report_url"],
})
return {"approved": decision == "approve"}
Now the reviewer gets a page, not a JSON blob — and the notes they leave are anchored to the paragraph they disagree with, per commenting on HTML. On resume, the graph can pull those threads and act on them rather than on a single approve/reject bit.
Deployed graphs have no filesystem you can reach
This is the failure people hit second. Locally, open report.html works and
the publish node feels optional. On LangGraph Platform — or any container, any
worker — the file is written into a sandbox that is gone when the run ends.
It is the same problem a CI job has, and it has the same fix: a URL. See publishing from CI.
Worth knowing
- One report, many revisions. Store
report_idin state and update it on the next run, so a recurring graph keeps one address instead of scattering a new link per execution. - Scope the token to
reports:write. A graph with tool access should not hold a token that can do more than publish — see scoped tokens for AI agents. - 5 MB per report body.
- Reports are snapshots. The HTML renders sandboxed; nothing re-executes.
Try it
Free — unlimited reports, commenters and revisions.