Skip to main content
This page traces the complete lifecycle of a single workflow step through every layer of Forge. Follow the request as it moves from an operator’s click in the Forge Console, through Convex Convex’s interpreter, the Trust Fabric Trust Fabric governance pipeline, the Bridge Bridge execution router, a gateway adapter, and back again.
This traces the Bridge-dispatched async path — the most common execution model. For Convex-native agents (Factory Manager, Task Manager, Copilot), see Convex-Native Agents. Those agents run inside Convex and never touch Bridge.

1. Operator Action

The flow begins when an operator clicks Run in the Forge Console.
Forge ConsoleForge Consoleapps/burgundy

The dashboard calls the interpreter.startWorkflow Convex mutation. This is a public mutation protected by requirePermission(ctx, “workflow:execute”).

DetailValue
Fileconvex/interpreter.ts
FunctionstartWorkflow (public mutation, line ~2060)
AuthrequirePermission(ctx, "workflow:execute")
Delegates tostartWorkflowImpl() (private, line ~1977)
startWorkflowImpl does three things:
  1. Loads the workflow definition — fetches the workflows and workflowVersions documents, pinning the YAML definition at run start (immutable for the run’s lifetime).
  2. Resolves the gateway — checks for a pinned gatewayId from the active deployment, or falls back to the org’s default gateway.
  3. Creates the workflowRuns record — inserts a new row with status "running", a fresh traceId and rootSpanId, then starts the durable workflow:
// convex/interpreter.ts — startWorkflowImpl (simplified)
const runId = await ctx.db.insert("workflowRuns", {
  workflowId: args.workflowId,
  workflowVersion: workflowDef.currentVersion,
  definition: version.definition,  // pinned YAML string
  gatewayId,
  executionAuthorityPosture,
  status: "running",
  startedAt: Date.now(),
  traceId,
  rootSpanId,
});

const workflowId = await workflow.start(
  ctx,
  internal.interpreter.lobsterXInterpreter,
  { runId, definition: version.definition, workflowName, ... }
);
The workflow.start() call uses @convex-dev/workflow’s WorkflowManager to launch a durable workflow. The workflow survives Convex function restarts and resumes from the last committed step.
Cross-links: Workflow Execution Flow | .lobsterX Format

2. DAG Resolution

Inside the durable workflow handler (lobsterXInterpreter), the interpreter parses the YAML and computes the execution graph.
ConvexConvex Interpreterconvex/interpreter.ts

Deterministic graph resolution — no LLM tokens burned. Convex walks the DAG, the LLM executes steps.

DetailValue
Fileconvex/interpreter.ts
Key functionresolveExecutionLayers() (from convex/lib/interpreter_utils.ts)
InputArray of LobsterXStep objects parsed from YAML
OutputOrdered execution layers — steps within a layer can run in parallel
The interpreter’s event loop then iterates:
  1. Find ready steps — steps whose depends_on dependencies are all completed or skipped, and which have not been dispatched, failed, or cancelled.
  2. Evaluate conditions — if a step has a condition field, evaluate it against accumulated outputs and variables. Steps where the condition is not met are marked skipped.
  3. Dispatch — each ready step is dispatched based on its type: agent step, approval gate, or sub-workflow.
  4. Await completionstep.awaitEvent({ name: "completion" }) blocks until a completion event arrives.
  5. Process and loop — the completed step’s outputs are accumulated, variables updated, and the loop returns to step 1.
// convex/interpreter.ts — event loop (simplified)
while (true) {
  const readySteps = findReadySteps(def.steps, completedSteps, skippedSteps, ...);

  for (const step of readySteps) {
    await dispatchAgentStep(step, lobsterXStep, ctx, 0);
    dispatchedSteps.add(step.id);
  }

  // Wait for any one step to complete
  const event = await step.awaitEvent({ name: "completion" });
  // Process completion, update state, loop back...
}
Cross-links: Interpreter Deep Dive | Checkpoints

3. Governance Evaluation

Before any step reaches the network, the Trust Fabric Trust Fabric evaluates it.
Trust FabricTrust Fabric — The Covenantconvex/trust_fabric/

A fail-fast gate pipeline. The first rejection terminates evaluation and returns a structured GovernanceDecision.

