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:

  1. Validate input. The input is parsed against the workflow's inputSchema. If it fails, the run throws an AppError with code VALIDATION_FAILED before any agent executes.
  2. Run the topology. Pipeline, supervisor, or parallel steps execute (see workflows). Each agent call, tool call, routing decision, and policy decision emits trace events.
  3. Record the trace. Every event flows through the trace bus to your configured sinks and is reduced into the run's RunState. When persistRunState is on (the default), the state is written to disk after each step.
  4. Return a discriminated result. The final output is validated against outputSchema (when one is set), a run.completed event is emitted, sinks are flushed, and the result is returned.
ts
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:

StatusExtra fieldsMeaning
"completed"output: TOutputThe topology finished and the output passed outputSchema.
"awaiting_approval"approvalId: stringA 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 outputSchema throws an AppError; the persisted RunState records status: "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 returns status: "rejected" from resumeRun — this variant only appears in WorkflowResumeResult, never from run itself.

The full RunState.status enum is pending, running, awaiting_approval, completed, failed, canceled, and rejected.

Note: Narrow on result.status before touching result.output. TypeScript only knows output exists 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.

FieldTypeDescription
runIdstringUnique run identifier.
workflowNamestringName of the workflow definition.
statusRunStatusTerminal or current status of the run.
inputJsonValueThe validated workflow input.
messagesRunMessage[]Thread messages recorded during the run.
agentCallsAgentCall[]One entry per agent execution: input, output, status, latency, error.
toolCallsToolCall[]One entry per tool call: requested, approved, blocked, completed, or failed.
policyDecisionsPolicyDecision[]Every policy evaluation with its action and reason.
routingDecisionsRoutingDecision[]Supervisor route choices with candidates and reasons.
approvalsRunApproval[]Approval requests with their status and decider.
artifactsRunArtifact[]Keyed values produced by the run, such as parallel:merged.
errorsRunError[]Errors recorded by failed agents, tools, or the run itself.
metricsRunMetricsAggregated counters and latency (see below).
finalOutputJsonValue | nullThe validated output once the run completes.
createdAt / startedAt / completedAtstring | nullISO timestamps for the run lifecycle.

Metrics#

state.metrics is recomputed after every event:

MetricTypeDescription
totalLatencyMsnumber | nullWall-clock duration, set when the run completes or fails.
tokenUsage{ inputTokens, outputTokens, totalTokens } | nullAggregated token usage when the provider reports it.
agentCallCountnumberLength of agentCalls.
toolCallCountnumberLength of toolCalls.
policyDecisionCountnumberLength of policyDecisions.
approvalCountnumberLength of approvals.

Run options#

Both workflow.run(input, options) and runWorkflow accept the same RunWorkflowOptions:

OptionTypeDescription
sinksTraceSink[]Trace destinations for this run — console, JSONL, platform, or your own. Defaults to none. See tracing.
runIdstringExplicit run id. Defaults to a generated id. Set it when you need to resume or replay a known run.
now() => DateClock override for deterministic timestamps in tests.
runStateDirectorystringWhere run state JSON is written. Defaults to .assemble/runs.
persistRunStatebooleanPersist RunState to <runStateDirectory>/<runId>.json after each step. Defaults to true; required for resumeRun and replay.
ts
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:

ts
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.