How to work with runs¶
Overview¶
A run is Zeroth's persistent record of one graph execution. After the orchestrator finishes (or pauses) a run, you can load it back from the repository to inspect its status, history, outputs, and failure state. This guide shows how to execute a graph and read back the resulting run.
Minimal example¶
import asyncio
from zeroth.integrations.persistence.runs import RunRepository
from zeroth.runtime.runs import Run, RunStatus
from zeroth.platform.storage.async_sqlite import AsyncSQLiteDatabase
async def main() -> None:
# 1. Open a database and a run repository
db = AsyncSQLiteDatabase(path=":memory:")
await db.connect()
repo = RunRepository(db)
await repo.initialize()
# 2. Create a run (in practice, the orchestrator does this for you)
run = Run(
graph_id="doc.summariser",
thread_id=None,
status=RunStatus.PENDING,
inputs={"text": "Hello world"},
)
await repo.create(run)
# 3. ...orchestrator executes the graph, advancing the run...
await repo.transition(run.id, RunStatus.RUNNING)
await repo.transition(run.id, RunStatus.COMPLETED)
# 4. Load it back and inspect its state
loaded: Run = await repo.get(run.id)
print("status :", loaded.status)
print("outputs:", loaded.outputs)
print("history:")
for entry in loaded.history:
print(f" {entry.node_id} -> {entry.status} (attempt {entry.attempt})")
asyncio.run(main())
After any graph execution, loading a Run by its ID gives you everything the orchestrator wrote: the final RunStatus, every RunHistoryEntry, every RunConditionResult, and — if the run failed — a structured RunFailureState explaining why.
Common patterns¶
- Filter by thread. Use
ThreadRepository.list_runs(thread_id)to get every run that belongs to one conversation or long-lived task. - Check status before acting. Operator tooling should always check
run.statusbefore attempting a replay —RunStatus.COMPLETEDis terminal,RunStatus.FAILEDcan be replayed, others are in-flight. - Inspect the last entry.
run.history[-1]is the most recent node that executed and is usually the best place to start when debugging a partial run. - Use
ThreadMemoryBinding. When resuming a thread, load itsThreadMemoryBindingto connect the new run to the thread's persistent memory.
Pitfalls¶
- Invalid transitions.
RunRepository.transitionenforcesALLOWED_TRANSITIONS. You cannot jump fromPENDINGdirectly toCOMPLETED; attempting to do so raises an error. Always move throughRUNNING. - Mutating loaded runs. A
Runreturned fromrepo.getis a snapshot — mutating it in Python does not update the database. Userepo.updateorrepo.transitionto persist changes. - Thread vs. run scope. Memory and approval context belong on threads, not runs. Attaching long-lived state to a single run loses it on the next turn.
- Assuming
outputsis always populated. Outputs are only set once the orchestrator marks the runCOMPLETED. For failed or in-flight runs, checkrun.failure_stateandrun.historyinstead. - Forgetting initialisation.
RunRepositoryandThreadRepositoryrequireinitialize()to be called once before first use (they run Alembic migrations at startup).