The dispatchStep action (in convex/dispatch.ts) calls the governance engine before doing anything else:
// convex/dispatch.ts — dispatchStep (line ~90)
let decision = await ctx.runAction(
  internal.trust_fabric.bindings.runGovernanceGates,
  { agentId, gatewayId, stepId, runId, dispatchType: "step" }
);
This invokes evaluateGovernance() in convex/trust_fabric/engine.ts, which runs the dispatch pipeline — 10 sequential gates:
#GateError CodePurpose
1gatewayHealthgateway_unreachableIs the target gateway reachable?
2agentStatusagent_disabledIs the agent enabled and active?
3concurrencyagent_busyIs the agent under its max concurrent step limit?
4rateLimitrate_limitedHas the agent exceeded its dispatch rate?
5budgetAgentbudget_exceededHas the agent’s monthly spend exceeded its ceiling?
6budgetEnvelopesbudget_exceededHas any scoped budget envelope been breached?
7trustLeveltrust_level_insufficientDoes the agent meet the gateway’s minimum trust level?
8contextTrustcontext_untrustedIs the execution context trusted for this agent’s role?
9policyRulespolicy_blockedDo dispatch policies permit this action?
10approvalRequiredDoes this step require human approval before proceeding?
Gates 5—8 use POC reduced enforcement for agents in the poc lifecycle phase. These gates log warnings but do not block dispatch, allowing teams to iterate quickly during prototyping.

Decision Outcomes

The engine returns a GovernanceDecision with one of three dispositions:
DispositionWhat happensCode path
allowStep proceeds to dispatchNormal flow continues
blockStep is marked failed, SafetyGateError thrownstepResults.markFailed + audit event
holdApproval request created, step waitstrust_fabric.approvals.mutations.create
The dispatchStep action retries retryable blocks up to 3 times with exponential backoff (1s, 2s, 4s) before giving up.
// convex/dispatch.ts — governance retry loop (simplified)
for (let attempt = 0; attempt < 3; attempt++) {
  if (decision.disposition !== "block") break;
  if (!decision.blockedBy?.retryable) break;
  await sleep(backoffDelays[attempt]);
  decision = await runGovernanceGates(...);
}
Every decision is persisted to the audit trail via persistGovernanceDecision(). Cross-links: Governance Pipeline | Safety Gates | Trust Fabric Overview | Approvals | Agent Trust

4. Dispatch

If the Trust Fabric allows the step, dispatchStep performs three operations atomically before hitting the network.
ConvexDispatch Actionconvex/dispatch.ts

Atomic concurrency check, mark step running, then fire-and-forget HTTP dispatch to Bridge.

DetailValue
Fileconvex/dispatch.ts
FunctiondispatchStep (internalAction, line ~69)
Key mutationstepResults.atomicCheckConcurrencyAndMarkRunning

Step-by-step:

a) Atomic concurrency check + mark running A single mutation combines the concurrency gate and markRunning to eliminate the TOCTOU race between separate query + mutation calls:
// convex/dispatch.ts (line ~278)
const concurrencyResult = await ctx.runMutation(
  internal.stepResults.atomicCheckConcurrencyAndMarkRunning,
  { runId, stepId, agent, maxConcurrentSteps, attempt, runtimeProvider, executionPath: "bridge" }
);
If the agent is at its concurrency limit, the step is marked failed with agent_busy and a SafetyGateError is thrown. b) Build callback URL The callback URL is derived from CONVEX_SITE_URL (or transformed from CONVEX_URL):
${convexSiteUrl}/api/workflow/step-complete
c) Dispatch to Bridge via adapter The adapter’s dispatchStep() method sends the request to Bridge with 3-attempt retry and exponential backoff (1s, 4s, 16s):
// convex/dispatch.ts (line ~448)
const response = await adapter.dispatchStep(gateway, {
  runId, stepId, workflowName, agent, command, stdin,
  callbackUrl,
  governanceConstraints,  // scope constraints from Trust Fabric
  conversationThreadId,   // for real-time streaming
  traceId,                // distributed trace context
  ...
});
The payload includes governance scope constraints (environment eligibility, tool allow/deny lists, data access restrictions) propagated from the Trust Fabric decision. Cross-links: Bridge Architecture | Gateways | Traceability

5. Adapter Execution

Bridge receives the dispatch, ACKs immediately, and delegates to the adapter.
BridgeBridgepackages/bridge/src/

Async execution router. Receives dispatch, returns 202 immediately, executes via adapter in the background.

Bridge endpoint

DetailValue
Filepackages/bridge/src/server.ts
RoutePOST /api/workflow/dispatch (line ~503)
AuthauthenticateToken middleware (bearer token)
The endpoint validates the request, returns 202 Accepted immediately, then delegates execution asynchronously:
// packages/bridge/src/server.ts (line ~530)
res.status(202).json({ status: "dispatched", run_id, step_id, dispatched_at });

setImmediate(async () => {
  await dispatchStepV2(adapters, dispatchBody);
});

