F
Flowman docsWorkflow SDK
Back to app
Workflow step actions

Sub-agent wait and review loop

Sources: src/workflows/shared/actions/sub-agent-dispatch.ts and src/workflows/shared/actions/sub-agent-review-loop.ts. Sub-agent dispatch is fire-and-forget by default; these helpers add an opt-in wait: the child run carries the parent's Workflow SDK webhook from creation, and Convex delivers the terminal payload (status + runs.output) with bounded retries when the child closes.

Helpers

  • dispatchSubAgentAndWait — dispatches a registered Sub-agent workflow with a completion webhook and durably suspends until the child reports. A child that finishes failed or cancelled is returned in the payload, not thrown. Idempotency-reused runs are re-bound to the new webhook, with immediate delivery when already terminal.
  • openAndWaitForFollowUpTurn (in src/workflows/shared/actions/coding-agent.ts) — queues a custom-prompt follow-up turn in an existing local coding session and waits for its webhook. Declines (busy, done, or cloud session) are returned structurally as { queued: false, reason }.
  • runSubAgentReviewLoop — composes both: dispatch a review Sub-agent, evaluate its findings, run a fix follow-up in the parent session, and re-dispatch until the review is clean or the iteration cap is reached.

Waiting for one Sub-agent

typescript
import { dispatchSubAgentAndWait } from "@/workflows/shared/actions/sub-agent-dispatch";

const result = await dispatchSubAgentAndWait({
  record,
  dispatch: {
    // Illustrative slug: dispatch resolves workflows through
    // src/lib/sub-agent-workflow-registry.ts and throws for unregistered
    // slugs. Register your child workflow there first; today only
    // "ticket-voice-summary" and "pull-request-ready" exist.
    workflowSlug: "visual-review",
    taskKey: "visual-review",
    userMessage: "Review the implemented screens for layout problems.",
    subject: { kind: "run", id: record.runSlug },
    context: { version: 1, origin: { type: "workflow", phase: "review" } },
    idempotencyKey: `${record.runSlug}:visual-review:iter-1`,
  },
});

// result.status: "completed" | "failed" | "cancelled"
// result.output: the child run's terminal output, verbatim

Review loop

typescript
import { runSubAgentReviewLoop } from "@/workflows/shared/actions/sub-agent-review-loop";

const review = await runSubAgentReviewLoop({
  record,
  sessionId, // the parent coding session that receives fix turns
  dispatch: {
    workflowSlug: "visual-review",
    taskKey: "visual-review",
    userMessage: "Review the implemented screens.",
    subject: { kind: "run", id: record.runSlug },
    context: { version: 1, origin: { type: "workflow", phase: "review" } },
  },
  evaluate: (payload) => {
    if (payload.status !== "completed") {
      return { verdict: "abort", reason: "Review run did not complete." };
    }
    const findings = readFindings(payload.output); // caller-defined parser
    return findings.length === 0
      ? { verdict: "clean" }
      : { verdict: "issues", followUpPrompt: renderFixPrompt(findings) };
  },
  maxIterations: 5, // default 5, clamped to 1–10
});

// review.outcome: "clean" | "max_iterations" | "aborted"

Guardrails and failure behavior

  • maxIterations defaults to 5 and is clamped to 1–10. Convex additionally refuses attempt numbers beyond 25 per (parent, task, subject) family as defense in depth.
  • Every waited dispatch is replay-safe: the loop supplies a per-iteration idempotency key, and dispatchSubAgentAndWait derives one from the parent run and wait step key when the caller omits it — so workflow step replays reuse the same child run instead of spawning duplicates.
  • evaluate must be pure and deterministic — it runs in workflow context on every replay.
  • Webhook delivery is at-least-once and durable: a fast retry ladder (5s, 30s, 120s) followed by slow redelivery every 10 minutes for roughly 5.5 hours, so a webhook-endpoint outage cannot permanently strand a waiting parent. A webhook that is gone (parent already resumed or ended) is absorbed without affecting the closed child run. Cancelling either side is safe: a cancelled child delivers its cancelled status to the waiting parent, and cancelling the parent lets an in-flight child finish.
  • Fix follow-up turns require a local parent coding session; cloud sessions are declined with cloud_session and the loop ends as aborted.