# How to use Tidebase with LangGraph

Use LangGraph's own checkpointer for graph state — that's what it's for — and use Tidebase for what the checkpointer doesn't give you: a queryable run record in your own Postgres, durable human approval gates, a side-effect replay contract, and a per-run cost ledger. The integration point is the node function: pass the Tidebase run context through the graph's `configurable`, and wrap external writes and approvals inside nodes.

```python
from langgraph.graph import StateGraph, START, END
from tidebase import Tidebase

tide = Tidebase()

def publish_node(state, config):
    run = config["configurable"]["tide_run"]

    decision = run.gate(
        "approve-publish",
        "Publish this draft?",
        data={"preview": state["draft"][:200]},
    )
    if not decision.approved:
        return {"published": False}

    run.step(
        "publish",
        lambda: cms.publish(state["draft"]),
        input={"draft": state["draft"]},
        side_effects=["cms"],
        idempotency_key=f"publish-{run.run_id}",
    )
    return {"published": True}

def workflow(run, input):
    graph = build_graph()  # your StateGraph with publish_node compiled in
    result = graph.invoke(
        {"topic": input["topic"]},
        config={"configurable": {"tide_run": run, "thread_id": run.run_id}},
    )
    return result

tide.run("research-and-publish", workflow, run_id=run_id, input={"topic": topic})
```

Tidebase is an open-source checkpoint layer for AI agents: wrap your steps, and failed runs resume from the last safe point — in your own Postgres, without moving execution into a new runtime.

## Two layers, two jobs

This guide deliberately does **not** wrap `graph.invoke()` in one giant step. The two layers do different jobs:

- **LangGraph's checkpointer** persists graph state per super-step so an interrupted graph resumes at the right node with the right state. Keep it — use `thread_id = run.run_id` so the two layers share an identity.
- **Tidebase** records the run as queryable rows in your Postgres (state, events, gates, usage), enforces the [replay contract](../replay-contract-is-it-safe-to-rerun.md) on side effects, and parks approvals durably.

If all you need is graph-state persistence, the checkpointer alone may be enough — the structural differences are laid out in [Tidebase vs LangGraph checkpointers](../compare/tidebase-vs-langgraph-checkpointer.md). Reach for both when your graph touches the outside world or needs humans in the loop.

## Side effects inside nodes

A LangGraph resume re-executes the node that was interrupted. If that node had already half-fired an external write, you get a double-fire — unless the write sits behind a Tidebase step. The `publish` step above is checkpointed by name and input hash: when the node re-runs, a completed publish replays from Postgres instead of hitting the CMS again. A failed side-effecting step without an idempotency key parks as `manual_review` rather than being blindly retried.

## Gates instead of `interrupt`

LangGraph's `interrupt` pauses a graph for input, but the pending question lives in graph state. A Tidebase [gate](../human-approval-gates-for-ai-agents.md) is a durable, exactly-once decision in Postgres: it survives restarts, is resolvable from Studio or a webhook channel (Slack, your product UI), and records who decided. The blocking `run.gate(...)` call inside a node works because the graph is already executing inside your own process — Tidebase isn't running anything.

## Recording model usage

LangGraph doesn't meter your LLM calls; wherever a node calls a model, record it:

```python
run.usage.record(
    kind="llm", provider="openai", model="gpt-4.1-mini",
    input_tokens=cb.input_tokens, output_tokens=cb.output_tokens,
)
```

## The honest tradeoffs

- **Tidebase does not execute your code.** Something must re-invoke the workflow after a crash — a Tidebase queue, a recovery webhook, or your own infrastructure. Re-invocation with the same `run_id` is always safe.
- **Two persistence layers means two things to operate.** That's real. The split earns its keep when you need gates, the side-effect contract, or the run record outside the framework; skip it when you don't.
- **Alpha, opt-in auth.** Self-hosted alpha — set `TIDEBASE_API_KEY` before exposing the server beyond localhost.

Repo: <https://github.com/BlueprintLabIO/tidebase> · See also: [Tidebase vs LangGraph checkpointers](../compare/tidebase-vs-langgraph-checkpointer.md)