Dispatch orchestrator

DetailValue
Filepackages/bridge/src/dispatch-orchestrator.ts
FunctiondispatchStepV2() (line ~319)
The orchestrator:
  1. Resolves the adapter based on the provider field in the request. In multi-adapter mode, Bridge loads multiple adapters from BRIDGE_ADAPTERS env var.
  2. Assembles the ExecutionRequest — builds the full request with output strategy, model, timeout, and tools.
  3. Calls adapter.execute(request) — this is where the actual LLM call happens, with timeout enforcement via AbortController.
  4. Normalizes the output via OutputNormalizer — parses structured output, extracts file content, resolves output strategies.
  5. Builds the callback payload with status, outputs, telemetry (tokens, cost, model), and conversation messages.

Adapter example: Vercel AI

DetailValue
Filepackages/adapter-vercel-ai/src/provider.ts
ClassVercelAIProvider implements ExecutionAdapter
The adapter dynamically loads AI SDK providers (Anthropic, OpenAI, Google), resolves the model, and calls generateText():
// packages/adapter-vercel-ai/src/provider.ts (simplified)
async execute(request: ExecutionRequest): Promise<ExecutionResult> {
  const { generateText } = await import("ai");
  const model = this.resolveModel(request.model);
  const result = await generateText({ model, prompt: request.message, ... });
  return { status: "success", text: result.text, durationMs, usage };
}
Other adapters (Claude SDK, OpenClaw) follow the same ExecutionAdapter interface but use their native SDKs. Cross-links: Bridge Architecture | Writing Adapters | Gateways

6. Callback

When the adapter finishes (or fails), Bridge delivers the result back to Convex.
BridgeCallbackBridge —> Convex HTTP action

Bridge POSTs the result to Convex’s /api/workflow/step-complete endpoint with 3x retry.

Bridge side

DetailValue
Filepackages/bridge/src/dispatch-orchestrator.ts
FunctioncallbackWithRetry() (invoked at line ~502)
After execution completes, Bridge sends the callback payload:
// Callback payload shape
{
  run_id, step_id,
  status: "completed" | "failed" | "timed_out",
  outputs: { ... },
  started_at, completed_at,
  error: null | string,
  model, input_tokens, output_tokens, thinking_tokens,
  total_tokens, cost, session_id,
  conversation: [ ... ]  // if adapter captured messages
}

Convex side

DetailValue
Fileconvex/http.ts
RoutePOST /api/workflow/step-complete (line ~28)
AuthX-Gateway-Token header verified against gateway registry
The HTTP action handler performs a 10-step sequence:
  1. Verify gateway token — looks up gateway by token hash.
  2. Parse and validate — validates against stepCompleteSchema, range-checks numeric fields (tokens, cost).
  3. Idempotency checkinsertIfNotDuplicate prevents duplicate processing of the same callback.
  4. Check workflow status — if the run is cancelled, ACK but discard the result.
  5. Update step resultmarkStepCompleted or markStepFailed depending on status.
  6. Log audit event — records step_completed or step_failed with full detail.
  7. Record telemetry — inserts a tokenEvents record for cost attribution.
  8. Schedule post-execution governance — evaluates budget thresholds, triggers alerts if spend is approaching limits.
  9. Persist logs and conversation — stores step logs as NDJSON blobs, persists conversation transcripts to agent threads.
  10. Fire completion event — resumes the waiting interpreter:
// convex/http.ts (line ~476)
await ctx.runMutation(internal.workflowEvents.fireCompletion, {
  workflowId: workflowRun.convexWorkflowId,
  type: "step_complete",
  stepId: step_id,
  status,
  outputs,
  ...
});
This fireCompletion mutation sends an event to the durable workflow, which unblocks the interpreter’s awaitEvent("completion") call. Cross-links: Audit Trail | Traces | Analytics

7. DAG Advance

Back in the interpreter, the completion event is received and the DAG advances.
ConvexInterpreter Event Loopconvex/interpreter.ts

The event loop processes the completion, accumulates outputs, resolves new dependencies, and dispatches the next wave of steps.

DetailValue
Fileconvex/interpreter.ts
EventawaitEvent({ name: "completion" }) (line ~677)
Completion handlerprocessAgentCompletion() from convex/lib/interpreter/completion_handlers.ts
When a step completes:
  1. Parse the completion event — extracts stepId, type, status, outputs, error.
  2. Validate outputs — if the step declares outputs in the .lobsterX definition, the interpreter validates and coerces them.
  3. Accumulate state — outputs are merged into accumulatedOutputs[stepId] and declared variables are updated.
  4. Save checkpoint — a workflow checkpoint is saved so a restart can resume from this point.
  5. Resolve next steps — the loop returns to step 1, finding steps whose dependencies are now satisfied.
  6. Termination check — when all steps are completed, skipped, failed, or cancelled, the loop exits.
