Core concepts
Runtime & run state
How a run executes, what RunState accumulates, and the results a run can return.
Every Assemble run follows the same contract: validate the input, execute the topology step by step, reduce every trace event into a single RunState, and return a discriminated result you can switch on. Because the runtime is the same whether you call workflow.run or the standalone runWorkflow, everything you learn here applies to the CLI, replay, and evals too.
This page covers the run lifecycle, the result union, what RunState accumulates, and the options you can pass to a run.
Run lifecycle#
workflow.run(input, options) executes four phases in order:
- Validate input. The input is parsed against the workflow's
inputSchema. If it fails, the run throws anAppErrorwith codeVALIDATION_FAILEDbefore any agent executes. - Run the topology. Pipeline, supervisor, or parallel steps execute (see workflows). Each agent call, tool call, routing decision, and policy decision emits trace events.
- Record the trace. Every event flows through the trace bus to your configured sinks and is reduced into the run's
RunState. WhenpersistRunStateis on (the default), the state is written to disk after each step. - Return a discriminated result. The final output is validated against
outputSchema(when one is set), arun.completedevent is emitted, sinks are flushed, and the result is returned.
const result = await supervised.run({ ticket: "refund please" });
if (result.status === "completed") {
console.log(result.output.reply);
console.log(result.state.metrics.agentCallCount);
}The result union#
run returns a WorkflowRunResult<TOutput>, a union of two shapes:
| Status | Extra fields | Meaning |
|---|---|---|
"completed" | output: TOutput | The topology finished and the output passed outputSchema. |
"awaiting_approval" | approvalId: string | A policy or tool paused the run for a human decision. See human approval. |
Both variants also carry runId, the full state: RunState, and the ordered events: readonly TraceEvent[].
Two more outcomes exist outside this union:
- Failures throw. An agent error, a blocked tool, or an output that fails
outputSchemathrows anAppError; the persistedRunStaterecordsstatus: "failed"along with the error, so the failure is still traceable and replayable. - Rejections resolve as
rejected. Resuming a paused run with a rejected decision returnsstatus: "rejected"fromresumeRun— this variant only appears inWorkflowResumeResult, never fromrunitself.
The full RunState.status enum is pending, running, awaiting_approval, completed, failed, canceled, and rejected.
Note: Narrow on
result.statusbefore touchingresult.output. TypeScript only knowsoutputexists on the"completed"branch.
RunState#
RunState is the single accumulated record of everything a run produced. It is built by reducing the trace event stream, so it always agrees with the events you see in tracing.
| Field | Type | Description |
|---|---|---|
runId | string | Unique run identifier. |
workflowName | string | Name of the workflow definition. |
status | RunStatus | Terminal or current status of the run. |
input | JsonValue | The validated workflow input. |
messages | RunMessage[] | Thread messages recorded during the run. |
agentCalls | AgentCall[] | One entry per agent execution: input, output, status, latency, error. |
toolCalls | ToolCall[] | One entry per tool call: requested, approved, blocked, completed, or failed. |
policyDecisions | PolicyDecision[] | Every policy evaluation with its action and reason. |
routingDecisions | RoutingDecision[] | Supervisor route choices with candidates and reasons. |
approvals | RunApproval[] | Approval requests with their status and decider. |
artifacts | RunArtifact[] | Keyed values produced by the run, such as parallel:merged. |
errors | RunError[] | Errors recorded by failed agents, tools, or the run itself. |
metrics | RunMetrics | Aggregated counters and latency (see below). |
finalOutput | JsonValue | null | The validated output once the run completes. |
createdAt / startedAt / completedAt | string | null | ISO timestamps for the run lifecycle. |
Metrics#
state.metrics is recomputed after every event:
| Metric | Type | Description |
|---|---|---|
totalLatencyMs | number | null | Wall-clock duration, set when the run completes or fails. |
tokenUsage | { inputTokens, outputTokens, totalTokens } | null | Aggregated token usage when the provider reports it. |
agentCallCount | number | Length of agentCalls. |
toolCallCount | number | Length of toolCalls. |
policyDecisionCount | number | Length of policyDecisions. |
approvalCount | number | Length of approvals. |
Run options#
Both workflow.run(input, options) and runWorkflow accept the same RunWorkflowOptions:
| Option | Type | Description |
|---|---|---|
sinks | TraceSink[] | Trace destinations for this run — console, JSONL, platform, or your own. Defaults to none. See tracing. |
runId | string | Explicit run id. Defaults to a generated id. Set it when you need to resume or replay a known run. |
now | () => Date | Clock override for deterministic timestamps in tests. |
runStateDirectory | string | Where run state JSON is written. Defaults to .assemble/runs. |
persistRunState | boolean | Persist RunState to <runStateDirectory>/<runId>.json after each step. Defaults to true; required for resumeRun and replay. |
import { createConsoleSink, createJsonlSink } from "@assemble-dev/sdk";
await supervised.run(
{ ticket: "refund please" },
{
runId: "run_1",
sinks: [createConsoleSink(), createJsonlSink({ runId: "run_1", directory: ".assemble/runs" })],
persistRunState: true,
},
);Standalone runWorkflow#
workflow.run is a thin wrapper over runWorkflow. Use the standalone form when you hold a definition as a value — for example in a job runner or test harness that receives workflows generically:
import { runWorkflow } from "@assemble-dev/sdk";
const result = await runWorkflow({
definition: supervised,
input: { ticket: "refund please" },
options: { runId: "run_1" },
});The two are interchangeable: same validation, same result union, same options.
Where to go next#
- Pause runs on sensitive tool calls with policies and resume them via human approval.
- Send the event stream somewhere useful with tracing.
- Re-execute a recorded run and diff it with replay.