// convex/interpreter.ts — completion processing (simplified)
const event = await step.awaitEvent({ name: "completion" });
const { result, error } = await processAgentCompletion(step, completedStep, ctx, event);

if (result === "completed") {
  completedSteps.add(completedStep.id);
  // Save checkpoint for restart resilience
  await saveCheckpoint(runId, completedSteps, skippedSteps, variables, accumulatedOutputs);
} else if (result === "retry") {
  retryDispatched.add(completedStep.id);
  await dispatchAgentStep(step, completedStep, ctx, 1);  // re-dispatch
}
// Loop back to find newly ready steps...
After the loop exits, the interpreter marks the workflow run as "completed" and logs the final audit event. Cross-links: Workflow Execution Flow | Checkpoints | Execution Overview

Failure Paths

Things go wrong. Here is what happens at each layer.

Governance blocks the step

TriggerBehavior
Any gate returns blockdispatchStep retries retryable blocks up to 3x, then marks the step failed via stepResults.markFailed, logs safety_gate_rejected audit event, throws SafetyGateError.
Gate returns holdAn approval request is created. The step stays in running status, waiting for human approval via the Approval Lifecycle.
The interpreter receives the SafetyGateError and applies the step’s on_failure policy:
  • halt (default) — the workflow stops, recording a failure snapshot for restart.
  • skip — the step is marked skipped, downstream steps cascade-skip if all their dependencies were skipped.
  • retry_once — the step is re-dispatched once. If it fails again, it halts.
  • retry_once_then_escalate — an escalation approval is created. A human can approve retry or abort.

Adapter execution fails

TriggerBehavior
Adapter throwsBridge catches the error, builds a failed callback payload with error_code classification, delivers callback to Convex.
TimeoutAbortController cancels the adapter after timeoutMs. Bridge sends a timed_out callback. A best-effort cancel is sent to the adapter.

Callback delivery fails

TriggerBehavior
Callback HTTP errorBridge retries up to 3x with backoff. If all attempts fail, the error is logged but the step remains in running status on the Convex side.
Convex timeout enforcerConvex’s cron-based timeout enforcer independently detects steps that have been running beyond their timeout. It marks them failed and fires a completion event, even if Bridge’s callback never arrived.

Duplicate callbacks

The HTTP handler uses insertIfNotDuplicate for atomic idempotency. If Bridge retries a callback that Convex already processed, the duplicate is ACKed ({ received: true, deduplicated: true }) without side effects. If the timeout enforcer already handled a timed-out step, any late gateway callback only reconciles cost — it does not replay audit events or fire a second completion.

DAG stall detection

If the interpreter finds no ready steps and no in-flight steps, but not all steps are resolved, it throws a stall error:
Workflow stalled: N step(s) have unmet dependencies: step_a, step_b
This typically indicates a circular dependency or a missing step definition. Cross-links: Safety Gates | Approval Lifecycle | Budgets

Data Shape Reference

Key types that flow through this pipeline:
TypeLocationUsed at
LobsterXDocument@tangogroup/sharedInterpreter YAML parsing
LobsterXStep@tangogroup/sharedStep definitions within the DAG
GovernanceDecisionconvex/trust_fabric/types.tsTrust Fabric evaluation result
GovernanceEvaluationContextconvex/trust_fabric/engine.tsPre-fetched context for gate evaluation
ExecutionRequest@tangogroup/bridgeBridge adapter input
ExecutionResult@tangogroup/bridgeBridge adapter output
CallbackPayloadpackages/bridge/src/dispatch-orchestrator.tsBridge-to-Convex callback body
StepCompleteEventconvex/lib/interpreter/helpers.tsParsed completion event in interpreter

Summary Diagram

The complete flow, layer by layer:

END-TO-END DATA FLOW

1
Forge Console calls interpreter.startWorkflow
|
2
Interpreter parses YAML, resolveExecutionLayers() builds the DAG
|
3
Trust Fabric evaluates 10 gates: evaluateGovernance() returns allow / block / hold
|
4
Atomic concurrency check + mark running, then HTTP dispatch to Bridge
|
5
Bridge ACKs 202, adapter calls execute() on the gateway (LLM call happens here)
|
6
Bridge POSTs callback to /api/workflow/step-complete with 3x retry
|
7
Convex processes callback, fires fireCompletion, interpreter resumes and advances the DAG

Next Steps