Repository files navigation

relayflows

npmLicense

Workflow engine and CLI for Agent Relay. Orchestrate multi-agent workflows using YAML, TypeScript, or Python. Define agents, wire up dependencies, and let the runner handle execution, retries, and verification.

Quick Start

CLI

# Run a YAML workflow
relayflows run workflow.yaml
# Run a TypeScript workflow
relayflows run workflow.ts
# Run a Python workflow
relayflows run workflow.py
# Run a specific named workflow from a file
relayflows run workflow.yaml --workflow deploy

TypeScript

import{workflow}from"@relayflows/core";constresult=awaitworkflow("ship-feature").pattern("dag").agent("planner",{cli: "claude",role: "Plans implementation"}).agent("developer",{cli: "codex",role: "Writes code"}).agent("reviewer",{cli: "claude",role: "Reviews code"}).step("plan",{agent: "planner",task: "Create implementation plan for user authentication",}).step("implement",{agent: "developer",task: "Implement the plan",dependsOn: ["plan"],}).step("review",{agent: "reviewer",task: "Review the implementation",dependsOn: ["implement"],}).run();console.log(result.status);// "completed" | "completed_early" | "failed" | "cancelled" | "needs_human"

Python

fromagent_relayimportworkflowresult= (
workflow("ship-feature")
.pattern("dag")
.agent("planner", cli="claude", role="Plans implementation")
.agent("developer", cli="codex", role="Writes code")
.agent("reviewer", cli="claude", role="Reviews code")
.step("plan", agent="planner", task="Create implementation plan for user auth")
.step("implement", agent="developer", task="Implement the plan", depends_on=["plan"])
.step("review", agent="reviewer", task="Review the implementation", depends_on=["implement"])
.run()
)

Watching a Run

When a run starts, the runner mints and prints a link you can open to follow it live:

[workflow 00:02] Workspace created for this workflow.
[workflow 00:02] Observer: https://agentrelay.com/observer?key=ot_live_...
[workflow 00:02] Channel: wf-ship-feature-a1b2c3

Open the Observer: URL and you see messages, agent activity, handoffs, and deliveries in real time. The link carries a scoped observer token (ot_live_): read-only, expiring in 24 hours, and individually revocable.

Minting is best-effort, so a link is not guaranteed — see If no link appears. A failed mint never fails the run.

Treat the link itself as a shared secret. The token is a bearer credential in a query parameter, so anyone who gets the URL can read the stream it covers until it expires or you revoke it. It is far safer than a workspace key — it cannot send, spawn, or administer — but it is not public.

The two ways a run gets a workspace

Which link you get depends on where the run's Relaycast workspace comes from.

No RELAY_API_KEY set — the runner creates a throwaway workspace for this run alone and mints an observer link covering all of it, DMs included. The workspace is anonymous and disappears from your reach when the run ends, so copy the link while the run is going. Nothing persists the underlying key, and there is no way to recover it afterward.

RELAY_API_KEY set — the runner uses your workspace and mints a link scoped to just this run's channel, with agent DMs excluded, so the link does not expose unrelated traffic in a shared workspace. This is the better setup for anything you may want to revisit: the run is in a workspace you own, so you can mint fresh links whenever you like.

# One-time: create a workspace you own and keep the key
curl -sX POST https://api.relaycast.dev/v1/workspaces \
-H 'content-type: application/json' -d '{"name":"my-workflows"}' \
| jq -r '.data.api_key // .api_key'export RELAY_API_KEY=rk_live_... # put this in your shell profile
relayflows run workflow.yaml

If you already use the Agent Relay CLI, agent-relay workspace key --reveal-secrets prints the key of your active workspace. Note it is masked without --reveal-secrets.

Minting more links yourself

With a workspace you own, agent-relay observer mints links on demand:

agent-relay observer # read-only link, 24h, DMs excluded
agent-relay observer --channels wf-ship-a1b2c3 # scope to one run
agent-relay observer --include-dms # include agent DMs
agent-relay observer --expires 7d # longer-lived link
agent-relay observer list # what is outstanding
agent-relay observer revoke <id># cut one off immediately

Never share the workspace key

A workspace key (rk_live_) is an administrative credential — it can send messages, spawn and remove agents, and change workspace settings. Do not put one in an observer URL, a chat message, or a terminal transcript; query strings end up in browser history, referrer headers, and proxy logs. The realtime endpoint rejects it outright, so a link built from one cannot stream anyway.

Workspace key (rk_live_)Observer token (ot_live_)
Read messages and activityyesyes
Send, spawn agents, administeryesno
Expiresnoyes
Revocable individuallynoyes
Scopable to channelsnoyes

The runner only ever prints ot_live_ links, and scrubs rk_live_ values out of channel output.

Configuration

VariablePurpose
RELAY_API_KEYWorkspace key to run against. Unset means a throwaway workspace per run.
RELAY_OBSERVER_URLObserver dashboard base. Defaults to https://agentrelay.com/observer.
RELAY_OBSERVER_EXPIRESLink lifetime as 30m / 24h / 7d. Defaults to 24h; unparseable or over 90d falls back to 24h.
RELAYCAST_BASE_URLRelaycast engine base. Defaults to https://api.relaycast.dev.

If no link appears

  • Observation: unavailable — the token could not be minted (engine unreachable, or it rejected the request). The run is unaffected; minting is best-effort by design and never fails a run.
  • No observer lines at all — the run needed no broker: every step was deterministic, worktree, integration, or waitFor, or an external executor handled agent spawning, or Relaycast was disabled with AGENT_RELAY_WORKFLOW_DISABLE_RELAYCAST=1.
  • Observation: run agent-relay observer — you set RELAY_API_KEY and minting failed. Mint a link by hand with agent-relay observer.

Consumer-Facing Apps + AI SDK Communicate Flows

A good production split is:

  1. AI SDK app handles the user conversation and streaming UI
  2. Communicate / onRelay() lets that point-person coordinate with specialists over Relay
  3. Workflows / runWorkflow() take over when a request needs multi-step execution, verification, or handoffs
import{streamText,wrapLanguageModel}from'ai';import{openai}from'@ai-sdk/openai';import{Relay}from'@agent-relay/sdk/communicate';import{onRelay}from'@agent-relay/sdk/communicate/adapters/ai-sdk';import{runWorkflow}from'@relayflows/core';exportasyncfunctionPOST(req: Request){const{ prompt, escalate, repo }=awaitreq.json();constrelay=newRelay('AppLead');constrelaySession=onRelay({name: 'AppLead',instructions: 'You are the customer-facing lead. Keep the user updated and delegate implementation via Relay when needed.',},relay);constmodel=wrapLanguageModel({model: openai('gpt-4o-mini'),middleware: relaySession.middleware,});if(escalate){constworkflow=awaitrunWorkflow('workflows/feature-dev.yaml',{vars: {task: prompt, repo },});returnResponse.json({status: workflow.status,runId: workflow.runId});}returnstreamText({
model,tools: relaySession.tools,system: 'Answer directly when possible; coordinate internally when the task needs specialists.',
prompt,}).toUIMessageStreamResponse({onFinish(){relaySession.cleanup();voidrelay.close();},});}

That pattern keeps the user experience snappy while still letting longer Relay workflows run with proper ownership, retries, and verification.

YAML Format

Workflows are defined as relay.yaml files:

version: "1.0"name: my-workflowdescription: "Optional description"swarm:
pattern: dag # Execution pattern (see Patterns below)maxConcurrency: 3# Max agents running in paralleltimeoutMs: 3600000# Global timeout (1 hour)channel: my-channel # Relay channel for agent communicationagents:
- name: backendcli: claude # claude | codex | gemini | aider | goose | opencode | droidrole: "Backend engineer"constraints:
model: opustimeoutMs: 600000retries: 2
- name: testercli: codexrole: "Test engineer"interactive: false # Non-interactive: runs as subprocess, no PTY/messaging# A persona replaces cli + role. Its harness, model, standing instructions,# installed skills, MCP servers, and harness settings come from the spec.# Persona agents are interactive-only: do not set cli, preset, or# constraints.model, and do not use interactive: false.
- name: integrationspersona: nango-integrationsworkflows:
- name: build-and-testonError: retry # fail | skip | retrysteps:
- name: build-apiagent: backendtask: "Build the REST API endpoints for user management"verification:
type: file_existsvalue: "src/api/users.ts"retries: 1
- name: write-testsagent: testertask: "Write integration tests for: {{steps.build-api.output}}"dependsOn: [build-api]
- name: run-testsagent: testertask: "Run the test suite and report results"dependsOn: [write-tests]verification:
type: exit_codevalue: "0"errorHandling:
strategy: retrymaxRetries: 2retryDelayMs: 5000repairAgent: testerrepairRetries: 2onExhaustion: needs-humannotifyChannel: my-channel

Template Variables

Use {{variable}} for user-provided values and {{steps.STEP_NAME.output}} for previous step outputs:

steps:
- name: planagent: plannertask: "Plan implementation for: {{task}}"# User variable
- name: implementagent: developerdependsOn: [plan]task: "Implement: {{steps.plan.output}}"# Previous step output

User variables are passed via the CLI or programmatically:

awaitrunWorkflow("workflow.yaml",{vars: {task: "Add OAuth2 support"},});

Blocking Slack Questions

Workflows can pause on a Slack question by adding a Slack integration step with action: askQuestion. The step posts a question, waits for the first human reply in the message thread, exposes that answer through {{steps.<name>.output}}, and can inject it into a running agent when injectToAgent is set.

workflows:
- name: defaultsteps:
- name: ask-humantype: integrationintegration: slackaction: askQuestionparams:
channel: "#engineering"text: "The implementer is blocked on migration strategy. Which path should it take?"waitTimeoutMs: "3600000"injectToAgent: "backend-runtime-name"injectTemplate: "HUMAN_ANSWER: {{answer.text}}"output: '{"format":"text","path":"answer.text"}'
- name: continue-with-answeragent: backenddependsOn: [ask-human]task: "Continue using this human guidance: {{steps.ask-human.output}}"

askQuestion can use the local Slack API runtime, or Relayfile-backed Slack writebacks when the workflow already has a Relayfile Slack integration.

Interactive agent steps can also opt into marker-driven assistance. With humanAssistance.slack enabled, an agent can print a line beginning with HUMAN_QUESTION:. The runner posts that question to Slack, blocks while waiting for a human reply, then injects HUMAN_ANSWER: ... back into that same agent session. If integrations.relayfile is present, Relayflows automatically uses the existing Relayfile/Pear Slack connection; no Slack bot token, Relayfile workspace id, or Relayfile token is required in the workflow.

Runnable TypeScript proof: examples/typescript/slack-human-assistance-e2e.ts.

swarm:
pattern: daghumanAssistance:
slack:
channel: proj-cloudtimeoutMs: 3600000integrations:
relayfile: {}workflows:
- name: defaultsteps:
- name: implementagent: backendtask: "Proceed, but ask for human guidance if the migration strategy is ambiguous."

Relayfile Event Subscriptions

Relayflows can subscribe to Relayfile integration events and inject matching events into active agents. Workflow-level subscriptions live under integrations.subscriptions; agent-level subscriptions use Workforce-style agents[].watch or Relayflows-style agents[].subscriptions.

integrations:
relayfile: {}subscriptions:
- name: pr-feedbackprovider: githubpath: /github/repos/acme/web/pulls/42/**events: [created, updated]agents: [pr-babysitter]agents:
- name: pr-babysittercli: codexwatch:
- paths: [/github/repos/acme/web/pulls/42/reviews/**]events: [created, updated]workflows:
- name: defaultsteps:
- name: babysit-pragent: pr-babysittertask: | Stay active and wait for INTEGRATION_EVENT messages about PR feedback. Read the Relayfile path from the event, address comments until no open feedback remains, then notify the user in Slack.

Verification Checks

Each step can include a verification check. Verification is one input to the runner's completion decision pipeline — when verification passes, the step completes even without a sentinel marker.

TypeDescription
exit_codeAgent must exit with the specified code (preferred for code-editing steps)
file_existsA file must exist at the specified path after the step
output_containsStep output must contain the specified string (optional accelerator)
customNo-op in the runner; handled by external callers
# Preferred — deterministic verificationverification:
type: exit_codevalue: "0"description: "Process exited successfully"# Also valid — output_contains as an optional acceleratorverification:
type: output_containsvalue: "IMPLEMENTATION_COMPLETE"description: "Agent confirms completion (optional fast-path)"

Completion Decision Pipeline

The runner uses a multi-signal pipeline to decide step completion:

  1. Deterministic verification — if a verification check passes, the step completes immediately (completed_verified)
  2. Owner decision — the step owner can issue OWNER_DECISION: COMPLETE|INCOMPLETE_RETRY|INCOMPLETE_FAIL (completed_by_owner_decision)
  3. Evidence-based completion — channel messages, file artifacts, and exit codes are collected as evidence (completed_by_evidence)
  4. Marker fast-pathSTEP_COMPLETE:<step-name> still works as an accelerator but is never required
Completion StateMeaning
completed_verifiedDeterministic verification passed
completed_by_owner_decisionOwner approved the step
completed_by_evidenceEvidence-based completion
retry_requested_by_ownerOwner requested retry
failed_verificationVerification explicitly failed
failed_owner_decisionOwner rejected the step
failed_no_evidenceNo verification, no owner decision, no evidence

Review parsing is tolerant: The runner accepts semantically equivalent outputs like "Approved", "Complete", "LGTM" — not just exact REVIEW_DECISION: APPROVE strings.

Swarm Patterns

The swarm.pattern field controls how agents are coordinated:

Core Patterns

PatternDescription
dagDirected acyclic graph — steps run based on dependency edges (default)
fan-outAll agents run in parallel
pipelineSequential chaining of steps
hub-spokeCentral hub coordinates spoke agents
consensusAgents vote on decisions
meshFull communication graph between agents
handoffSequential handoff between agents
cascadeWaterfall with phase gates
debateAgents propose and counter-argue
hierarchicalMulti-level reporting structure

Data Processing Patterns

PatternDescription
map-reduceSplit work into chunks (mappers), process in parallel, aggregate results (reducers)
scatter-gatherFan out requests to workers, collect and synthesize responses

Supervision & Quality Patterns

PatternDescription
supervisorMonitor agent monitors workers, restarts on failure, manages health
reflectionAgent produces output, critic reviews and provides feedback for iteration
verifierProducer agents submit work to verifier agents for validation

Adversarial & Validation Patterns

PatternDescription
red-teamAttacker agents probe for weaknesses, defender agents respond
auctionAuctioneer broadcasts tasks, agents bid based on capability/cost

Resilience Patterns

PatternDescription
escalationStart with fast/cheap agents, escalate to more capable on failure
sagaDistributed transactions with compensating actions on failure
circuit-breakerPrimary agent with fallback chain, fail fast and recover

Collaborative Patterns

PatternDescription
blackboardShared workspace where agents contribute incrementally to a solution
swarmEmergent behavior from simple agent rules (neighbor communication)

Auto-Selection by Role

When swarm.pattern is omitted, the coordinator auto-selects based on agent roles. Patterns are checked in priority order below (first match wins):

PriorityPatternRequired Roles/Config
1dagSteps with dependsOn
2consensusUses coordination.consensusStrategy config
3map-reducemapper + reducer
4red-team(attacker OR red-team) + (defender OR blue-team)
5reflectioncritic
6escalationtier-1, tier-2, etc.
7auctionauctioneer
8sagasaga-orchestrator OR compensate-handler
9circuit-breakerfallback, backup, OR primary
10blackboardblackboard OR shared-workspace
11swarmhive-mind OR swarm-agent
12verifierverifier
13supervisorsupervisor
14hierarchicallead (with 4+ agents)
15hub-spokehub OR coordinator
16pipelineUnique agents per step, 3+ steps
17fan-outDefault fallback

Error Handling

Step-Level

steps:
- name: risky-stepagent: workertask: "Do something that might fail"retries: 3# Retry up to 3 times on failuretimeoutMs: 300000# 5 minute timeout

Successful early termination

A deterministic gate can explicitly declare exit codes that mean “there is no work to do.” A matching code ends the run with the distinct completed_early status and skips every step that has not started:

steps:
- name: claim-worktype: deterministiccommand: node bin/claim-work.mjsterminalSuccessExitCodes: [78]
- name: process-claimagent: workertask: Process the claimed workdependsOn: [claim-work]

Terminal-capable gates are scheduling barriers, so other ready work does not race the gate. The triggering step is completed with completion reason completed_early_exit; remaining steps are skipped, and the CLI exits 0 while clearly reporting COMPLETED EARLY. Verification still applies, so a verification failure remains a real failure.

This behavior is opt-in. Without terminalSuccessExitCodes, exit 78 and every other non-zero exit retain their existing failure behavior. The new completed_early run status is an additive public API value: consumers with exhaustive status switches, strict validators, database constraints, or terminal-status polling must handle it separately from completed.

Workflow-Level

The onError field on a workflow controls what happens when a step fails:

ValueBehavior
fail / fail-fastStop immediately, skip downstream steps
skip / continueSkip downstream dependents, continue independent steps
retryRetry the step; deterministic gates ask a workflow agent to repair before each retry when an agent is available

Global

errorHandling:
strategy: retrymaxRetries: 2retryDelayMs: 5000repairAgent: testerrepairRetries: 2notifyChannel: alerts

Retry-mode workflows are repair-aware by default. Deterministic step failures, verification gate failures, and malformed agent artifacts are treated as repairable work before terminal failure. The runner chooses errorHandling.repairAgent when set, otherwise it uses the step's owning/upstream agent when possible, then falls back to the best available workflow agent. The selected agent gets the failed command or agent output, working directory, exit information, and captured evidence, then the failed gate or step is retried. Use repairRetries: 0, strategy: fail-fast, or strategy: continue when a workflow intentionally should not invoke repair agents. Set onExhaustion: needs-human to end an exhausted repairable run as needs_human instead of failed.

Built-in Templates

Six pre-built workflow templates are included:

TemplatePatternDescription
feature-devhub-spokePlan, implement, review, and finalize a feature
bug-fixhub-spokeInvestigate, patch, validate, and document a bug fix
code-reviewfan-outParallel multi-reviewer assessment with consolidated findings
security-auditpipelineScan, triage, remediate, and verify security issues
refactorhierarchicalAnalyze, plan, execute, and validate a refactor
documentationhandoffResearch, draft, review, and publish documentation

Using Templates

import{TemplateRegistry,WorkflowRunner}from"@relayflows/core";constregistry=newTemplateRegistry();// List available templatesconsttemplates=awaitregistry.listTemplates();// Load and run a templateconstconfig=awaitregistry.loadTemplate("feature-dev");construnner=newWorkflowRunner();constresult=awaitrunner.execute(config,undefined,{task: "Add WebSocket support to the API",});// Install a custom template from a URLawaitregistry.installExternalTemplate("https://example.com/my-template.yaml","my-template");

TypeScript Builder API

The builder constructs a RelayYamlConfig object and can run it, export it as YAML, or return the raw config.

import{workflow}from"@relayflows/core";// Build and runconstresult=awaitworkflow("my-workflow").pattern("dag").maxConcurrency(3).timeout(60*60*1000).channel("my-channel").agent("backend",{cli: "claude",role: "Backend engineer",model: "opus",retries: 2,}).agent("frontend",{cli: "codex",role: "Frontend engineer",interactive: false,// Non-interactive subprocess mode}).step("api",{agent: "backend",task: "Build REST API",verification: {type: "output_contains",value: "API_READY"},}).step("ui",{agent: "frontend",task: "Build the UI",dependsOn: ["api"],}).onError("retry",{maxRetries: 2,retryDelayMs: 5000}).run();// Or export to YAMLconstyaml=workflow("my-workflow").pattern("dag").agent("worker",{cli: "claude"}).step("task1",{agent: "worker",task: "Do something"}).toYaml();// Or get the raw config objectconstconfig=workflow("my-workflow").pattern("dag").agent("worker",{cli: "claude"}).step("task1",{agent: "worker",task: "Do something"}).toConfig();

Python Builder API

The Python builder ships with @agent-relay/sdk-py:

pip install agent-relay
fromagent_relayimportworkflow, run_yaml# Build and runresult= (
workflow("my-workflow")
.pattern("dag")
.max_concurrency(3)
.timeout(3600000)
.agent("backend", cli="claude", role="Backend engineer")
.agent("frontend", cli="codex", role="Frontend engineer")
.step("api", agent="backend", task="Build REST API")
.step("ui", agent="frontend", task="Build the UI", depends_on=["api"])
.on_error("retry", max_retries=2, retry_delay_ms=5000)
.run()
)
# Run an existing YAML fileresult=run_yaml("workflows/my-workflow.yaml")
# Export to YAML stringyaml_str= (
workflow("my-workflow")
.pattern("dag")
.agent("worker", cli="claude")
.step("task1", agent="worker", task="Do something")
.to_yaml()
)
# Get the raw config dictconfig= (
workflow("my-workflow")
.pattern("dag")
.agent("worker", cli="claude")
.step("task1", agent="worker", task="Do something")
.to_config()
)

Programmatic API

For full control, use the WorkflowRunner directly:

import{WorkflowRunner}from"@relayflows/core";construnner=newWorkflowRunner({cwd: "/path/to/project",// Working directory (default: process.cwd())relay: {port: 3000},// AgentRelay options (optional)});// Listen to events (broker:event fires frequently — filter it out for cleaner output)runner.on((event)=>{if(event.type==='broker:event')return;console.log(event.type,event);});// Parse and executeconstconfig=awaitrunner.parseYamlFile("workflow.yaml");construn=awaitrunner.execute(config,"workflow-name",{task: "Build the feature",});// Pause / resume / abortrunner.pause();runner.unpause();runner.abort();// Resume a failed runconstresumed=awaitrunner.resume(run.id);

Zero-Config Convenience Function

import{runWorkflow}from"@relayflows/core";constresult=awaitrunWorkflow("workflow.yaml",{workflow: "deploy",vars: {environment: "staging"},onEvent: (event)=>{if(event.type!=='broker:event')console.log(event.type);},});

Coordination

Barriers

Synchronization points that wait for specific steps to complete:

coordination:
barriers:
- name: all-reviews-donewaitFor: [review-arch, review-security, review-correctness]timeoutMs: 900000consensusStrategy: majority # majority | unanimous | quorum

Shared State

Agents can share state during execution:

state:
backend: memory # memory | redis | databasettlMs: 86400000namespace: my-workflow

Supported Agent CLIs

CLIDescription
claudeClaude Code (Anthropic)
codexCodex CLI (OpenAI)
geminiGemini CLI (Google)
aiderAider coding assistant
gooseGoose AI assistant
opencodeOpenCode CLI
droidDroid CLI

Non-Interactive Agents

By default, agents run in interactive PTY mode with full relay messaging. For workers that just need to execute a task and return output — common in fan-out, map-reduce, and pipeline patterns — set interactive: false to run them as lightweight subprocesses.

YAML

agents:
- name: leadcli: clauderole: "Coordinates work"# interactive: true (default) — full PTY, relay messaging, /exit detection
- name: workercli: codexrole: "Executes tasks"interactive: false # Runs "codex exec <task>", captures stdout

TypeScript

workflow("fan-out-analysis").pattern("fan-out").agent("lead",{cli: "claude",role: "Coordinator"}).agent("worker-1",{cli: "codex",interactive: false,role: "Analyst"}).agent("worker-2",{cli: "codex",interactive: false,role: "Analyst"}).step("analyze-1",{agent: "worker-1",task: "Analyze module A"}).step("analyze-2",{agent: "worker-2",task: "Analyze module B"}).step("synthesize",{agent: "lead",task: "Combine: {{steps.analyze-1.output}} + {{steps.analyze-2.output}}",dependsOn: ["analyze-1","analyze-2"],}).run();

How It Works

AspectInteractive (default)Non-Interactive
ExecutionFull PTY with stdin/stdoutchild_process.spawn() with piped stdio
CLI invocationStandard interactive sessionOne-shot mode (claude -p, codex exec, etc.)
Relay messagingCan send/receive messagesNo messaging — excluded from topology edges
Self-terminationMust output /exitProcess exits naturally when done
Output capturePTY output bufferstdout capture
OverheadHigher (PTY, echo verification, SIGWINCH)Lower (simple subprocess)

Non-Interactive CLI Commands

CLICommandNotes
claudeclaude -p "<task>"Print mode, exits after response
codexcodex exec "<task>"One-shot execution
geminigemini -p "<task>"Prompt mode
opencodeopencode --prompt "<task>"One-shot prompt
droiddroid exec "<task>"One-shot execution
aideraider --message "<task>" --yes-always --no-gitAuto-approve, skip git
goosegoose run --text "<task>" --no-sessionText mode, no session file

When to Use

  • Fan-out workers that process a task and return results
  • Map-reduce mappers that don't need mid-task communication
  • Pipeline stages that transform input to output
  • Any agent that doesn't need turn-by-turn relay messaging

When NOT to Use

  • Lead/coordinator agents that communicate with others
  • Agents in debate, consensus, or reflection patterns
  • Agents that need to receive messages during execution

Agent Slash Commands

Agents running inside a workflow can output slash commands to signal the broker. These are detected in the agent's PTY output at the broker level — the agent simply prints the command on its own line.

/exit

Signals that the agent has completed its current step and is ready to be released.

/exit

The workflow runner waits for each agent to /exit after delivering a step task. When the broker detects /exit in the agent's output (exact line match after ANSI stripping), it:

  1. Emits an agent_exit frame with reason: "agent_requested"
  2. Triggers graceful PTY shutdown

If an agent does not /exit within the step's timeoutMs, the runner treats the step as timed out. As a safety net, steps with file_exists verification will still pass if the expected file is present despite the timeout.

Best practice: Instruct agents to output /exit when done in your step task descriptions:

steps:
- name: build-apiagent: backendtask: | Build the REST API endpoints for user management. When finished, output /exit.

Idle Agent Detection and Nudging

Interactive agents sometimes finish their task but forget to /exit, sitting idle and blocking downstream steps. The runner can detect idle agents and take action automatically.

Configuration

Add idleNudge to your swarm config:

swarm:
pattern: hub-spokeidleNudge:
nudgeAfterMs: 120000# 2 min before first nudge (default)escalateAfterMs: 120000# 2 min after nudge before force-release (default)maxNudges: 1# Nudges before escalation (default)

All built-in templates include idle nudging with these defaults.

How It Works

  1. Detection: The broker tracks agent output timestamps and emits agent_idle events when an agent goes silent for the configured threshold
  2. Nudge: For hub patterns (hub-spoke, fan-out, hierarchical, etc.), the runner tells the hub agent to check on the idle agent. For non-hub patterns, a system message is injected directly into the agent's PTY
  3. Escalation: If the agent remains idle after maxNudges attempts, the runner force-releases it and captures whatever output was produced
  4. No config: When idleNudge is omitted, the runner uses simple waitForExit (backward compatible)

Events

The runner emits two new events for idle nudging:

EventDescription
step:nudgedFired when a nudge message is sent to an idle agent
step:force-releasedFired when an agent is force-released after exhausting nudges

Automatic Step Owner and Review

For interactive agent steps, the runner uses a point-person-led completion model:

  1. Elects a step owner (prefers lead/coordinator-style agents, falls back to the step agent)
  2. Runs a completion decision pipeline — checks deterministic verification first, then owner judgment, then evidence
  3. Owner can issue structured decisions via OWNER_DECISION: COMPLETE|INCOMPLETE_RETRY|INCOMPLETE_FAIL|NEEDS_CLARIFICATION with optional REASON: <text>
  4. Review parsing is tolerant — accepts "Approved", "Complete", "LGTM", not just exact REVIEW_DECISION: APPROVE
  5. Markers are optional acceleratorsSTEP_COMPLETE:<step-name> still works as a fast-path but is never required
  6. Stores primary output plus review output in the step artifact

Evidence-based completion: The runner collects channel messages, file artifacts, process exit codes, and coordination signals (e.g., WORKER_DONE posted in channel) as completion evidence. When sufficient evidence exists, the step completes without requiring any sentinel marker.

Deterministic and worktree steps are unchanged and do not require owner/review delegation.

Sandbox Execution

By default the runner spawns steps as local child processes. To run them in isolated sandboxes instead, select a provider — the runner still owns command construction, env, cwd, timeout, and the whole DAG/retry/verification pipeline; the provider only supplies "where the command runs".

# Off by default. Unset the flag to get local child processes back.export RELAYFLOWS_SANDBOX_PROVIDER=daytona
export DAYTONA_API_KEY=...
export RELAYFLOWS_SANDBOX_HOME_DIR=/home/daytona # image-specific, requiredexport RELAYFLOWS_SANDBOX_SNAPSHOT=my-snapshot # optional

Or in code:

import{WorkflowRunner}from"@relayflows/core";construnner=newWorkflowRunner({sandbox: {provider: "daytona",homeDir: "/home/daytona"},});
ProviderWhat it gives you
none (default)No sandbox. Local child processes, exactly as before.
daytonaReal remote sandboxes via @agent-relay/sandbox. Needs the optional peer @daytonaio/sdk.
local-processReal local processes in a private per-step directory with its own HOME. Isolates the filesystem root, not the machine — good for development and CI, not a security boundary.

Reversibility.provider: "none" (or an unset RELAYFLOWS_SANDBOX_PROVIDER) produces no backend at all, so nothing about the default path changes. An explicit executor or processBackend still wins over sandbox config, so a host that injects its own backend today keeps it.

Custom providers. Register a runtime under any name, or hand one in directly. This is the seam a host uses to plug in a runtime that does not live in this repo:

import{registerSandboxProvider,WorkflowRunner}from"@relayflows/core";registerSandboxProvider("my-runtime",(config)=>newMyRuntime(config));// ...or skip the registry entirely:newWorkflowRunner({sandbox: {runtime: myRuntime}});

A runtime needs five methods — launch, exec, uploadFile, getHomeDir, destroy — matching @agent-relay/sandbox's WorkflowRuntime.

Schema Validation

A JSON Schema is available at packages/core/src/schema.json for editor autocompletion and validation of relay.yaml files.

Development

npm install
npm run typecheck
npm run test

Requirements

  • Node.js 22+
  • @relayflows/cli installed (npm install -g @relayflows/cli)
  • For Python: Python 3.10+ with pip install agent-relay
  • For TypeScript workflow files: tsx or ts-node installed

License

Apache-2.0 — Copyright 2025 Agent Workforce Incorporated

About

Orchestrate multi-step, multi-agent execution across Agent Relay workers

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

relayflows

npmLicense

Workflow engine and CLI for Agent Relay. Orchestrate multi-agent workflows using YAML, TypeScript, or Python. Define agents, wire up dependencies, and let the runner handle execution, retries, and verification.

Quick Start

CLI

# Run a YAML workflow
relayflows run workflow.yaml
# Run a TypeScript workflow
relayflows run workflow.ts
# Run a Python workflow
relayflows run workflow.py
# Run a specific named workflow from a file
relayflows run workflow.yaml --workflow deploy

TypeScript

import{workflow}from"@relayflows/core";constresult=awaitworkflow("ship-feature").pattern("dag").agent("planner",{cli: "claude",role: "Plans implementation"}).agent("developer",{cli: "codex",role: "Writes code"}).agent("reviewer",{cli: "claude",role: "Reviews code"}).step("plan",{agent: "planner",task: "Create implementation plan for user authentication",}).step("implement",{agent: "developer",task: "Implement the plan",dependsOn: ["plan"],}).step("review",{agent: "reviewer",task: "Review the implementation",dependsOn: ["implement"],}).run();console.log(result.status);// "completed" | "completed_early" | "failed" | "cancelled" | "needs_human"

Python

fromagent_relayimportworkflowresult= (
workflow("ship-feature")
.pattern("dag")
.agent("planner", cli="claude", role="Plans implementation")
.agent("developer", cli="codex", role="Writes code")
.agent("reviewer", cli="claude", role="Reviews code")
.step("plan", agent="planner", task="Create implementation plan for user auth")
.step("implement", agent="developer", task="Implement the plan", depends_on=["plan"])
.step("review", agent="reviewer", task="Review the implementation", depends_on=["implement"])
.run()
)

Watching a Run

When a run starts, the runner mints and prints a link you can open to follow it live:

[workflow 00:02] Workspace created for this workflow.
[workflow 00:02] Observer: https://agentrelay.com/observer?key=ot_live_...
[workflow 00:02] Channel: wf-ship-feature-a1b2c3

Open the Observer: URL and you see messages, agent activity, handoffs, and deliveries in real time. The link carries a scoped observer token (ot_live_): read-only, expiring in 24 hours, and individually revocable.

Minting is best-effort, so a link is not guaranteed — see If no link appears. A failed mint never fails the run.

Treat the link itself as a shared secret. The token is a bearer credential in a query parameter, so anyone who gets the URL can read the stream it covers until it expires or you revoke it. It is far safer than a workspace key — it cannot send, spawn, or administer — but it is not public.

The two ways a run gets a workspace

Which link you get depends on where the run's Relaycast workspace comes from.

No RELAY_API_KEY set — the runner creates a throwaway workspace for this run alone and mints an observer link covering all of it, DMs included. The workspace is anonymous and disappears from your reach when the run ends, so copy the link while the run is going. Nothing persists the underlying key, and there is no way to recover it afterward.

RELAY_API_KEY set — the runner uses your workspace and mints a link scoped to just this run's channel, with agent DMs excluded, so the link does not expose unrelated traffic in a shared workspace. This is the better setup for anything you may want to revisit: the run is in a workspace you own, so you can mint fresh links whenever you like.

# One-time: create a workspace you own and keep the key
curl -sX POST https://api.relaycast.dev/v1/workspaces \
-H 'content-type: application/json' -d '{"name":"my-workflows"}' \
| jq -r '.data.api_key // .api_key'export RELAY_API_KEY=rk_live_... # put this in your shell profile
relayflows run workflow.yaml

If you already use the Agent Relay CLI, agent-relay workspace key --reveal-secrets prints the key of your active workspace. Note it is masked without --reveal-secrets.

Minting more links yourself

With a workspace you own, agent-relay observer mints links on demand:

agent-relay observer # read-only link, 24h, DMs excluded
agent-relay observer --channels wf-ship-a1b2c3 # scope to one run
agent-relay observer --include-dms # include agent DMs
agent-relay observer --expires 7d # longer-lived link
agent-relay observer list # what is outstanding
agent-relay observer revoke <id># cut one off immediately

Never share the workspace key

A workspace key (rk_live_) is an administrative credential — it can send messages, spawn and remove agents, and change workspace settings. Do not put one in an observer URL, a chat message, or a terminal transcript; query strings end up in browser history, referrer headers, and proxy logs. The realtime endpoint rejects it outright, so a link built from one cannot stream anyway.

Workspace key (rk_live_)Observer token (ot_live_)
Read messages and activityyesyes
Send, spawn agents, administeryesno
Expiresnoyes
Revocable individuallynoyes
Scopable to channelsnoyes

The runner only ever prints ot_live_ links, and scrubs rk_live_ values out of channel output.

Configuration

VariablePurpose
RELAY_API_KEYWorkspace key to run against. Unset means a throwaway workspace per run.
RELAY_OBSERVER_URLObserver dashboard base. Defaults to https://agentrelay.com/observer.
RELAY_OBSERVER_EXPIRESLink lifetime as 30m / 24h / 7d. Defaults to 24h; unparseable or over 90d falls back to 24h.
RELAYCAST_BASE_URLRelaycast engine base. Defaults to https://api.relaycast.dev.

If no link appears

  • Observation: unavailable — the token could not be minted (engine unreachable, or it rejected the request). The run is unaffected; minting is best-effort by design and never fails a run.
  • No observer lines at all — the run needed no broker: every step was deterministic, worktree, integration, or waitFor, or an external executor handled agent spawning, or Relaycast was disabled with AGENT_RELAY_WORKFLOW_DISABLE_RELAYCAST=1.
  • Observation: run agent-relay observer — you set RELAY_API_KEY and minting failed. Mint a link by hand with agent-relay observer.

Consumer-Facing Apps + AI SDK Communicate Flows

A good production split is:

  1. AI SDK app handles the user conversation and streaming UI
  2. Communicate / onRelay() lets that point-person coordinate with specialists over Relay
  3. Workflows / runWorkflow() take over when a request needs multi-step execution, verification, or handoffs
import{streamText,wrapLanguageModel}from'ai';import{openai}from'@ai-sdk/openai';import{Relay}from'@agent-relay/sdk/communicate';import{onRelay}from'@agent-relay/sdk/communicate/adapters/ai-sdk';import{runWorkflow}from'@relayflows/core';exportasyncfunctionPOST(req: Request){const{ prompt, escalate, repo }=awaitreq.json();constrelay=newRelay('AppLead');constrelaySession=onRelay({name: 'AppLead',instructions: 'You are the customer-facing lead. Keep the user updated and delegate implementation via Relay when needed.',},relay);constmodel=wrapLanguageModel({model: openai('gpt-4o-mini'),middleware: relaySession.middleware,});if(escalate){constworkflow=awaitrunWorkflow('workflows/feature-dev.yaml',{vars: {task: prompt, repo },});returnResponse.json({status: workflow.status,runId: workflow.runId});}returnstreamText({
model,tools: relaySession.tools,system: 'Answer directly when possible; coordinate internally when the task needs specialists.',
prompt,}).toUIMessageStreamResponse({onFinish(){relaySession.cleanup();voidrelay.close();},});}

That pattern keeps the user experience snappy while still letting longer Relay workflows run with proper ownership, retries, and verification.

YAML Format

Workflows are defined as relay.yaml files:

version: "1.0"name: my-workflowdescription: "Optional description"swarm:
pattern: dag # Execution pattern (see Patterns below)maxConcurrency: 3# Max agents running in paralleltimeoutMs: 3600000# Global timeout (1 hour)channel: my-channel # Relay channel for agent communicationagents:
- name: backendcli: claude # claude | codex | gemini | aider | goose | opencode | droidrole: "Backend engineer"constraints:
model: opustimeoutMs: 600000retries: 2
- name: testercli: codexrole: "Test engineer"interactive: false # Non-interactive: runs as subprocess, no PTY/messaging# A persona replaces cli + role. Its harness, model, standing instructions,# installed skills, MCP servers, and harness settings come from the spec.# Persona agents are interactive-only: do not set cli, preset, or# constraints.model, and do not use interactive: false.
- name: integrationspersona: nango-integrationsworkflows:
- name: build-and-testonError: retry # fail | skip | retrysteps:
- name: build-apiagent: backendtask: "Build the REST API endpoints for user management"verification:
type: file_existsvalue: "src/api/users.ts"retries: 1
- name: write-testsagent: testertask: "Write integration tests for: {{steps.build-api.output}}"dependsOn: [build-api]
- name: run-testsagent: testertask: "Run the test suite and report results"dependsOn: [write-tests]verification:
type: exit_codevalue: "0"errorHandling:
strategy: retrymaxRetries: 2retryDelayMs: 5000repairAgent: testerrepairRetries: 2onExhaustion: needs-humannotifyChannel: my-channel

Template Variables

Use {{variable}} for user-provided values and {{steps.STEP_NAME.output}} for previous step outputs:

steps:
- name: planagent: plannertask: "Plan implementation for: {{task}}"# User variable
- name: implementagent: developerdependsOn: [plan]task: "Implement: {{steps.plan.output}}"# Previous step output

User variables are passed via the CLI or programmatically:

awaitrunWorkflow("workflow.yaml",{vars: {task: "Add OAuth2 support"},});

Blocking Slack Questions

Workflows can pause on a Slack question by adding a Slack integration step with action: askQuestion. The step posts a question, waits for the first human reply in the message thread, exposes that answer through {{steps.<name>.output}}, and can inject it into a running agent when injectToAgent is set.

workflows:
- name: defaultsteps:
- name: ask-humantype: integrationintegration: slackaction: askQuestionparams:
channel: "#engineering"text: "The implementer is blocked on migration strategy. Which path should it take?"waitTimeoutMs: "3600000"injectToAgent: "backend-runtime-name"injectTemplate: "HUMAN_ANSWER: {{answer.text}}"output: '{"format":"text","path":"answer.text"}'
- name: continue-with-answeragent: backenddependsOn: [ask-human]task: "Continue using this human guidance: {{steps.ask-human.output}}"

askQuestion can use the local Slack API runtime, or Relayfile-backed Slack writebacks when the workflow already has a Relayfile Slack integration.

Interactive agent steps can also opt into marker-driven assistance. With humanAssistance.slack enabled, an agent can print a line beginning with HUMAN_QUESTION:. The runner posts that question to Slack, blocks while waiting for a human reply, then injects HUMAN_ANSWER: ... back into that same agent session. If integrations.relayfile is present, Relayflows automatically uses the existing Relayfile/Pear Slack connection; no Slack bot token, Relayfile workspace id, or Relayfile token is required in the workflow.

Runnable TypeScript proof: examples/typescript/slack-human-assistance-e2e.ts.

swarm:
pattern: daghumanAssistance:
slack:
channel: proj-cloudtimeoutMs: 3600000integrations:
relayfile: {}workflows:
- name: defaultsteps:
- name: implementagent: backendtask: "Proceed, but ask for human guidance if the migration strategy is ambiguous."

Relayfile Event Subscriptions

Relayflows can subscribe to Relayfile integration events and inject matching events into active agents. Workflow-level subscriptions live under integrations.subscriptions; agent-level subscriptions use Workforce-style agents[].watch or Relayflows-style agents[].subscriptions.

integrations:
relayfile: {}subscriptions:
- name: pr-feedbackprovider: githubpath: /github/repos/acme/web/pulls/42/**events: [created, updated]agents: [pr-babysitter]agents:
- name: pr-babysittercli: codexwatch:
- paths: [/github/repos/acme/web/pulls/42/reviews/**]events: [created, updated]workflows:
- name: defaultsteps:
- name: babysit-pragent: pr-babysittertask: | Stay active and wait for INTEGRATION_EVENT messages about PR feedback. Read the Relayfile path from the event, address comments until no open feedback remains, then notify the user in Slack.

Verification Checks

Each step can include a verification check. Verification is one input to the runner's completion decision pipeline — when verification passes, the step completes even without a sentinel marker.

TypeDescription
exit_codeAgent must exit with the specified code (preferred for code-editing steps)
file_existsA file must exist at the specified path after the step
output_containsStep output must contain the specified string (optional accelerator)
customNo-op in the runner; handled by external callers
# Preferred — deterministic verificationverification:
type: exit_codevalue: "0"description: "Process exited successfully"# Also valid — output_contains as an optional acceleratorverification:
type: output_containsvalue: "IMPLEMENTATION_COMPLETE"description: "Agent confirms completion (optional fast-path)"

Completion Decision Pipeline

The runner uses a multi-signal pipeline to decide step completion:

  1. Deterministic verification — if a verification check passes, the step completes immediately (completed_verified)
  2. Owner decision — the step owner can issue OWNER_DECISION: COMPLETE|INCOMPLETE_RETRY|INCOMPLETE_FAIL (completed_by_owner_decision)
  3. Evidence-based completion — channel messages, file artifacts, and exit codes are collected as evidence (completed_by_evidence)
  4. Marker fast-pathSTEP_COMPLETE:<step-name> still works as an accelerator but is never required
Completion StateMeaning
completed_verifiedDeterministic verification passed
completed_by_owner_decisionOwner approved the step
completed_by_evidenceEvidence-based completion
retry_requested_by_ownerOwner requested retry
failed_verificationVerification explicitly failed
failed_owner_decisionOwner rejected the step
failed_no_evidenceNo verification, no owner decision, no evidence

Review parsing is tolerant: The runner accepts semantically equivalent outputs like "Approved", "Complete", "LGTM" — not just exact REVIEW_DECISION: APPROVE strings.

Swarm Patterns

The swarm.pattern field controls how agents are coordinated:

Core Patterns

PatternDescription
dagDirected acyclic graph — steps run based on dependency edges (default)
fan-outAll agents run in parallel
pipelineSequential chaining of steps
hub-spokeCentral hub coordinates spoke agents
consensusAgents vote on decisions
meshFull communication graph between agents
handoffSequential handoff between agents
cascadeWaterfall with phase gates
debateAgents propose and counter-argue
hierarchicalMulti-level reporting structure

Data Processing Patterns

PatternDescription
map-reduceSplit work into chunks (mappers), process in parallel, aggregate results (reducers)
scatter-gatherFan out requests to workers, collect and synthesize responses

Supervision & Quality Patterns

PatternDescription
supervisorMonitor agent monitors workers, restarts on failure, manages health
reflectionAgent produces output, critic reviews and provides feedback for iteration
verifierProducer agents submit work to verifier agents for validation

Adversarial & Validation Patterns

PatternDescription
red-teamAttacker agents probe for weaknesses, defender agents respond
auctionAuctioneer broadcasts tasks, agents bid based on capability/cost

Resilience Patterns

PatternDescription
escalationStart with fast/cheap agents, escalate to more capable on failure
sagaDistributed transactions with compensating actions on failure
circuit-breakerPrimary agent with fallback chain, fail fast and recover

Collaborative Patterns

PatternDescription
blackboardShared workspace where agents contribute incrementally to a solution
swarmEmergent behavior from simple agent rules (neighbor communication)

Auto-Selection by Role

When swarm.pattern is omitted, the coordinator auto-selects based on agent roles. Patterns are checked in priority order below (first match wins):

PriorityPatternRequired Roles/Config
1dagSteps with dependsOn
2consensusUses coordination.consensusStrategy config
3map-reducemapper + reducer
4red-team(attacker OR red-team) + (defender OR blue-team)
5reflectioncritic
6escalationtier-1, tier-2, etc.
7auctionauctioneer
8sagasaga-orchestrator OR compensate-handler
9circuit-breakerfallback, backup, OR primary
10blackboardblackboard OR shared-workspace
11swarmhive-mind OR swarm-agent
12verifierverifier
13supervisorsupervisor
14hierarchicallead (with 4+ agents)
15hub-spokehub OR coordinator
16pipelineUnique agents per step, 3+ steps
17fan-outDefault fallback

Error Handling

Step-Level

steps:
- name: risky-stepagent: workertask: "Do something that might fail"retries: 3# Retry up to 3 times on failuretimeoutMs: 300000# 5 minute timeout

Successful early termination

A deterministic gate can explicitly declare exit codes that mean “there is no work to do.” A matching code ends the run with the distinct completed_early status and skips every step that has not started:

steps:
- name: claim-worktype: deterministiccommand: node bin/claim-work.mjsterminalSuccessExitCodes: [78]
- name: process-claimagent: workertask: Process the claimed workdependsOn: [claim-work]

Terminal-capable gates are scheduling barriers, so other ready work does not race the gate. The triggering step is completed with completion reason completed_early_exit; remaining steps are skipped, and the CLI exits 0 while clearly reporting COMPLETED EARLY. Verification still applies, so a verification failure remains a real failure.

This behavior is opt-in. Without terminalSuccessExitCodes, exit 78 and every other non-zero exit retain their existing failure behavior. The new completed_early run status is an additive public API value: consumers with exhaustive status switches, strict validators, database constraints, or terminal-status polling must handle it separately from completed.

Workflow-Level

The onError field on a workflow controls what happens when a step fails:

ValueBehavior
fail / fail-fastStop immediately, skip downstream steps
skip / continueSkip downstream dependents, continue independent steps
retryRetry the step; deterministic gates ask a workflow agent to repair before each retry when an agent is available

Global

errorHandling:
strategy: retrymaxRetries: 2retryDelayMs: 5000repairAgent: testerrepairRetries: 2notifyChannel: alerts

Retry-mode workflows are repair-aware by default. Deterministic step failures, verification gate failures, and malformed agent artifacts are treated as repairable work before terminal failure. The runner chooses errorHandling.repairAgent when set, otherwise it uses the step's owning/upstream agent when possible, then falls back to the best available workflow agent. The selected agent gets the failed command or agent output, working directory, exit information, and captured evidence, then the failed gate or step is retried. Use repairRetries: 0, strategy: fail-fast, or strategy: continue when a workflow intentionally should not invoke repair agents. Set onExhaustion: needs-human to end an exhausted repairable run as needs_human instead of failed.

Built-in Templates

Six pre-built workflow templates are included:

TemplatePatternDescription
feature-devhub-spokePlan, implement, review, and finalize a feature
bug-fixhub-spokeInvestigate, patch, validate, and document a bug fix
code-reviewfan-outParallel multi-reviewer assessment with consolidated findings
security-auditpipelineScan, triage, remediate, and verify security issues
refactorhierarchicalAnalyze, plan, execute, and validate a refactor
documentationhandoffResearch, draft, review, and publish documentation

Using Templates

import{TemplateRegistry,WorkflowRunner}from"@relayflows/core";constregistry=newTemplateRegistry();// List available templatesconsttemplates=awaitregistry.listTemplates();// Load and run a templateconstconfig=awaitregistry.loadTemplate("feature-dev");construnner=newWorkflowRunner();constresult=awaitrunner.execute(config,undefined,{task: "Add WebSocket support to the API",});// Install a custom template from a URLawaitregistry.installExternalTemplate("https://example.com/my-template.yaml","my-template");

TypeScript Builder API

The builder constructs a RelayYamlConfig object and can run it, export it as YAML, or return the raw config.

import{workflow}from"@relayflows/core";// Build and runconstresult=awaitworkflow("my-workflow").pattern("dag").maxConcurrency(3).timeout(60*60*1000).channel("my-channel").agent("backend",{cli: "claude",role: "Backend engineer",model: "opus",retries: 2,}).agent("frontend",{cli: "codex",role: "Frontend engineer",interactive: false,// Non-interactive subprocess mode}).step("api",{agent: "backend",task: "Build REST API",verification: {type: "output_contains",value: "API_READY"},}).step("ui",{agent: "frontend",task: "Build the UI",dependsOn: ["api"],}).onError("retry",{maxRetries: 2,retryDelayMs: 5000}).run();// Or export to YAMLconstyaml=workflow("my-workflow").pattern("dag").agent("worker",{cli: "claude"}).step("task1",{agent: "worker",task: "Do something"}).toYaml();// Or get the raw config objectconstconfig=workflow("my-workflow").pattern("dag").agent("worker",{cli: "claude"}).step("task1",{agent: "worker",task: "Do something"}).toConfig();

Python Builder API

The Python builder ships with @agent-relay/sdk-py:

pip install agent-relay
fromagent_relayimportworkflow, run_yaml# Build and runresult= (
workflow("my-workflow")
.pattern("dag")
.max_concurrency(3)
.timeout(3600000)
.agent("backend", cli="claude", role="Backend engineer")
.agent("frontend", cli="codex", role="Frontend engineer")
.step("api", agent="backend", task="Build REST API")
.step("ui", agent="frontend", task="Build the UI", depends_on=["api"])
.on_error("retry", max_retries=2, retry_delay_ms=5000)
.run()
)
# Run an existing YAML fileresult=run_yaml("workflows/my-workflow.yaml")
# Export to YAML stringyaml_str= (
workflow("my-workflow")
.pattern("dag")
.agent("worker", cli="claude")
.step("task1", agent="worker", task="Do something")
.to_yaml()
)
# Get the raw config dictconfig= (
workflow("my-workflow")
.pattern("dag")
.agent("worker", cli="claude")
.step("task1", agent="worker", task="Do something")
.to_config()
)

Programmatic API

For full control, use the WorkflowRunner directly:

import{WorkflowRunner}from"@relayflows/core";construnner=newWorkflowRunner({cwd: "/path/to/project",// Working directory (default: process.cwd())relay: {port: 3000},// AgentRelay options (optional)});// Listen to events (broker:event fires frequently — filter it out for cleaner output)runner.on((event)=>{if(event.type==='broker:event')return;console.log(event.type,event);});// Parse and executeconstconfig=awaitrunner.parseYamlFile("workflow.yaml");construn=awaitrunner.execute(config,"workflow-name",{task: "Build the feature",});// Pause / resume / abortrunner.pause();runner.unpause();runner.abort();// Resume a failed runconstresumed=awaitrunner.resume(run.id);

Zero-Config Convenience Function

import{runWorkflow}from"@relayflows/core";constresult=awaitrunWorkflow("workflow.yaml",{workflow: "deploy",vars: {environment: "staging"},onEvent: (event)=>{if(event.type!=='broker:event')console.log(event.type);},});

Coordination

Barriers

Synchronization points that wait for specific steps to complete:

coordination:
barriers:
- name: all-reviews-donewaitFor: [review-arch, review-security, review-correctness]timeoutMs: 900000consensusStrategy: majority # majority | unanimous | quorum

Shared State

Agents can share state during execution:

state:
backend: memory # memory | redis | databasettlMs: 86400000namespace: my-workflow

Supported Agent CLIs

CLIDescription
claudeClaude Code (Anthropic)
codexCodex CLI (OpenAI)
geminiGemini CLI (Google)
aiderAider coding assistant
gooseGoose AI assistant
opencodeOpenCode CLI
droidDroid CLI

Non-Interactive Agents

By default, agents run in interactive PTY mode with full relay messaging. For workers that just need to execute a task and return output — common in fan-out, map-reduce, and pipeline patterns — set interactive: false to run them as lightweight subprocesses.

YAML

agents:
- name: leadcli: clauderole: "Coordinates work"# interactive: true (default) — full PTY, relay messaging, /exit detection
- name: workercli: codexrole: "Executes tasks"interactive: false # Runs "codex exec <task>", captures stdout

TypeScript

workflow("fan-out-analysis").pattern("fan-out").agent("lead",{cli: "claude",role: "Coordinator"}).agent("worker-1",{cli: "codex",interactive: false,role: "Analyst"}).agent("worker-2",{cli: "codex",interactive: false,role: "Analyst"}).step("analyze-1",{agent: "worker-1",task: "Analyze module A"}).step("analyze-2",{agent: "worker-2",task: "Analyze module B"}).step("synthesize",{agent: "lead",task: "Combine: {{steps.analyze-1.output}} + {{steps.analyze-2.output}}",dependsOn: ["analyze-1","analyze-2"],}).run();

How It Works

AspectInteractive (default)Non-Interactive
ExecutionFull PTY with stdin/stdoutchild_process.spawn() with piped stdio
CLI invocationStandard interactive sessionOne-shot mode (claude -p, codex exec, etc.)
Relay messagingCan send/receive messagesNo messaging — excluded from topology edges
Self-terminationMust output /exitProcess exits naturally when done
Output capturePTY output bufferstdout capture
OverheadHigher (PTY, echo verification, SIGWINCH)Lower (simple subprocess)

Non-Interactive CLI Commands

CLICommandNotes
claudeclaude -p "<task>"Print mode, exits after response
codexcodex exec "<task>"One-shot execution
geminigemini -p "<task>"Prompt mode
opencodeopencode --prompt "<task>"One-shot prompt
droiddroid exec "<task>"One-shot execution
aideraider --message "<task>" --yes-always --no-gitAuto-approve, skip git
goosegoose run --text "<task>" --no-sessionText mode, no session file

When to Use

  • Fan-out workers that process a task and return results
  • Map-reduce mappers that don't need mid-task communication
  • Pipeline stages that transform input to output
  • Any agent that doesn't need turn-by-turn relay messaging

When NOT to Use

  • Lead/coordinator agents that communicate with others
  • Agents in debate, consensus, or reflection patterns
  • Agents that need to receive messages during execution

Agent Slash Commands

Agents running inside a workflow can output slash commands to signal the broker. These are detected in the agent's PTY output at the broker level — the agent simply prints the command on its own line.

/exit

Signals that the agent has completed its current step and is ready to be released.

/exit

The workflow runner waits for each agent to /exit after delivering a step task. When the broker detects /exit in the agent's output (exact line match after ANSI stripping), it:

  1. Emits an agent_exit frame with reason: "agent_requested"
  2. Triggers graceful PTY shutdown

If an agent does not /exit within the step's timeoutMs, the runner treats the step as timed out. As a safety net, steps with file_exists verification will still pass if the expected file is present despite the timeout.

Best practice: Instruct agents to output /exit when done in your step task descriptions:

steps:
- name: build-apiagent: backendtask: | Build the REST API endpoints for user management. When finished, output /exit.

Idle Agent Detection and Nudging

Interactive agents sometimes finish their task but forget to /exit, sitting idle and blocking downstream steps. The runner can detect idle agents and take action automatically.

Configuration

Add idleNudge to your swarm config:

swarm:
pattern: hub-spokeidleNudge:
nudgeAfterMs: 120000# 2 min before first nudge (default)escalateAfterMs: 120000# 2 min after nudge before force-release (default)maxNudges: 1# Nudges before escalation (default)

All built-in templates include idle nudging with these defaults.

How It Works

  1. Detection: The broker tracks agent output timestamps and emits agent_idle events when an agent goes silent for the configured threshold
  2. Nudge: For hub patterns (hub-spoke, fan-out, hierarchical, etc.), the runner tells the hub agent to check on the idle agent. For non-hub patterns, a system message is injected directly into the agent's PTY
  3. Escalation: If the agent remains idle after maxNudges attempts, the runner force-releases it and captures whatever output was produced
  4. No config: When idleNudge is omitted, the runner uses simple waitForExit (backward compatible)

Events

The runner emits two new events for idle nudging:

EventDescription
step:nudgedFired when a nudge message is sent to an idle agent
step:force-releasedFired when an agent is force-released after exhausting nudges

Automatic Step Owner and Review

For interactive agent steps, the runner uses a point-person-led completion model:

  1. Elects a step owner (prefers lead/coordinator-style agents, falls back to the step agent)
  2. Runs a completion decision pipeline — checks deterministic verification first, then owner judgment, then evidence
  3. Owner can issue structured decisions via OWNER_DECISION: COMPLETE|INCOMPLETE_RETRY|INCOMPLETE_FAIL|NEEDS_CLARIFICATION with optional REASON: <text>
  4. Review parsing is tolerant — accepts "Approved", "Complete", "LGTM", not just exact REVIEW_DECISION: APPROVE
  5. Markers are optional acceleratorsSTEP_COMPLETE:<step-name> still works as a fast-path but is never required
  6. Stores primary output plus review output in the step artifact

Evidence-based completion: The runner collects channel messages, file artifacts, process exit codes, and coordination signals (e.g., WORKER_DONE posted in channel) as completion evidence. When sufficient evidence exists, the step completes without requiring any sentinel marker.

Deterministic and worktree steps are unchanged and do not require owner/review delegation.

Sandbox Execution

By default the runner spawns steps as local child processes. To run them in isolated sandboxes instead, select a provider — the runner still owns command construction, env, cwd, timeout, and the whole DAG/retry/verification pipeline; the provider only supplies "where the command runs".

# Off by default. Unset the flag to get local child processes back.export RELAYFLOWS_SANDBOX_PROVIDER=daytona
export DAYTONA_API_KEY=...
export RELAYFLOWS_SANDBOX_HOME_DIR=/home/daytona # image-specific, requiredexport RELAYFLOWS_SANDBOX_SNAPSHOT=my-snapshot # optional

Or in code:

import{WorkflowRunner}from"@relayflows/core";construnner=newWorkflowRunner({sandbox: {provider: "daytona",homeDir: "/home/daytona"},});
ProviderWhat it gives you
none (default)No sandbox. Local child processes, exactly as before.
daytonaReal remote sandboxes via @agent-relay/sandbox. Needs the optional peer @daytonaio/sdk.
local-processReal local processes in a private per-step directory with its own HOME. Isolates the filesystem root, not the machine — good for development and CI, not a security boundary.

Reversibility.provider: "none" (or an unset RELAYFLOWS_SANDBOX_PROVIDER) produces no backend at all, so nothing about the default path changes. An explicit executor or processBackend still wins over sandbox config, so a host that injects its own backend today keeps it.

Custom providers. Register a runtime under any name, or hand one in directly. This is the seam a host uses to plug in a runtime that does not live in this repo:

import{registerSandboxProvider,WorkflowRunner}from"@relayflows/core";registerSandboxProvider("my-runtime",(config)=>newMyRuntime(config));// ...or skip the registry entirely:newWorkflowRunner({sandbox: {runtime: myRuntime}});

A runtime needs five methods — launch, exec, uploadFile, getHomeDir, destroy — matching @agent-relay/sandbox's WorkflowRuntime.

Schema Validation

A JSON Schema is available at packages/core/src/schema.json for editor autocompletion and validation of relay.yaml files.

Development

npm install
npm run typecheck
npm run test

Requirements

  • Node.js 22+
  • @relayflows/cli installed (npm install -g @relayflows/cli)
  • For Python: Python 3.10+ with pip install agent-relay
  • For TypeScript workflow files: tsx or ts-node installed

License

Apache-2.0 — Copyright 2025 Agent Workforce Incorporated

About

Orchestrate multi-step, multi-agent execution across Agent Relay workers

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

relayflows

npmLicense

Workflow engine and CLI for Agent Relay. Orchestrate multi-agent workflows using YAML, TypeScript, or Python. Define agents, wire up dependencies, and let the runner handle execution, retries, and verification.

Quick Start

CLI

# Run a YAML workflow
relayflows run workflow.yaml
# Run a TypeScript workflow
relayflows run workflow.ts
# Run a Python workflow
relayflows run workflow.py
# Run a specific named workflow from a file
relayflows run workflow.yaml --workflow deploy

TypeScript

import{workflow}from"@relayflows/core";constresult=awaitworkflow("ship-feature").pattern("dag").agent("planner",{cli: "claude",role: "Plans implementation"}).agent("developer",{cli: "codex",role: "Writes code"}).agent("reviewer",{cli: "claude",role: "Reviews code"}).step("plan",{agent: "planner",task: "Create implementation plan for user authentication",}).step("implement",{agent: "developer",task: "Implement the plan",dependsOn: ["plan"],}).step("review",{agent: "reviewer",task: "Review the implementation",dependsOn: ["implement"],}).run();console.log(result.status);// "completed" | "completed_early" | "failed" | "cancelled" | "needs_human"

Python

fromagent_relayimportworkflowresult= (
workflow("ship-feature")
.pattern("dag")
.agent("planner", cli="claude", role="Plans implementation")
.agent("developer", cli="codex", role="Writes code")
.agent("reviewer", cli="claude", role="Reviews code")
.step("plan", agent="planner", task="Create implementation plan for user auth")
.step("implement", agent="developer", task="Implement the plan", depends_on=["plan"])
.step("review", agent="reviewer", task="Review the implementation", depends_on=["implement"])
.run()
)

Watching a Run

When a run starts, the runner mints and prints a link you can open to follow it live:

[workflow 00:02] Workspace created for this workflow.
[workflow 00:02] Observer: https://agentrelay.com/observer?key=ot_live_...
[workflow 00:02] Channel: wf-ship-feature-a1b2c3

Open the Observer: URL and you see messages, agent activity, handoffs, and deliveries in real time. The link carries a scoped observer token (ot_live_): read-only, expiring in 24 hours, and individually revocable.

Minting is best-effort, so a link is not guaranteed — see If no link appears. A failed mint never fails the run.

Treat the link itself as a shared secret. The token is a bearer credential in a query parameter, so anyone who gets the URL can read the stream it covers until it expires or you revoke it. It is far safer than a workspace key — it cannot send, spawn, or administer — but it is not public.

The two ways a run gets a workspace

Which link you get depends on where the run's Relaycast workspace comes from.

No RELAY_API_KEY set — the runner creates a throwaway workspace for this run alone and mints an observer link covering all of it, DMs included. The workspace is anonymous and disappears from your reach when the run ends, so copy the link while the run is going. Nothing persists the underlying key, and there is no way to recover it afterward.

RELAY_API_KEY set — the runner uses your workspace and mints a link scoped to just this run's channel, with agent DMs excluded, so the link does not expose unrelated traffic in a shared workspace. This is the better setup for anything you may want to revisit: the run is in a workspace you own, so you can mint fresh links whenever you like.

# One-time: create a workspace you own and keep the key
curl -sX POST https://api.relaycast.dev/v1/workspaces \
-H 'content-type: application/json' -d '{"name":"my-workflows"}' \
| jq -r '.data.api_key // .api_key'export RELAY_API_KEY=rk_live_... # put this in your shell profile
relayflows run workflow.yaml

If you already use the Agent Relay CLI, agent-relay workspace key --reveal-secrets prints the key of your active workspace. Note it is masked without --reveal-secrets.

Minting more links yourself

With a workspace you own, agent-relay observer mints links on demand:

agent-relay observer # read-only link, 24h, DMs excluded
agent-relay observer --channels wf-ship-a1b2c3 # scope to one run
agent-relay observer --include-dms # include agent DMs
agent-relay observer --expires 7d # longer-lived link
agent-relay observer list # what is outstanding
agent-relay observer revoke <id># cut one off immediately

Never share the workspace key

A workspace key (rk_live_) is an administrative credential — it can send messages, spawn and remove agents, and change workspace settings. Do not put one in an observer URL, a chat message, or a terminal transcript; query strings end up in browser history, referrer headers, and proxy logs. The realtime endpoint rejects it outright, so a link built from one cannot stream anyway.

Workspace key (rk_live_)Observer token (ot_live_)
Read messages and activityyesyes
Send, spawn agents, administeryesno
Expiresnoyes
Revocable individuallynoyes
Scopable to channelsnoyes

The runner only ever prints ot_live_ links, and scrubs rk_live_ values out of channel output.

Configuration

VariablePurpose
RELAY_API_KEYWorkspace key to run against. Unset means a throwaway workspace per run.
RELAY_OBSERVER_URLObserver dashboard base. Defaults to https://agentrelay.com/observer.
RELAY_OBSERVER_EXPIRESLink lifetime as 30m / 24h / 7d. Defaults to 24h; unparseable or over 90d falls back to 24h.
RELAYCAST_BASE_URLRelaycast engine base. Defaults to https://api.relaycast.dev.

If no link appears

  • Observation: unavailable — the token could not be minted (engine unreachable, or it rejected the request). The run is unaffected; minting is best-effort by design and never fails a run.
  • No observer lines at all — the run needed no broker: every step was deterministic, worktree, integration, or waitFor, or an external executor handled agent spawning, or Relaycast was disabled with AGENT_RELAY_WORKFLOW_DISABLE_RELAYCAST=1.
  • Observation: run agent-relay observer — you set RELAY_API_KEY and minting failed. Mint a link by hand with agent-relay observer.

Consumer-Facing Apps + AI SDK Communicate Flows

A good production split is:

  1. AI SDK app handles the user conversation and streaming UI
  2. Communicate / onRelay() lets that point-person coordinate with specialists over Relay
  3. Workflows / runWorkflow() take over when a request needs multi-step execution, verification, or handoffs
import{streamText,wrapLanguageModel}from'ai';import{openai}from'@ai-sdk/openai';import{Relay}from'@agent-relay/sdk/communicate';import{onRelay}from'@agent-relay/sdk/communicate/adapters/ai-sdk';import{runWorkflow}from'@relayflows/core';exportasyncfunctionPOST(req: Request){const{ prompt, escalate, repo }=awaitreq.json();constrelay=newRelay('AppLead');constrelaySession=onRelay({name: 'AppLead',instructions: 'You are the customer-facing lead. Keep the user updated and delegate implementation via Relay when needed.',},relay);constmodel=wrapLanguageModel({model: openai('gpt-4o-mini'),middleware: relaySession.middleware,});if(escalate){constworkflow=awaitrunWorkflow('workflows/feature-dev.yaml',{vars: {task: prompt, repo },});returnResponse.json({status: workflow.status,runId: workflow.runId});}returnstreamText({
model,tools: relaySession.tools,system: 'Answer directly when possible; coordinate internally when the task needs specialists.',
prompt,}).toUIMessageStreamResponse({onFinish(){relaySession.cleanup();voidrelay.close();},});}

That pattern keeps the user experience snappy while still letting longer Relay workflows run with proper ownership, retries, and verification.

YAML Format

Workflows are defined as relay.yaml files:

version: "1.0"name: my-workflowdescription: "Optional description"swarm:
pattern: dag # Execution pattern (see Patterns below)maxConcurrency: 3# Max agents running in paralleltimeoutMs: 3600000# Global timeout (1 hour)channel: my-channel # Relay channel for agent communicationagents:
- name: backendcli: claude # claude | codex | gemini | aider | goose | opencode | droidrole: "Backend engineer"constraints:
model: opustimeoutMs: 600000retries: 2
- name: testercli: codexrole: "Test engineer"interactive: false # Non-interactive: runs as subprocess, no PTY/messaging# A persona replaces cli + role. Its harness, model, standing instructions,# installed skills, MCP servers, and harness settings come from the spec.# Persona agents are interactive-only: do not set cli, preset, or# constraints.model, and do not use interactive: false.
- name: integrationspersona: nango-integrationsworkflows:
- name: build-and-testonError: retry # fail | skip | retrysteps:
- name: build-apiagent: backendtask: "Build the REST API endpoints for user management"verification:
type: file_existsvalue: "src/api/users.ts"retries: 1
- name: write-testsagent: testertask: "Write integration tests for: {{steps.build-api.output}}"dependsOn: [build-api]
- name: run-testsagent: testertask: "Run the test suite and report results"dependsOn: [write-tests]verification:
type: exit_codevalue: "0"errorHandling:
strategy: retrymaxRetries: 2retryDelayMs: 5000repairAgent: testerrepairRetries: 2onExhaustion: needs-humannotifyChannel: my-channel

Template Variables

Use {{variable}} for user-provided values and {{steps.STEP_NAME.output}} for previous step outputs:

steps:
- name: planagent: plannertask: "Plan implementation for: {{task}}"# User variable
- name: implementagent: developerdependsOn: [plan]task: "Implement: {{steps.plan.output}}"# Previous step output

User variables are passed via the CLI or programmatically:

awaitrunWorkflow("workflow.yaml",{vars: {task: "Add OAuth2 support"},});

Blocking Slack Questions

Workflows can pause on a Slack question by adding a Slack integration step with action: askQuestion. The step posts a question, waits for the first human reply in the message thread, exposes that answer through {{steps.<name>.output}}, and can inject it into a running agent when injectToAgent is set.

workflows:
- name: defaultsteps:
- name: ask-humantype: integrationintegration: slackaction: askQuestionparams:
channel: "#engineering"text: "The implementer is blocked on migration strategy. Which path should it take?"waitTimeoutMs: "3600000"injectToAgent: "backend-runtime-name"injectTemplate: "HUMAN_ANSWER: {{answer.text}}"output: '{"format":"text","path":"answer.text"}'
- name: continue-with-answeragent: backenddependsOn: [ask-human]task: "Continue using this human guidance: {{steps.ask-human.output}}"

askQuestion can use the local Slack API runtime, or Relayfile-backed Slack writebacks when the workflow already has a Relayfile Slack integration.

Interactive agent steps can also opt into marker-driven assistance. With humanAssistance.slack enabled, an agent can print a line beginning with HUMAN_QUESTION:. The runner posts that question to Slack, blocks while waiting for a human reply, then injects HUMAN_ANSWER: ... back into that same agent session. If integrations.relayfile is present, Relayflows automatically uses the existing Relayfile/Pear Slack connection; no Slack bot token, Relayfile workspace id, or Relayfile token is required in the workflow.

Runnable TypeScript proof: examples/typescript/slack-human-assistance-e2e.ts.

swarm:
pattern: daghumanAssistance:
slack:
channel: proj-cloudtimeoutMs: 3600000integrations:
relayfile: {}workflows:
- name: defaultsteps:
- name: implementagent: backendtask: "Proceed, but ask for human guidance if the migration strategy is ambiguous."

Relayfile Event Subscriptions

Relayflows can subscribe to Relayfile integration events and inject matching events into active agents. Workflow-level subscriptions live under integrations.subscriptions; agent-level subscriptions use Workforce-style agents[].watch or Relayflows-style agents[].subscriptions.

integrations:
relayfile: {}subscriptions:
- name: pr-feedbackprovider: githubpath: /github/repos/acme/web/pulls/42/**events: [created, updated]agents: [pr-babysitter]agents:
- name: pr-babysittercli: codexwatch:
- paths: [/github/repos/acme/web/pulls/42/reviews/**]events: [created, updated]workflows:
- name: defaultsteps:
- name: babysit-pragent: pr-babysittertask: | Stay active and wait for INTEGRATION_EVENT messages about PR feedback. Read the Relayfile path from the event, address comments until no open feedback remains, then notify the user in Slack.

Verification Checks

Each step can include a verification check. Verification is one input to the runner's completion decision pipeline — when verification passes, the step completes even without a sentinel marker.

TypeDescription
exit_codeAgent must exit with the specified code (preferred for code-editing steps)
file_existsA file must exist at the specified path after the step
output_containsStep output must contain the specified string (optional accelerator)
customNo-op in the runner; handled by external callers
# Preferred — deterministic verificationverification:
type: exit_codevalue: "0"description: "Process exited successfully"# Also valid — output_contains as an optional acceleratorverification:
type: output_containsvalue: "IMPLEMENTATION_COMPLETE"description: "Agent confirms completion (optional fast-path)"

Completion Decision Pipeline

The runner uses a multi-signal pipeline to decide step completion:

  1. Deterministic verification — if a verification check passes, the step completes immediately (completed_verified)
  2. Owner decision — the step owner can issue OWNER_DECISION: COMPLETE|INCOMPLETE_RETRY|INCOMPLETE_FAIL (completed_by_owner_decision)
  3. Evidence-based completion — channel messages, file artifacts, and exit codes are collected as evidence (completed_by_evidence)
  4. Marker fast-pathSTEP_COMPLETE:<step-name> still works as an accelerator but is never required
Completion StateMeaning
completed_verifiedDeterministic verification passed
completed_by_owner_decisionOwner approved the step
completed_by_evidenceEvidence-based completion
retry_requested_by_ownerOwner requested retry
failed_verificationVerification explicitly failed
failed_owner_decisionOwner rejected the step
failed_no_evidenceNo verification, no owner decision, no evidence

Review parsing is tolerant: The runner accepts semantically equivalent outputs like "Approved", "Complete", "LGTM" — not just exact REVIEW_DECISION: APPROVE strings.

Swarm Patterns

The swarm.pattern field controls how agents are coordinated:

Core Patterns

PatternDescription
dagDirected acyclic graph — steps run based on dependency edges (default)
fan-outAll agents run in parallel
pipelineSequential chaining of steps
hub-spokeCentral hub coordinates spoke agents
consensusAgents vote on decisions
meshFull communication graph between agents
handoffSequential handoff between agents
cascadeWaterfall with phase gates
debateAgents propose and counter-argue
hierarchicalMulti-level reporting structure

Data Processing Patterns

PatternDescription
map-reduceSplit work into chunks (mappers), process in parallel, aggregate results (reducers)
scatter-gatherFan out requests to workers, collect and synthesize responses

Supervision & Quality Patterns

PatternDescription
supervisorMonitor agent monitors workers, restarts on failure, manages health
reflectionAgent produces output, critic reviews and provides feedback for iteration
verifierProducer agents submit work to verifier agents for validation

Adversarial & Validation Patterns

PatternDescription
red-teamAttacker agents probe for weaknesses, defender agents respond
auctionAuctioneer broadcasts tasks, agents bid based on capability/cost

Resilience Patterns

PatternDescription
escalationStart with fast/cheap agents, escalate to more capable on failure
sagaDistributed transactions with compensating actions on failure
circuit-breakerPrimary agent with fallback chain, fail fast and recover

Collaborative Patterns

PatternDescription
blackboardShared workspace where agents contribute incrementally to a solution
swarmEmergent behavior from simple agent rules (neighbor communication)

Auto-Selection by Role

When swarm.pattern is omitted, the coordinator auto-selects based on agent roles. Patterns are checked in priority order below (first match wins):

PriorityPatternRequired Roles/Config
1dagSteps with dependsOn
2consensusUses coordination.consensusStrategy config
3map-reducemapper + reducer
4red-team(attacker OR red-team) + (defender OR blue-team)
5reflectioncritic
6escalationtier-1, tier-2, etc.
7auctionauctioneer
8sagasaga-orchestrator OR compensate-handler
9circuit-breakerfallback, backup, OR primary
10blackboardblackboard OR shared-workspace
11swarmhive-mind OR swarm-agent
12verifierverifier
13supervisorsupervisor
14hierarchicallead (with 4+ agents)
15hub-spokehub OR coordinator
16pipelineUnique agents per step, 3+ steps
17fan-outDefault fallback

Error Handling

Step-Level

steps:
- name: risky-stepagent: workertask: "Do something that might fail"retries: 3# Retry up to 3 times on failuretimeoutMs: 300000# 5 minute timeout

Successful early termination

A deterministic gate can explicitly declare exit codes that mean “there is no work to do.” A matching code ends the run with the distinct completed_early status and skips every step that has not started:

steps:
- name: claim-worktype: deterministiccommand: node bin/claim-work.mjsterminalSuccessExitCodes: [78]
- name: process-claimagent: workertask: Process the claimed workdependsOn: [claim-work]

Terminal-capable gates are scheduling barriers, so other ready work does not race the gate. The triggering step is completed with completion reason completed_early_exit; remaining steps are skipped, and the CLI exits 0 while clearly reporting COMPLETED EARLY. Verification still applies, so a verification failure remains a real failure.

This behavior is opt-in. Without terminalSuccessExitCodes, exit 78 and every other non-zero exit retain their existing failure behavior. The new completed_early run status is an additive public API value: consumers with exhaustive status switches, strict validators, database constraints, or terminal-status polling must handle it separately from completed.

Workflow-Level

The onError field on a workflow controls what happens when a step fails:

ValueBehavior
fail / fail-fastStop immediately, skip downstream steps
skip / continueSkip downstream dependents, continue independent steps
retryRetry the step; deterministic gates ask a workflow agent to repair before each retry when an agent is available

Global

errorHandling:
strategy: retrymaxRetries: 2retryDelayMs: 5000repairAgent: testerrepairRetries: 2notifyChannel: alerts

Retry-mode workflows are repair-aware by default. Deterministic step failures, verification gate failures, and malformed agent artifacts are treated as repairable work before terminal failure. The runner chooses errorHandling.repairAgent when set, otherwise it uses the step's owning/upstream agent when possible, then falls back to the best available workflow agent. The selected agent gets the failed command or agent output, working directory, exit information, and captured evidence, then the failed gate or step is retried. Use repairRetries: 0, strategy: fail-fast, or strategy: continue when a workflow intentionally should not invoke repair agents. Set onExhaustion: needs-human to end an exhausted repairable run as needs_human instead of failed.

Built-in Templates

Six pre-built workflow templates are included:

TemplatePatternDescription
feature-devhub-spokePlan, implement, review, and finalize a feature
bug-fixhub-spokeInvestigate, patch, validate, and document a bug fix
code-reviewfan-outParallel multi-reviewer assessment with consolidated findings
security-auditpipelineScan, triage, remediate, and verify security issues
refactorhierarchicalAnalyze, plan, execute, and validate a refactor
documentationhandoffResearch, draft, review, and publish documentation

Using Templates

import{TemplateRegistry,WorkflowRunner}from"@relayflows/core";constregistry=newTemplateRegistry();// List available templatesconsttemplates=awaitregistry.listTemplates();// Load and run a templateconstconfig=awaitregistry.loadTemplate("feature-dev");construnner=newWorkflowRunner();constresult=awaitrunner.execute(config,undefined,{task: "Add WebSocket support to the API",});// Install a custom template from a URLawaitregistry.installExternalTemplate("https://example.com/my-template.yaml","my-template");

TypeScript Builder API

The builder constructs a RelayYamlConfig object and can run it, export it as YAML, or return the raw config.

import{workflow}from"@relayflows/core";// Build and runconstresult=awaitworkflow("my-workflow").pattern("dag").maxConcurrency(3).timeout(60*60*1000).channel("my-channel").agent("backend",{cli: "claude",role: "Backend engineer",model: "opus",retries: 2,}).agent("frontend",{cli: "codex",role: "Frontend engineer",interactive: false,// Non-interactive subprocess mode}).step("api",{agent: "backend",task: "Build REST API",verification: {type: "output_contains",value: "API_READY"},}).step("ui",{agent: "frontend",task: "Build the UI",dependsOn: ["api"],}).onError("retry",{maxRetries: 2,retryDelayMs: 5000}).run();// Or export to YAMLconstyaml=workflow("my-workflow").pattern("dag").agent("worker",{cli: "claude"}).step("task1",{agent: "worker",task: "Do something"}).toYaml();// Or get the raw config objectconstconfig=workflow("my-workflow").pattern("dag").agent("worker",{cli: "claude"}).step("task1",{agent: "worker",task: "Do something"}).toConfig();

Python Builder API

The Python builder ships with @agent-relay/sdk-py:

pip install agent-relay
fromagent_relayimportworkflow, run_yaml# Build and runresult= (
workflow("my-workflow")
.pattern("dag")
.max_concurrency(3)
.timeout(3600000)
.agent("backend", cli="claude", role="Backend engineer")
.agent("frontend", cli="codex", role="Frontend engineer")
.step("api", agent="backend", task="Build REST API")
.step("ui", agent="frontend", task="Build the UI", depends_on=["api"])
.on_error("retry", max_retries=2, retry_delay_ms=5000)
.run()
)
# Run an existing YAML fileresult=run_yaml("workflows/my-workflow.yaml")
# Export to YAML stringyaml_str= (
workflow("my-workflow")
.pattern("dag")
.agent("worker", cli="claude")
.step("task1", agent="worker", task="Do something")
.to_yaml()
)
# Get the raw config dictconfig= (
workflow("my-workflow")
.pattern("dag")
.agent("worker", cli="claude")
.step("task1", agent="worker", task="Do something")
.to_config()
)

Programmatic API

For full control, use the WorkflowRunner directly:

import{WorkflowRunner}from"@relayflows/core";construnner=newWorkflowRunner({cwd: "/path/to/project",// Working directory (default: process.cwd())relay: {port: 3000},// AgentRelay options (optional)});// Listen to events (broker:event fires frequently — filter it out for cleaner output)runner.on((event)=>{if(event.type==='broker:event')return;console.log(event.type,event);});// Parse and executeconstconfig=awaitrunner.parseYamlFile("workflow.yaml");construn=awaitrunner.execute(config,"workflow-name",{task: "Build the feature",});// Pause / resume / abortrunner.pause();runner.unpause();runner.abort();// Resume a failed runconstresumed=awaitrunner.resume(run.id);

Zero-Config Convenience Function

import{runWorkflow}from"@relayflows/core";constresult=awaitrunWorkflow("workflow.yaml",{workflow: "deploy",vars: {environment: "staging"},onEvent: (event)=>{if(event.type!=='broker:event')console.log(event.type);},});

Coordination

Barriers

Synchronization points that wait for specific steps to complete:

coordination:
barriers:
- name: all-reviews-donewaitFor: [review-arch, review-security, review-correctness]timeoutMs: 900000consensusStrategy: majority # majority | unanimous | quorum

Shared State

Agents can share state during execution:

state:
backend: memory # memory | redis | databasettlMs: 86400000namespace: my-workflow

Supported Agent CLIs

CLIDescription
claudeClaude Code (Anthropic)
codexCodex CLI (OpenAI)
geminiGemini CLI (Google)
aiderAider coding assistant
gooseGoose AI assistant
opencodeOpenCode CLI
droidDroid CLI

Non-Interactive Agents

By default, agents run in interactive PTY mode with full relay messaging. For workers that just need to execute a task and return output — common in fan-out, map-reduce, and pipeline patterns — set interactive: false to run them as lightweight subprocesses.

YAML

agents:
- name: leadcli: clauderole: "Coordinates work"# interactive: true (default) — full PTY, relay messaging, /exit detection
- name: workercli: codexrole: "Executes tasks"interactive: false # Runs "codex exec <task>", captures stdout

TypeScript

workflow("fan-out-analysis").pattern("fan-out").agent("lead",{cli: "claude",role: "Coordinator"}).agent("worker-1",{cli: "codex",interactive: false,role: "Analyst"}).agent("worker-2",{cli: "codex",interactive: false,role: "Analyst"}).step("analyze-1",{agent: "worker-1",task: "Analyze module A"}).step("analyze-2",{agent: "worker-2",task: "Analyze module B"}).step("synthesize",{agent: "lead",task: "Combine: {{steps.analyze-1.output}} + {{steps.analyze-2.output}}",dependsOn: ["analyze-1","analyze-2"],}).run();

How It Works

AspectInteractive (default)Non-Interactive
ExecutionFull PTY with stdin/stdoutchild_process.spawn() with piped stdio
CLI invocationStandard interactive sessionOne-shot mode (claude -p, codex exec, etc.)
Relay messagingCan send/receive messagesNo messaging — excluded from topology edges
Self-terminationMust output /exitProcess exits naturally when done
Output capturePTY output bufferstdout capture
OverheadHigher (PTY, echo verification, SIGWINCH)Lower (simple subprocess)

Non-Interactive CLI Commands

CLICommandNotes
claudeclaude -p "<task>"Print mode, exits after response
codexcodex exec "<task>"One-shot execution
geminigemini -p "<task>"Prompt mode
opencodeopencode --prompt "<task>"One-shot prompt
droiddroid exec "<task>"One-shot execution
aideraider --message "<task>" --yes-always --no-gitAuto-approve, skip git
goosegoose run --text "<task>" --no-sessionText mode, no session file

When to Use

  • Fan-out workers that process a task and return results
  • Map-reduce mappers that don't need mid-task communication
  • Pipeline stages that transform input to output
  • Any agent that doesn't need turn-by-turn relay messaging

When NOT to Use

  • Lead/coordinator agents that communicate with others
  • Agents in debate, consensus, or reflection patterns
  • Agents that need to receive messages during execution

Agent Slash Commands

Agents running inside a workflow can output slash commands to signal the broker. These are detected in the agent's PTY output at the broker level — the agent simply prints the command on its own line.

/exit

Signals that the agent has completed its current step and is ready to be released.

/exit

The workflow runner waits for each agent to /exit after delivering a step task. When the broker detects /exit in the agent's output (exact line match after ANSI stripping), it:

  1. Emits an agent_exit frame with reason: "agent_requested"
  2. Triggers graceful PTY shutdown

If an agent does not /exit within the step's timeoutMs, the runner treats the step as timed out. As a safety net, steps with file_exists verification will still pass if the expected file is present despite the timeout.

Best practice: Instruct agents to output /exit when done in your step task descriptions:

steps:
- name: build-apiagent: backendtask: | Build the REST API endpoints for user management. When finished, output /exit.

Idle Agent Detection and Nudging

Interactive agents sometimes finish their task but forget to /exit, sitting idle and blocking downstream steps. The runner can detect idle agents and take action automatically.

Configuration

Add idleNudge to your swarm config:

swarm:
pattern: hub-spokeidleNudge:
nudgeAfterMs: 120000# 2 min before first nudge (default)escalateAfterMs: 120000# 2 min after nudge before force-release (default)maxNudges: 1# Nudges before escalation (default)

All built-in templates include idle nudging with these defaults.

How It Works

  1. Detection: The broker tracks agent output timestamps and emits agent_idle events when an agent goes silent for the configured threshold
  2. Nudge: For hub patterns (hub-spoke, fan-out, hierarchical, etc.), the runner tells the hub agent to check on the idle agent. For non-hub patterns, a system message is injected directly into the agent's PTY
  3. Escalation: If the agent remains idle after maxNudges attempts, the runner force-releases it and captures whatever output was produced
  4. No config: When idleNudge is omitted, the runner uses simple waitForExit (backward compatible)

Events

The runner emits two new events for idle nudging:

EventDescription
step:nudgedFired when a nudge message is sent to an idle agent
step:force-releasedFired when an agent is force-released after exhausting nudges

Automatic Step Owner and Review

For interactive agent steps, the runner uses a point-person-led completion model:

  1. Elects a step owner (prefers lead/coordinator-style agents, falls back to the step agent)
  2. Runs a completion decision pipeline — checks deterministic verification first, then owner judgment, then evidence
  3. Owner can issue structured decisions via OWNER_DECISION: COMPLETE|INCOMPLETE_RETRY|INCOMPLETE_FAIL|NEEDS_CLARIFICATION with optional REASON: <text>
  4. Review parsing is tolerant — accepts "Approved", "Complete", "LGTM", not just exact REVIEW_DECISION: APPROVE
  5. Markers are optional acceleratorsSTEP_COMPLETE:<step-name> still works as a fast-path but is never required
  6. Stores primary output plus review output in the step artifact

Evidence-based completion: The runner collects channel messages, file artifacts, process exit codes, and coordination signals (e.g., WORKER_DONE posted in channel) as completion evidence. When sufficient evidence exists, the step completes without requiring any sentinel marker.

Deterministic and worktree steps are unchanged and do not require owner/review delegation.

Sandbox Execution

By default the runner spawns steps as local child processes. To run them in isolated sandboxes instead, select a provider — the runner still owns command construction, env, cwd, timeout, and the whole DAG/retry/verification pipeline; the provider only supplies "where the command runs".

# Off by default. Unset the flag to get local child processes back.export RELAYFLOWS_SANDBOX_PROVIDER=daytona
export DAYTONA_API_KEY=...
export RELAYFLOWS_SANDBOX_HOME_DIR=/home/daytona # image-specific, requiredexport RELAYFLOWS_SANDBOX_SNAPSHOT=my-snapshot # optional

Or in code:

import{WorkflowRunner}from"@relayflows/core";construnner=newWorkflowRunner({sandbox: {provider: "daytona",homeDir: "/home/daytona"},});
ProviderWhat it gives you
none (default)No sandbox. Local child processes, exactly as before.
daytonaReal remote sandboxes via @agent-relay/sandbox. Needs the optional peer @daytonaio/sdk.
local-processReal local processes in a private per-step directory with its own HOME. Isolates the filesystem root, not the machine — good for development and CI, not a security boundary.

Reversibility.provider: "none" (or an unset RELAYFLOWS_SANDBOX_PROVIDER) produces no backend at all, so nothing about the default path changes. An explicit executor or processBackend still wins over sandbox config, so a host that injects its own backend today keeps it.

Custom providers. Register a runtime under any name, or hand one in directly. This is the seam a host uses to plug in a runtime that does not live in this repo:

import{registerSandboxProvider,WorkflowRunner}from"@relayflows/core";registerSandboxProvider("my-runtime",(config)=>newMyRuntime(config));// ...or skip the registry entirely:newWorkflowRunner({sandbox: {runtime: myRuntime}});

A runtime needs five methods — launch, exec, uploadFile, getHomeDir, destroy — matching @agent-relay/sandbox's WorkflowRuntime.

Schema Validation

A JSON Schema is available at packages/core/src/schema.json for editor autocompletion and validation of relay.yaml files.

Development

npm install
npm run typecheck
npm run test

Requirements

  • Node.js 22+
  • @relayflows/cli installed (npm install -g @relayflows/cli)
  • For Python: Python 3.10+ with pip install agent-relay
  • For TypeScript workflow files: tsx or ts-node installed

License

Apache-2.0 — Copyright 2025 Agent Workforce Incorporated

About

Orchestrate multi-step, multi-agent execution across Agent Relay workers

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

relayflows

npmLicense

Workflow engine and CLI for Agent Relay. Orchestrate multi-agent workflows using YAML, TypeScript, or Python. Define agents, wire up dependencies, and let the runner handle execution, retries, and verification.

Quick Start

CLI

# Run a YAML workflow
relayflows run workflow.yaml
# Run a TypeScript workflow
relayflows run workflow.ts
# Run a Python workflow
relayflows run workflow.py
# Run a specific named workflow from a file
relayflows run workflow.yaml --workflow deploy

TypeScript

import{workflow}from"@relayflows/core";constresult=awaitworkflow("ship-feature").pattern("dag").agent("planner",{cli: "claude",role: "Plans implementation"}).agent("developer",{cli: "codex",role: "Writes code"}).agent("reviewer",{cli: "claude",role: "Reviews code"}).step("plan",{agent: "planner",task: "Create implementation plan for user authentication",}).step("implement",{agent: "developer",task: "Implement the plan",dependsOn: ["plan"],}).step("review",{agent: "reviewer",task: "Review the implementation",dependsOn: ["implement"],}).run();console.log(result.status);// "completed" | "completed_early" | "failed" | "cancelled" | "needs_human"

Python

fromagent_relayimportworkflowresult= (
workflow("ship-feature")
.pattern("dag")
.agent("planner", cli="claude", role="Plans implementation")
.agent("developer", cli="codex", role="Writes code")
.agent("reviewer", cli="claude", role="Reviews code")
.step("plan", agent="planner", task="Create implementation plan for user auth")
.step("implement", agent="developer", task="Implement the plan", depends_on=["plan"])
.step("review", agent="reviewer", task="Review the implementation", depends_on=["implement"])
.run()
)

Watching a Run

When a run starts, the runner mints and prints a link you can open to follow it live:

[workflow 00:02] Workspace created for this workflow.
[workflow 00:02] Observer: https://agentrelay.com/observer?key=ot_live_...
[workflow 00:02] Channel: wf-ship-feature-a1b2c3

Open the Observer: URL and you see messages, agent activity, handoffs, and deliveries in real time. The link carries a scoped observer token (ot_live_): read-only, expiring in 24 hours, and individually revocable.

Minting is best-effort, so a link is not guaranteed — see If no link appears. A failed mint never fails the run.

Treat the link itself as a shared secret. The token is a bearer credential in a query parameter, so anyone who gets the URL can read the stream it covers until it expires or you revoke it. It is far safer than a workspace key — it cannot send, spawn, or administer — but it is not public.

The two ways a run gets a workspace

Which link you get depends on where the run's Relaycast workspace comes from.

No RELAY_API_KEY set — the runner creates a throwaway workspace for this run alone and mints an observer link covering all of it, DMs included. The workspace is anonymous and disappears from your reach when the run ends, so copy the link while the run is going. Nothing persists the underlying key, and there is no way to recover it afterward.

RELAY_API_KEY set — the runner uses your workspace and mints a link scoped to just this run's channel, with agent DMs excluded, so the link does not expose unrelated traffic in a shared workspace. This is the better setup for anything you may want to revisit: the run is in a workspace you own, so you can mint fresh links whenever you like.

# One-time: create a workspace you own and keep the key
curl -sX POST https://api.relaycast.dev/v1/workspaces \
-H 'content-type: application/json' -d '{"name":"my-workflows"}' \
| jq -r '.data.api_key // .api_key'export RELAY_API_KEY=rk_live_... # put this in your shell profile
relayflows run workflow.yaml

If you already use the Agent Relay CLI, agent-relay workspace key --reveal-secrets prints the key of your active workspace. Note it is masked without --reveal-secrets.

Minting more links yourself

With a workspace you own, agent-relay observer mints links on demand:

agent-relay observer # read-only link, 24h, DMs excluded
agent-relay observer --channels wf-ship-a1b2c3 # scope to one run
agent-relay observer --include-dms # include agent DMs
agent-relay observer --expires 7d # longer-lived link
agent-relay observer list # what is outstanding
agent-relay observer revoke <id># cut one off immediately

Never share the workspace key

A workspace key (rk_live_) is an administrative credential — it can send messages, spawn and remove agents, and change workspace settings. Do not put one in an observer URL, a chat message, or a terminal transcript; query strings end up in browser history, referrer headers, and proxy logs. The realtime endpoint rejects it outright, so a link built from one cannot stream anyway.

Workspace key (rk_live_)Observer token (ot_live_)
Read messages and activityyesyes
Send, spawn agents, administeryesno
Expiresnoyes
Revocable individuallynoyes
Scopable to channelsnoyes

The runner only ever prints ot_live_ links, and scrubs rk_live_ values out of channel output.

Configuration

VariablePurpose
RELAY_API_KEYWorkspace key to run against. Unset means a throwaway workspace per run.
RELAY_OBSERVER_URLObserver dashboard base. Defaults to https://agentrelay.com/observer.
RELAY_OBSERVER_EXPIRESLink lifetime as 30m / 24h / 7d. Defaults to 24h; unparseable or over 90d falls back to 24h.
RELAYCAST_BASE_URLRelaycast engine base. Defaults to https://api.relaycast.dev.

If no link appears

  • Observation: unavailable — the token could not be minted (engine unreachable, or it rejected the request). The run is unaffected; minting is best-effort by design and never fails a run.
  • No observer lines at all — the run needed no broker: every step was deterministic, worktree, integration, or waitFor, or an external executor handled agent spawning, or Relaycast was disabled with AGENT_RELAY_WORKFLOW_DISABLE_RELAYCAST=1.
  • Observation: run agent-relay observer — you set RELAY_API_KEY and minting failed. Mint a link by hand with agent-relay observer.

Consumer-Facing Apps + AI SDK Communicate Flows

A good production split is:

  1. AI SDK app handles the user conversation and streaming UI
  2. Communicate / onRelay() lets that point-person coordinate with specialists over Relay
  3. Workflows / runWorkflow() take over when a request needs multi-step execution, verification, or handoffs
import{streamText,wrapLanguageModel}from'ai';import{openai}from'@ai-sdk/openai';import{Relay}from'@agent-relay/sdk/communicate';import{onRelay}from'@agent-relay/sdk/communicate/adapters/ai-sdk';import{runWorkflow}from'@relayflows/core';exportasyncfunctionPOST(req: Request){const{ prompt, escalate, repo }=awaitreq.json();constrelay=newRelay('AppLead');constrelaySession=onRelay({name: 'AppLead',instructions: 'You are the customer-facing lead. Keep the user updated and delegate implementation via Relay when needed.',},relay);constmodel=wrapLanguageModel({model: openai('gpt-4o-mini'),middleware: relaySession.middleware,});if(escalate){constworkflow=awaitrunWorkflow('workflows/feature-dev.yaml',{vars: {task: prompt, repo },});returnResponse.json({status: workflow.status,runId: workflow.runId});}returnstreamText({
model,tools: relaySession.tools,system: 'Answer directly when possible; coordinate internally when the task needs specialists.',
prompt,}).toUIMessageStreamResponse({onFinish(){relaySession.cleanup();voidrelay.close();},});}

That pattern keeps the user experience snappy while still letting longer Relay workflows run with proper ownership, retries, and verification.

YAML Format

Workflows are defined as relay.yaml files:

version: "1.0"name: my-workflowdescription: "Optional description"swarm:
pattern: dag # Execution pattern (see Patterns below)maxConcurrency: 3# Max agents running in paralleltimeoutMs: 3600000# Global timeout (1 hour)channel: my-channel # Relay channel for agent communicationagents:
- name: backendcli: claude # claude | codex | gemini | aider | goose | opencode | droidrole: "Backend engineer"constraints:
model: opustimeoutMs: 600000retries: 2
- name: testercli: codexrole: "Test engineer"interactive: false # Non-interactive: runs as subprocess, no PTY/messaging# A persona replaces cli + role. Its harness, model, standing instructions,# installed skills, MCP servers, and harness settings come from the spec.# Persona agents are interactive-only: do not set cli, preset, or# constraints.model, and do not use interactive: false.
- name: integrationspersona: nango-integrationsworkflows:
- name: build-and-testonError: retry # fail | skip | retrysteps:
- name: build-apiagent: backendtask: "Build the REST API endpoints for user management"verification:
type: file_existsvalue: "src/api/users.ts"retries: 1
- name: write-testsagent: testertask: "Write integration tests for: {{steps.build-api.output}}"dependsOn: [build-api]
- name: run-testsagent: testertask: "Run the test suite and report results"dependsOn: [write-tests]verification:
type: exit_codevalue: "0"errorHandling:
strategy: retrymaxRetries: 2retryDelayMs: 5000repairAgent: testerrepairRetries: 2onExhaustion: needs-humannotifyChannel: my-channel

Template Variables

Use {{variable}} for user-provided values and {{steps.STEP_NAME.output}} for previous step outputs:

steps:
- name: planagent: plannertask: "Plan implementation for: {{task}}"# User variable
- name: implementagent: developerdependsOn: [plan]task: "Implement: {{steps.plan.output}}"# Previous step output

User variables are passed via the CLI or programmatically:

awaitrunWorkflow("workflow.yaml",{vars: {task: "Add OAuth2 support"},});

Blocking Slack Questions

Workflows can pause on a Slack question by adding a Slack integration step with action: askQuestion. The step posts a question, waits for the first human reply in the message thread, exposes that answer through {{steps.<name>.output}}, and can inject it into a running agent when injectToAgent is set.

workflows:
- name: defaultsteps:
- name: ask-humantype: integrationintegration: slackaction: askQuestionparams:
channel: "#engineering"text: "The implementer is blocked on migration strategy. Which path should it take?"waitTimeoutMs: "3600000"injectToAgent: "backend-runtime-name"injectTemplate: "HUMAN_ANSWER: {{answer.text}}"output: '{"format":"text","path":"answer.text"}'
- name: continue-with-answeragent: backenddependsOn: [ask-human]task: "Continue using this human guidance: {{steps.ask-human.output}}"

askQuestion can use the local Slack API runtime, or Relayfile-backed Slack writebacks when the workflow already has a Relayfile Slack integration.

Interactive agent steps can also opt into marker-driven assistance. With humanAssistance.slack enabled, an agent can print a line beginning with HUMAN_QUESTION:. The runner posts that question to Slack, blocks while waiting for a human reply, then injects HUMAN_ANSWER: ... back into that same agent session. If integrations.relayfile is present, Relayflows automatically uses the existing Relayfile/Pear Slack connection; no Slack bot token, Relayfile workspace id, or Relayfile token is required in the workflow.

Runnable TypeScript proof: examples/typescript/slack-human-assistance-e2e.ts.

swarm:
pattern: daghumanAssistance:
slack:
channel: proj-cloudtimeoutMs: 3600000integrations:
relayfile: {}workflows:
- name: defaultsteps:
- name: implementagent: backendtask: "Proceed, but ask for human guidance if the migration strategy is ambiguous."

Relayfile Event Subscriptions

Relayflows can subscribe to Relayfile integration events and inject matching events into active agents. Workflow-level subscriptions live under integrations.subscriptions; agent-level subscriptions use Workforce-style agents[].watch or Relayflows-style agents[].subscriptions.

integrations:
relayfile: {}subscriptions:
- name: pr-feedbackprovider: githubpath: /github/repos/acme/web/pulls/42/**events: [created, updated]agents: [pr-babysitter]agents:
- name: pr-babysittercli: codexwatch:
- paths: [/github/repos/acme/web/pulls/42/reviews/**]events: [created, updated]workflows:
- name: defaultsteps:
- name: babysit-pragent: pr-babysittertask: | Stay active and wait for INTEGRATION_EVENT messages about PR feedback. Read the Relayfile path from the event, address comments until no open feedback remains, then notify the user in Slack.

Verification Checks

Each step can include a verification check. Verification is one input to the runner's completion decision pipeline — when verification passes, the step completes even without a sentinel marker.

TypeDescription
exit_codeAgent must exit with the specified code (preferred for code-editing steps)
file_existsA file must exist at the specified path after the step
output_containsStep output must contain the specified string (optional accelerator)
customNo-op in the runner; handled by external callers
# Preferred — deterministic verificationverification:
type: exit_codevalue: "0"description: "Process exited successfully"# Also valid — output_contains as an optional acceleratorverification:
type: output_containsvalue: "IMPLEMENTATION_COMPLETE"description: "Agent confirms completion (optional fast-path)"

Completion Decision Pipeline

The runner uses a multi-signal pipeline to decide step completion:

  1. Deterministic verification — if a verification check passes, the step completes immediately (completed_verified)
  2. Owner decision — the step owner can issue OWNER_DECISION: COMPLETE|INCOMPLETE_RETRY|INCOMPLETE_FAIL (completed_by_owner_decision)
  3. Evidence-based completion — channel messages, file artifacts, and exit codes are collected as evidence (completed_by_evidence)
  4. Marker fast-pathSTEP_COMPLETE:<step-name> still works as an accelerator but is never required
Completion StateMeaning
completed_verifiedDeterministic verification passed
completed_by_owner_decisionOwner approved the step
completed_by_evidenceEvidence-based completion
retry_requested_by_ownerOwner requested retry
failed_verificationVerification explicitly failed
failed_owner_decisionOwner rejected the step
failed_no_evidenceNo verification, no owner decision, no evidence

Review parsing is tolerant: The runner accepts semantically equivalent outputs like "Approved", "Complete", "LGTM" — not just exact REVIEW_DECISION: APPROVE strings.

Swarm Patterns

The swarm.pattern field controls how agents are coordinated:

Core Patterns

PatternDescription
dagDirected acyclic graph — steps run based on dependency edges (default)
fan-outAll agents run in parallel
pipelineSequential chaining of steps
hub-spokeCentral hub coordinates spoke agents
consensusAgents vote on decisions
meshFull communication graph between agents
handoffSequential handoff between agents
cascadeWaterfall with phase gates
debateAgents propose and counter-argue
hierarchicalMulti-level reporting structure

Data Processing Patterns

PatternDescription
map-reduceSplit work into chunks (mappers), process in parallel, aggregate results (reducers)
scatter-gatherFan out requests to workers, collect and synthesize responses

Supervision & Quality Patterns

PatternDescription
supervisorMonitor agent monitors workers, restarts on failure, manages health
reflectionAgent produces output, critic reviews and provides feedback for iteration
verifierProducer agents submit work to verifier agents for validation

Adversarial & Validation Patterns

PatternDescription
red-teamAttacker agents probe for weaknesses, defender agents respond
auctionAuctioneer broadcasts tasks, agents bid based on capability/cost

Resilience Patterns

PatternDescription
escalationStart with fast/cheap agents, escalate to more capable on failure
sagaDistributed transactions with compensating actions on failure
circuit-breakerPrimary agent with fallback chain, fail fast and recover

Collaborative Patterns

PatternDescription
blackboardShared workspace where agents contribute incrementally to a solution
swarmEmergent behavior from simple agent rules (neighbor communication)

Auto-Selection by Role

When swarm.pattern is omitted, the coordinator auto-selects based on agent roles. Patterns are checked in priority order below (first match wins):

PriorityPatternRequired Roles/Config
1dagSteps with dependsOn
2consensusUses coordination.consensusStrategy config
3map-reducemapper + reducer
4red-team(attacker OR red-team) + (defender OR blue-team)
5reflectioncritic
6escalationtier-1, tier-2, etc.
7auctionauctioneer
8sagasaga-orchestrator OR compensate-handler
9circuit-breakerfallback, backup, OR primary
10blackboardblackboard OR shared-workspace
11swarmhive-mind OR swarm-agent
12verifierverifier
13supervisorsupervisor
14hierarchicallead (with 4+ agents)
15hub-spokehub OR coordinator
16pipelineUnique agents per step, 3+ steps
17fan-outDefault fallback

Error Handling

Step-Level

steps:
- name: risky-stepagent: workertask: "Do something that might fail"retries: 3# Retry up to 3 times on failuretimeoutMs: 300000# 5 minute timeout

Successful early termination

A deterministic gate can explicitly declare exit codes that mean “there is no work to do.” A matching code ends the run with the distinct completed_early status and skips every step that has not started:

steps:
- name: claim-worktype: deterministiccommand: node bin/claim-work.mjsterminalSuccessExitCodes: [78]
- name: process-claimagent: workertask: Process the claimed workdependsOn: [claim-work]

Terminal-capable gates are scheduling barriers, so other ready work does not race the gate. The triggering step is completed with completion reason completed_early_exit; remaining steps are skipped, and the CLI exits 0 while clearly reporting COMPLETED EARLY. Verification still applies, so a verification failure remains a real failure.

This behavior is opt-in. Without terminalSuccessExitCodes, exit 78 and every other non-zero exit retain their existing failure behavior. The new completed_early run status is an additive public API value: consumers with exhaustive status switches, strict validators, database constraints, or terminal-status polling must handle it separately from completed.

Workflow-Level

The onError field on a workflow controls what happens when a step fails:

ValueBehavior
fail / fail-fastStop immediately, skip downstream steps
skip / continueSkip downstream dependents, continue independent steps
retryRetry the step; deterministic gates ask a workflow agent to repair before each retry when an agent is available

Global

errorHandling:
strategy: retrymaxRetries: 2retryDelayMs: 5000repairAgent: testerrepairRetries: 2notifyChannel: alerts

Retry-mode workflows are repair-aware by default. Deterministic step failures, verification gate failures, and malformed agent artifacts are treated as repairable work before terminal failure. The runner chooses errorHandling.repairAgent when set, otherwise it uses the step's owning/upstream agent when possible, then falls back to the best available workflow agent. The selected agent gets the failed command or agent output, working directory, exit information, and captured evidence, then the failed gate or step is retried. Use repairRetries: 0, strategy: fail-fast, or strategy: continue when a workflow intentionally should not invoke repair agents. Set onExhaustion: needs-human to end an exhausted repairable run as needs_human instead of failed.

Built-in Templates

Six pre-built workflow templates are included:

TemplatePatternDescription
feature-devhub-spokePlan, implement, review, and finalize a feature
bug-fixhub-spokeInvestigate, patch, validate, and document a bug fix
code-reviewfan-outParallel multi-reviewer assessment with consolidated findings
security-auditpipelineScan, triage, remediate, and verify security issues
refactorhierarchicalAnalyze, plan, execute, and validate a refactor
documentationhandoffResearch, draft, review, and publish documentation

Using Templates

import{TemplateRegistry,WorkflowRunner}from"@relayflows/core";constregistry=newTemplateRegistry();// List available templatesconsttemplates=awaitregistry.listTemplates();// Load and run a templateconstconfig=awaitregistry.loadTemplate("feature-dev");construnner=newWorkflowRunner();constresult=awaitrunner.execute(config,undefined,{task: "Add WebSocket support to the API",});// Install a custom template from a URLawaitregistry.installExternalTemplate("https://example.com/my-template.yaml","my-template");

TypeScript Builder API

The builder constructs a RelayYamlConfig object and can run it, export it as YAML, or return the raw config.

import{workflow}from"@relayflows/core";// Build and runconstresult=awaitworkflow("my-workflow").pattern("dag").maxConcurrency(3).timeout(60*60*1000).channel("my-channel").agent("backend",{cli: "claude",role: "Backend engineer",model: "opus",retries: 2,}).agent("frontend",{cli: "codex",role: "Frontend engineer",interactive: false,// Non-interactive subprocess mode}).step("api",{agent: "backend",task: "Build REST API",verification: {type: "output_contains",value: "API_READY"},}).step("ui",{agent: "frontend",task: "Build the UI",dependsOn: ["api"],}).onError("retry",{maxRetries: 2,retryDelayMs: 5000}).run();// Or export to YAMLconstyaml=workflow("my-workflow").pattern("dag").agent("worker",{cli: "claude"}).step("task1",{agent: "worker",task: "Do something"}).toYaml();// Or get the raw config objectconstconfig=workflow("my-workflow").pattern("dag").agent("worker",{cli: "claude"}).step("task1",{agent: "worker",task: "Do something"}).toConfig();

Python Builder API

The Python builder ships with @agent-relay/sdk-py:

pip install agent-relay
fromagent_relayimportworkflow, run_yaml# Build and runresult= (
workflow("my-workflow")
.pattern("dag")
.max_concurrency(3)
.timeout(3600000)
.agent("backend", cli="claude", role="Backend engineer")
.agent("frontend", cli="codex", role="Frontend engineer")
.step("api", agent="backend", task="Build REST API")
.step("ui", agent="frontend", task="Build the UI", depends_on=["api"])
.on_error("retry", max_retries=2, retry_delay_ms=5000)
.run()
)
# Run an existing YAML fileresult=run_yaml("workflows/my-workflow.yaml")
# Export to YAML stringyaml_str= (
workflow("my-workflow")
.pattern("dag")
.agent("worker", cli="claude")
.step("task1", agent="worker", task="Do something")
.to_yaml()
)
# Get the raw config dictconfig= (
workflow("my-workflow")
.pattern("dag")
.agent("worker", cli="claude")
.step("task1", agent="worker", task="Do something")
.to_config()
)

Programmatic API

For full control, use the WorkflowRunner directly:

import{WorkflowRunner}from"@relayflows/core";construnner=newWorkflowRunner({cwd: "/path/to/project",// Working directory (default: process.cwd())relay: {port: 3000},// AgentRelay options (optional)});// Listen to events (broker:event fires frequently — filter it out for cleaner output)runner.on((event)=>{if(event.type==='broker:event')return;console.log(event.type,event);});// Parse and executeconstconfig=awaitrunner.parseYamlFile("workflow.yaml");construn=awaitrunner.execute(config,"workflow-name",{task: "Build the feature",});// Pause / resume / abortrunner.pause();runner.unpause();runner.abort();// Resume a failed runconstresumed=awaitrunner.resume(run.id);

Zero-Config Convenience Function

import{runWorkflow}from"@relayflows/core";constresult=awaitrunWorkflow("workflow.yaml",{workflow: "deploy",vars: {environment: "staging"},onEvent: (event)=>{if(event.type!=='broker:event')console.log(event.type);},});

Coordination

Barriers

Synchronization points that wait for specific steps to complete:

coordination:
barriers:
- name: all-reviews-donewaitFor: [review-arch, review-security, review-correctness]timeoutMs: 900000consensusStrategy: majority # majority | unanimous | quorum

Shared State

Agents can share state during execution:

state:
backend: memory # memory | redis | databasettlMs: 86400000namespace: my-workflow

Supported Agent CLIs

CLIDescription
claudeClaude Code (Anthropic)
codexCodex CLI (OpenAI)
geminiGemini CLI (Google)
aiderAider coding assistant
gooseGoose AI assistant
opencodeOpenCode CLI
droidDroid CLI

Non-Interactive Agents

By default, agents run in interactive PTY mode with full relay messaging. For workers that just need to execute a task and return output — common in fan-out, map-reduce, and pipeline patterns — set interactive: false to run them as lightweight subprocesses.

YAML

agents:
- name: leadcli: clauderole: "Coordinates work"# interactive: true (default) — full PTY, relay messaging, /exit detection
- name: workercli: codexrole: "Executes tasks"interactive: false # Runs "codex exec <task>", captures stdout

TypeScript

workflow("fan-out-analysis").pattern("fan-out").agent("lead",{cli: "claude",role: "Coordinator"}).agent("worker-1",{cli: "codex",interactive: false,role: "Analyst"}).agent("worker-2",{cli: "codex",interactive: false,role: "Analyst"}).step("analyze-1",{agent: "worker-1",task: "Analyze module A"}).step("analyze-2",{agent: "worker-2",task: "Analyze module B"}).step("synthesize",{agent: "lead",task: "Combine: {{steps.analyze-1.output}} + {{steps.analyze-2.output}}",dependsOn: ["analyze-1","analyze-2"],}).run();

How It Works

AspectInteractive (default)Non-Interactive
ExecutionFull PTY with stdin/stdoutchild_process.spawn() with piped stdio
CLI invocationStandard interactive sessionOne-shot mode (claude -p, codex exec, etc.)
Relay messagingCan send/receive messagesNo messaging — excluded from topology edges
Self-terminationMust output /exitProcess exits naturally when done
Output capturePTY output bufferstdout capture
OverheadHigher (PTY, echo verification, SIGWINCH)Lower (simple subprocess)

Non-Interactive CLI Commands

CLICommandNotes
claudeclaude -p "<task>"Print mode, exits after response
codexcodex exec "<task>"One-shot execution
geminigemini -p "<task>"Prompt mode
opencodeopencode --prompt "<task>"One-shot prompt
droiddroid exec "<task>"One-shot execution
aideraider --message "<task>" --yes-always --no-gitAuto-approve, skip git
goosegoose run --text "<task>" --no-sessionText mode, no session file

When to Use

  • Fan-out workers that process a task and return results
  • Map-reduce mappers that don't need mid-task communication
  • Pipeline stages that transform input to output
  • Any agent that doesn't need turn-by-turn relay messaging

When NOT to Use

  • Lead/coordinator agents that communicate with others
  • Agents in debate, consensus, or reflection patterns
  • Agents that need to receive messages during execution

Agent Slash Commands

Agents running inside a workflow can output slash commands to signal the broker. These are detected in the agent's PTY output at the broker level — the agent simply prints the command on its own line.

/exit

Signals that the agent has completed its current step and is ready to be released.

/exit

The workflow runner waits for each agent to /exit after delivering a step task. When the broker detects /exit in the agent's output (exact line match after ANSI stripping), it:

  1. Emits an agent_exit frame with reason: "agent_requested"
  2. Triggers graceful PTY shutdown

If an agent does not /exit within the step's timeoutMs, the runner treats the step as timed out. As a safety net, steps with file_exists verification will still pass if the expected file is present despite the timeout.

Best practice: Instruct agents to output /exit when done in your step task descriptions:

steps:
- name: build-apiagent: backendtask: | Build the REST API endpoints for user management. When finished, output /exit.

Idle Agent Detection and Nudging

Interactive agents sometimes finish their task but forget to /exit, sitting idle and blocking downstream steps. The runner can detect idle agents and take action automatically.

Configuration

Add idleNudge to your swarm config:

swarm:
pattern: hub-spokeidleNudge:
nudgeAfterMs: 120000# 2 min before first nudge (default)escalateAfterMs: 120000# 2 min after nudge before force-release (default)maxNudges: 1# Nudges before escalation (default)

All built-in templates include idle nudging with these defaults.

How It Works

  1. Detection: The broker tracks agent output timestamps and emits agent_idle events when an agent goes silent for the configured threshold
  2. Nudge: For hub patterns (hub-spoke, fan-out, hierarchical, etc.), the runner tells the hub agent to check on the idle agent. For non-hub patterns, a system message is injected directly into the agent's PTY
  3. Escalation: If the agent remains idle after maxNudges attempts, the runner force-releases it and captures whatever output was produced
  4. No config: When idleNudge is omitted, the runner uses simple waitForExit (backward compatible)

Events

The runner emits two new events for idle nudging:

EventDescription
step:nudgedFired when a nudge message is sent to an idle agent
step:force-releasedFired when an agent is force-released after exhausting nudges

Automatic Step Owner and Review

For interactive agent steps, the runner uses a point-person-led completion model:

  1. Elects a step owner (prefers lead/coordinator-style agents, falls back to the step agent)
  2. Runs a completion decision pipeline — checks deterministic verification first, then owner judgment, then evidence
  3. Owner can issue structured decisions via OWNER_DECISION: COMPLETE|INCOMPLETE_RETRY|INCOMPLETE_FAIL|NEEDS_CLARIFICATION with optional REASON: <text>
  4. Review parsing is tolerant — accepts "Approved", "Complete", "LGTM", not just exact REVIEW_DECISION: APPROVE
  5. Markers are optional acceleratorsSTEP_COMPLETE:<step-name> still works as a fast-path but is never required
  6. Stores primary output plus review output in the step artifact

Evidence-based completion: The runner collects channel messages, file artifacts, process exit codes, and coordination signals (e.g., WORKER_DONE posted in channel) as completion evidence. When sufficient evidence exists, the step completes without requiring any sentinel marker.

Deterministic and worktree steps are unchanged and do not require owner/review delegation.

Sandbox Execution

By default the runner spawns steps as local child processes. To run them in isolated sandboxes instead, select a provider — the runner still owns command construction, env, cwd, timeout, and the whole DAG/retry/verification pipeline; the provider only supplies "where the command runs".

# Off by default. Unset the flag to get local child processes back.export RELAYFLOWS_SANDBOX_PROVIDER=daytona
export DAYTONA_API_KEY=...
export RELAYFLOWS_SANDBOX_HOME_DIR=/home/daytona # image-specific, requiredexport RELAYFLOWS_SANDBOX_SNAPSHOT=my-snapshot # optional

Or in code:

import{WorkflowRunner}from"@relayflows/core";construnner=newWorkflowRunner({sandbox: {provider: "daytona",homeDir: "/home/daytona"},});
ProviderWhat it gives you
none (default)No sandbox. Local child processes, exactly as before.
daytonaReal remote sandboxes via @agent-relay/sandbox. Needs the optional peer @daytonaio/sdk.
local-processReal local processes in a private per-step directory with its own HOME. Isolates the filesystem root, not the machine — good for development and CI, not a security boundary.

Reversibility.provider: "none" (or an unset RELAYFLOWS_SANDBOX_PROVIDER) produces no backend at all, so nothing about the default path changes. An explicit executor or processBackend still wins over sandbox config, so a host that injects its own backend today keeps it.

Custom providers. Register a runtime under any name, or hand one in directly. This is the seam a host uses to plug in a runtime that does not live in this repo:

import{registerSandboxProvider,WorkflowRunner}from"@relayflows/core";registerSandboxProvider("my-runtime",(config)=>newMyRuntime(config));// ...or skip the registry entirely:newWorkflowRunner({sandbox: {runtime: myRuntime}});

A runtime needs five methods — launch, exec, uploadFile, getHomeDir, destroy — matching @agent-relay/sandbox's WorkflowRuntime.

Schema Validation

A JSON Schema is available at packages/core/src/schema.json for editor autocompletion and validation of relay.yaml files.

Development

npm install
npm run typecheck
npm run test

Requirements

  • Node.js 22+
  • @relayflows/cli installed (npm install -g @relayflows/cli)
  • For Python: Python 3.10+ with pip install agent-relay
  • For TypeScript workflow files: tsx or ts-node installed

License

Apache-2.0 — Copyright 2025 Agent Workforce Incorporated

About

Orchestrate multi-step, multi-agent execution across Agent Relay workers

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

relayflows

npmLicense

Workflow engine and CLI for Agent Relay. Orchestrate multi-agent workflows using YAML, TypeScript, or Python. Define agents, wire up dependencies, and let the runner handle execution, retries, and verification.

Quick Start

CLI

# Run a YAML workflow
relayflows run workflow.yaml
# Run a TypeScript workflow
relayflows run workflow.ts
# Run a Python workflow
relayflows run workflow.py
# Run a specific named workflow from a file
relayflows run workflow.yaml --workflow deploy

TypeScript

import{workflow}from"@relayflows/core";constresult=awaitworkflow("ship-feature").pattern("dag").agent("planner",{cli: "claude",role: "Plans implementation"}).agent("developer",{cli: "codex",role: "Writes code"}).agent("reviewer",{cli: "claude",role: "Reviews code"}).step("plan",{agent: "planner",task: "Create implementation plan for user authentication",}).step("implement",{agent: "developer",task: "Implement the plan",dependsOn: ["plan"],}).step("review",{agent: "reviewer",task: "Review the implementation",dependsOn: ["implement"],}).run();console.log(result.status);// "completed" | "completed_early" | "failed" | "cancelled" | "needs_human"

Python

fromagent_relayimportworkflowresult= (
workflow("ship-feature")
.pattern("dag")
.agent("planner", cli="claude", role="Plans implementation")
.agent("developer", cli="codex", role="Writes code")
.agent("reviewer", cli="claude", role="Reviews code")
.step("plan", agent="planner", task="Create implementation plan for user auth")
.step("implement", agent="developer", task="Implement the plan", depends_on=["plan"])
.step("review", agent="reviewer", task="Review the implementation", depends_on=["implement"])
.run()
)

Watching a Run

When a run starts, the runner mints and prints a link you can open to follow it live:

[workflow 00:02] Workspace created for this workflow.
[workflow 00:02] Observer: https://agentrelay.com/observer?key=ot_live_...
[workflow 00:02] Channel: wf-ship-feature-a1b2c3

Open the Observer: URL and you see messages, agent activity, handoffs, and deliveries in real time. The link carries a scoped observer token (ot_live_): read-only, expiring in 24 hours, and individually revocable.

Minting is best-effort, so a link is not guaranteed — see If no link appears. A failed mint never fails the run.

Treat the link itself as a shared secret. The token is a bearer credential in a query parameter, so anyone who gets the URL can read the stream it covers until it expires or you revoke it. It is far safer than a workspace key — it cannot send, spawn, or administer — but it is not public.

The two ways a run gets a workspace

Which link you get depends on where the run's Relaycast workspace comes from.

No RELAY_API_KEY set — the runner creates a throwaway workspace for this run alone and mints an observer link covering all of it, DMs included. The workspace is anonymous and disappears from your reach when the run ends, so copy the link while the run is going. Nothing persists the underlying key, and there is no way to recover it afterward.

RELAY_API_KEY set — the runner uses your workspace and mints a link scoped to just this run's channel, with agent DMs excluded, so the link does not expose unrelated traffic in a shared workspace. This is the better setup for anything you may want to revisit: the run is in a workspace you own, so you can mint fresh links whenever you like.

# One-time: create a workspace you own and keep the key
curl -sX POST https://api.relaycast.dev/v1/workspaces \
-H 'content-type: application/json' -d '{"name":"my-workflows"}' \
| jq -r '.data.api_key // .api_key'export RELAY_API_KEY=rk_live_... # put this in your shell profile
relayflows run workflow.yaml

If you already use the Agent Relay CLI, agent-relay workspace key --reveal-secrets prints the key of your active workspace. Note it is masked without --reveal-secrets.

Minting more links yourself

With a workspace you own, agent-relay observer mints links on demand:

agent-relay observer # read-only link, 24h, DMs excluded
agent-relay observer --channels wf-ship-a1b2c3 # scope to one run
agent-relay observer --include-dms # include agent DMs
agent-relay observer --expires 7d # longer-lived link
agent-relay observer list # what is outstanding
agent-relay observer revoke <id># cut one off immediately

Never share the workspace key

A workspace key (rk_live_) is an administrative credential — it can send messages, spawn and remove agents, and change workspace settings. Do not put one in an observer URL, a chat message, or a terminal transcript; query strings end up in browser history, referrer headers, and proxy logs. The realtime endpoint rejects it outright, so a link built from one cannot stream anyway.

Workspace key (rk_live_)Observer token (ot_live_)
Read messages and activityyesyes
Send, spawn agents, administeryesno
Expiresnoyes
Revocable individuallynoyes
Scopable to channelsnoyes

The runner only ever prints ot_live_ links, and scrubs rk_live_ values out of channel output.

Configuration

VariablePurpose
RELAY_API_KEYWorkspace key to run against. Unset means a throwaway workspace per run.
RELAY_OBSERVER_URLObserver dashboard base. Defaults to https://agentrelay.com/observer.
RELAY_OBSERVER_EXPIRESLink lifetime as 30m / 24h / 7d. Defaults to 24h; unparseable or over 90d falls back to 24h.
RELAYCAST_BASE_URLRelaycast engine base. Defaults to https://api.relaycast.dev.

If no link appears

  • Observation: unavailable — the token could not be minted (engine unreachable, or it rejected the request). The run is unaffected; minting is best-effort by design and never fails a run.
  • No observer lines at all — the run needed no broker: every step was deterministic, worktree, integration, or waitFor, or an external executor handled agent spawning, or Relaycast was disabled with AGENT_RELAY_WORKFLOW_DISABLE_RELAYCAST=1.
  • Observation: run agent-relay observer — you set RELAY_API_KEY and minting failed. Mint a link by hand with agent-relay observer.

Consumer-Facing Apps + AI SDK Communicate Flows

A good production split is:

  1. AI SDK app handles the user conversation and streaming UI
  2. Communicate / onRelay() lets that point-person coordinate with specialists over Relay
  3. Workflows / runWorkflow() take over when a request needs multi-step execution, verification, or handoffs
import{streamText,wrapLanguageModel}from'ai';import{openai}from'@ai-sdk/openai';import{Relay}from'@agent-relay/sdk/communicate';import{onRelay}from'@agent-relay/sdk/communicate/adapters/ai-sdk';import{runWorkflow}from'@relayflows/core';exportasyncfunctionPOST(req: Request){const{ prompt, escalate, repo }=awaitreq.json();constrelay=newRelay('AppLead');constrelaySession=onRelay({name: 'AppLead',instructions: 'You are the customer-facing lead. Keep the user updated and delegate implementation via Relay when needed.',},relay);constmodel=wrapLanguageModel({model: openai('gpt-4o-mini'),middleware: relaySession.middleware,});if(escalate){constworkflow=awaitrunWorkflow('workflows/feature-dev.yaml',{vars: {task: prompt, repo },});returnResponse.json({status: workflow.status,runId: workflow.runId});}returnstreamText({
model,tools: relaySession.tools,system: 'Answer directly when possible; coordinate internally when the task needs specialists.',
prompt,}).toUIMessageStreamResponse({onFinish(){relaySession.cleanup();voidrelay.close();},});}

That pattern keeps the user experience snappy while still letting longer Relay workflows run with proper ownership, retries, and verification.

YAML Format

Workflows are defined as relay.yaml files:

version: "1.0"name: my-workflowdescription: "Optional description"swarm:
pattern: dag # Execution pattern (see Patterns below)maxConcurrency: 3# Max agents running in paralleltimeoutMs: 3600000# Global timeout (1 hour)channel: my-channel # Relay channel for agent communicationagents:
- name: backendcli: claude # claude | codex | gemini | aider | goose | opencode | droidrole: "Backend engineer"constraints:
model: opustimeoutMs: 600000retries: 2
- name: testercli: codexrole: "Test engineer"interactive: false # Non-interactive: runs as subprocess, no PTY/messaging# A persona replaces cli + role. Its harness, model, standing instructions,# installed skills, MCP servers, and harness settings come from the spec.# Persona agents are interactive-only: do not set cli, preset, or# constraints.model, and do not use interactive: false.
- name: integrationspersona: nango-integrationsworkflows:
- name: build-and-testonError: retry # fail | skip | retrysteps:
- name: build-apiagent: backendtask: "Build the REST API endpoints for user management"verification:
type: file_existsvalue: "src/api/users.ts"retries: 1
- name: write-testsagent: testertask: "Write integration tests for: {{steps.build-api.output}}"dependsOn: [build-api]
- name: run-testsagent: testertask: "Run the test suite and report results"dependsOn: [write-tests]verification:
type: exit_codevalue: "0"errorHandling:
strategy: retrymaxRetries: 2retryDelayMs: 5000repairAgent: testerrepairRetries: 2onExhaustion: needs-humannotifyChannel: my-channel

Template Variables

Use {{variable}} for user-provided values and {{steps.STEP_NAME.output}} for previous step outputs:

steps:
- name: planagent: plannertask: "Plan implementation for: {{task}}"# User variable
- name: implementagent: developerdependsOn: [plan]task: "Implement: {{steps.plan.output}}"# Previous step output

User variables are passed via the CLI or programmatically:

awaitrunWorkflow("workflow.yaml",{vars: {task: "Add OAuth2 support"},});

Blocking Slack Questions

Workflows can pause on a Slack question by adding a Slack integration step with action: askQuestion. The step posts a question, waits for the first human reply in the message thread, exposes that answer through {{steps.<name>.output}}, and can inject it into a running agent when injectToAgent is set.

workflows:
- name: defaultsteps:
- name: ask-humantype: integrationintegration: slackaction: askQuestionparams:
channel: "#engineering"text: "The implementer is blocked on migration strategy. Which path should it take?"waitTimeoutMs: "3600000"injectToAgent: "backend-runtime-name"injectTemplate: "HUMAN_ANSWER: {{answer.text}}"output: '{"format":"text","path":"answer.text"}'
- name: continue-with-answeragent: backenddependsOn: [ask-human]task: "Continue using this human guidance: {{steps.ask-human.output}}"

askQuestion can use the local Slack API runtime, or Relayfile-backed Slack writebacks when the workflow already has a Relayfile Slack integration.

Interactive agent steps can also opt into marker-driven assistance. With humanAssistance.slack enabled, an agent can print a line beginning with HUMAN_QUESTION:. The runner posts that question to Slack, blocks while waiting for a human reply, then injects HUMAN_ANSWER: ... back into that same agent session. If integrations.relayfile is present, Relayflows automatically uses the existing Relayfile/Pear Slack connection; no Slack bot token, Relayfile workspace id, or Relayfile token is required in the workflow.

Runnable TypeScript proof: examples/typescript/slack-human-assistance-e2e.ts.

swarm:
pattern: daghumanAssistance:
slack:
channel: proj-cloudtimeoutMs: 3600000integrations:
relayfile: {}workflows:
- name: defaultsteps:
- name: implementagent: backendtask: "Proceed, but ask for human guidance if the migration strategy is ambiguous."

Relayfile Event Subscriptions

Relayflows can subscribe to Relayfile integration events and inject matching events into active agents. Workflow-level subscriptions live under integrations.subscriptions; agent-level subscriptions use Workforce-style agents[].watch or Relayflows-style agents[].subscriptions.

integrations:
relayfile: {}subscriptions:
- name: pr-feedbackprovider: githubpath: /github/repos/acme/web/pulls/42/**events: [created, updated]agents: [pr-babysitter]agents:
- name: pr-babysittercli: codexwatch:
- paths: [/github/repos/acme/web/pulls/42/reviews/**]events: [created, updated]workflows:
- name: defaultsteps:
- name: babysit-pragent: pr-babysittertask: | Stay active and wait for INTEGRATION_EVENT messages about PR feedback. Read the Relayfile path from the event, address comments until no open feedback remains, then notify the user in Slack.

Verification Checks

Each step can include a verification check. Verification is one input to the runner's completion decision pipeline — when verification passes, the step completes even without a sentinel marker.

TypeDescription
exit_codeAgent must exit with the specified code (preferred for code-editing steps)
file_existsA file must exist at the specified path after the step
output_containsStep output must contain the specified string (optional accelerator)
customNo-op in the runner; handled by external callers
# Preferred — deterministic verificationverification:
type: exit_codevalue: "0"description: "Process exited successfully"# Also valid — output_contains as an optional acceleratorverification:
type: output_containsvalue: "IMPLEMENTATION_COMPLETE"description: "Agent confirms completion (optional fast-path)"

Completion Decision Pipeline

The runner uses a multi-signal pipeline to decide step completion:

  1. Deterministic verification — if a verification check passes, the step completes immediately (completed_verified)
  2. Owner decision — the step owner can issue OWNER_DECISION: COMPLETE|INCOMPLETE_RETRY|INCOMPLETE_FAIL (completed_by_owner_decision)
  3. Evidence-based completion — channel messages, file artifacts, and exit codes are collected as evidence (completed_by_evidence)
  4. Marker fast-pathSTEP_COMPLETE:<step-name> still works as an accelerator but is never required
Completion StateMeaning
completed_verifiedDeterministic verification passed
completed_by_owner_decisionOwner approved the step
completed_by_evidenceEvidence-based completion
retry_requested_by_ownerOwner requested retry
failed_verificationVerification explicitly failed
failed_owner_decisionOwner rejected the step
failed_no_evidenceNo verification, no owner decision, no evidence

Review parsing is tolerant: The runner accepts semantically equivalent outputs like "Approved", "Complete", "LGTM" — not just exact REVIEW_DECISION: APPROVE strings.

Swarm Patterns

The swarm.pattern field controls how agents are coordinated:

Core Patterns

PatternDescription
dagDirected acyclic graph — steps run based on dependency edges (default)
fan-outAll agents run in parallel
pipelineSequential chaining of steps
hub-spokeCentral hub coordinates spoke agents
consensusAgents vote on decisions
meshFull communication graph between agents
handoffSequential handoff between agents
cascadeWaterfall with phase gates
debateAgents propose and counter-argue
hierarchicalMulti-level reporting structure

Data Processing Patterns

PatternDescription
map-reduceSplit work into chunks (mappers), process in parallel, aggregate results (reducers)
scatter-gatherFan out requests to workers, collect and synthesize responses

Supervision & Quality Patterns

PatternDescription
supervisorMonitor agent monitors workers, restarts on failure, manages health
reflectionAgent produces output, critic reviews and provides feedback for iteration
verifierProducer agents submit work to verifier agents for validation

Adversarial & Validation Patterns

PatternDescription
red-teamAttacker agents probe for weaknesses, defender agents respond
auctionAuctioneer broadcasts tasks, agents bid based on capability/cost

Resilience Patterns

PatternDescription
escalationStart with fast/cheap agents, escalate to more capable on failure
sagaDistributed transactions with compensating actions on failure
circuit-breakerPrimary agent with fallback chain, fail fast and recover

Collaborative Patterns

PatternDescription
blackboardShared workspace where agents contribute incrementally to a solution
swarmEmergent behavior from simple agent rules (neighbor communication)

Auto-Selection by Role

When swarm.pattern is omitted, the coordinator auto-selects based on agent roles. Patterns are checked in priority order below (first match wins):

PriorityPatternRequired Roles/Config
1dagSteps with dependsOn
2consensusUses coordination.consensusStrategy config
3map-reducemapper + reducer
4red-team(attacker OR red-team) + (defender OR blue-team)
5reflectioncritic
6escalationtier-1, tier-2, etc.
7auctionauctioneer
8sagasaga-orchestrator OR compensate-handler
9circuit-breakerfallback, backup, OR primary
10blackboardblackboard OR shared-workspace
11swarmhive-mind OR swarm-agent
12verifierverifier
13supervisorsupervisor
14hierarchicallead (with 4+ agents)
15hub-spokehub OR coordinator
16pipelineUnique agents per step, 3+ steps
17fan-outDefault fallback

Error Handling

Step-Level

steps:
- name: risky-stepagent: workertask: "Do something that might fail"retries: 3# Retry up to 3 times on failuretimeoutMs: 300000# 5 minute timeout

Successful early termination

A deterministic gate can explicitly declare exit codes that mean “there is no work to do.” A matching code ends the run with the distinct completed_early status and skips every step that has not started:

steps:
- name: claim-worktype: deterministiccommand: node bin/claim-work.mjsterminalSuccessExitCodes: [78]
- name: process-claimagent: workertask: Process the claimed workdependsOn: [claim-work]

Terminal-capable gates are scheduling barriers, so other ready work does not race the gate. The triggering step is completed with completion reason completed_early_exit; remaining steps are skipped, and the CLI exits 0 while clearly reporting COMPLETED EARLY. Verification still applies, so a verification failure remains a real failure.

This behavior is opt-in. Without terminalSuccessExitCodes, exit 78 and every other non-zero exit retain their existing failure behavior. The new completed_early run status is an additive public API value: consumers with exhaustive status switches, strict validators, database constraints, or terminal-status polling must handle it separately from completed.

Workflow-Level

The onError field on a workflow controls what happens when a step fails:

ValueBehavior
fail / fail-fastStop immediately, skip downstream steps
skip / continueSkip downstream dependents, continue independent steps
retryRetry the step; deterministic gates ask a workflow agent to repair before each retry when an agent is available

Global

errorHandling:
strategy: retrymaxRetries: 2retryDelayMs: 5000repairAgent: testerrepairRetries: 2notifyChannel: alerts

Retry-mode workflows are repair-aware by default. Deterministic step failures, verification gate failures, and malformed agent artifacts are treated as repairable work before terminal failure. The runner chooses errorHandling.repairAgent when set, otherwise it uses the step's owning/upstream agent when possible, then falls back to the best available workflow agent. The selected agent gets the failed command or agent output, working directory, exit information, and captured evidence, then the failed gate or step is retried. Use repairRetries: 0, strategy: fail-fast, or strategy: continue when a workflow intentionally should not invoke repair agents. Set onExhaustion: needs-human to end an exhausted repairable run as needs_human instead of failed.

Built-in Templates

Six pre-built workflow templates are included:

TemplatePatternDescription
feature-devhub-spokePlan, implement, review, and finalize a feature
bug-fixhub-spokeInvestigate, patch, validate, and document a bug fix
code-reviewfan-outParallel multi-reviewer assessment with consolidated findings
security-auditpipelineScan, triage, remediate, and verify security issues
refactorhierarchicalAnalyze, plan, execute, and validate a refactor
documentationhandoffResearch, draft, review, and publish documentation

Using Templates

import{TemplateRegistry,WorkflowRunner}from"@relayflows/core";constregistry=newTemplateRegistry();// List available templatesconsttemplates=awaitregistry.listTemplates();// Load and run a templateconstconfig=awaitregistry.loadTemplate("feature-dev");construnner=newWorkflowRunner();constresult=awaitrunner.execute(config,undefined,{task: "Add WebSocket support to the API",});// Install a custom template from a URLawaitregistry.installExternalTemplate("https://example.com/my-template.yaml","my-template");

TypeScript Builder API

The builder constructs a RelayYamlConfig object and can run it, export it as YAML, or return the raw config.

import{workflow}from"@relayflows/core";// Build and runconstresult=awaitworkflow("my-workflow").pattern("dag").maxConcurrency(3).timeout(60*60*1000).channel("my-channel").agent("backend",{cli: "claude",role: "Backend engineer",model: "opus",retries: 2,}).agent("frontend",{cli: "codex",role: "Frontend engineer",interactive: false,// Non-interactive subprocess mode}).step("api",{agent: "backend",task: "Build REST API",verification: {type: "output_contains",value: "API_READY"},}).step("ui",{agent: "frontend",task: "Build the UI",dependsOn: ["api"],}).onError("retry",{maxRetries: 2,retryDelayMs: 5000}).run();// Or export to YAMLconstyaml=workflow("my-workflow").pattern("dag").agent("worker",{cli: "claude"}).step("task1",{agent: "worker",task: "Do something"}).toYaml();// Or get the raw config objectconstconfig=workflow("my-workflow").pattern("dag").agent("worker",{cli: "claude"}).step("task1",{agent: "worker",task: "Do something"}).toConfig();

Python Builder API

The Python builder ships with @agent-relay/sdk-py:

pip install agent-relay
fromagent_relayimportworkflow, run_yaml# Build and runresult= (
workflow("my-workflow")
.pattern("dag")
.max_concurrency(3)
.timeout(3600000)
.agent("backend", cli="claude", role="Backend engineer")
.agent("frontend", cli="codex", role="Frontend engineer")
.step("api", agent="backend", task="Build REST API")
.step("ui", agent="frontend", task="Build the UI", depends_on=["api"])
.on_error("retry", max_retries=2, retry_delay_ms=5000)
.run()
)
# Run an existing YAML fileresult=run_yaml("workflows/my-workflow.yaml")
# Export to YAML stringyaml_str= (
workflow("my-workflow")
.pattern("dag")
.agent("worker", cli="claude")
.step("task1", agent="worker", task="Do something")
.to_yaml()
)
# Get the raw config dictconfig= (
workflow("my-workflow")
.pattern("dag")
.agent("worker", cli="claude")
.step("task1", agent="worker", task="Do something")
.to_config()
)

Programmatic API

For full control, use the WorkflowRunner directly:

import{WorkflowRunner}from"@relayflows/core";construnner=newWorkflowRunner({cwd: "/path/to/project",// Working directory (default: process.cwd())relay: {port: 3000},// AgentRelay options (optional)});// Listen to events (broker:event fires frequently — filter it out for cleaner output)runner.on((event)=>{if(event.type==='broker:event')return;console.log(event.type,event);});// Parse and executeconstconfig=awaitrunner.parseYamlFile("workflow.yaml");construn=awaitrunner.execute(config,"workflow-name",{task: "Build the feature",});// Pause / resume / abortrunner.pause();runner.unpause();runner.abort();// Resume a failed runconstresumed=awaitrunner.resume(run.id);

Zero-Config Convenience Function

import{runWorkflow}from"@relayflows/core";constresult=awaitrunWorkflow("workflow.yaml",{workflow: "deploy",vars: {environment: "staging"},onEvent: (event)=>{if(event.type!=='broker:event')console.log(event.type);},});

Coordination

Barriers

Synchronization points that wait for specific steps to complete:

coordination:
barriers:
- name: all-reviews-donewaitFor: [review-arch, review-security, review-correctness]timeoutMs: 900000consensusStrategy: majority # majority | unanimous | quorum

Shared State

Agents can share state during execution:

state:
backend: memory # memory | redis | databasettlMs: 86400000namespace: my-workflow

Supported Agent CLIs

CLIDescription
claudeClaude Code (Anthropic)
codexCodex CLI (OpenAI)
geminiGemini CLI (Google)
aiderAider coding assistant
gooseGoose AI assistant
opencodeOpenCode CLI
droidDroid CLI

Non-Interactive Agents

By default, agents run in interactive PTY mode with full relay messaging. For workers that just need to execute a task and return output — common in fan-out, map-reduce, and pipeline patterns — set interactive: false to run them as lightweight subprocesses.

YAML

agents:
- name: leadcli: clauderole: "Coordinates work"# interactive: true (default) — full PTY, relay messaging, /exit detection
- name: workercli: codexrole: "Executes tasks"interactive: false # Runs "codex exec <task>", captures stdout

TypeScript

workflow("fan-out-analysis").pattern("fan-out").agent("lead",{cli: "claude",role: "Coordinator"}).agent("worker-1",{cli: "codex",interactive: false,role: "Analyst"}).agent("worker-2",{cli: "codex",interactive: false,role: "Analyst"}).step("analyze-1",{agent: "worker-1",task: "Analyze module A"}).step("analyze-2",{agent: "worker-2",task: "Analyze module B"}).step("synthesize",{agent: "lead",task: "Combine: {{steps.analyze-1.output}} + {{steps.analyze-2.output}}",dependsOn: ["analyze-1","analyze-2"],}).run();

How It Works

AspectInteractive (default)Non-Interactive
ExecutionFull PTY with stdin/stdoutchild_process.spawn() with piped stdio
CLI invocationStandard interactive sessionOne-shot mode (claude -p, codex exec, etc.)
Relay messagingCan send/receive messagesNo messaging — excluded from topology edges
Self-terminationMust output /exitProcess exits naturally when done
Output capturePTY output bufferstdout capture
OverheadHigher (PTY, echo verification, SIGWINCH)Lower (simple subprocess)

Non-Interactive CLI Commands

CLICommandNotes
claudeclaude -p "<task>"Print mode, exits after response
codexcodex exec "<task>"One-shot execution
geminigemini -p "<task>"Prompt mode
opencodeopencode --prompt "<task>"One-shot prompt
droiddroid exec "<task>"One-shot execution
aideraider --message "<task>" --yes-always --no-gitAuto-approve, skip git
goosegoose run --text "<task>" --no-sessionText mode, no session file

When to Use

  • Fan-out workers that process a task and return results
  • Map-reduce mappers that don't need mid-task communication
  • Pipeline stages that transform input to output
  • Any agent that doesn't need turn-by-turn relay messaging

When NOT to Use

  • Lead/coordinator agents that communicate with others
  • Agents in debate, consensus, or reflection patterns
  • Agents that need to receive messages during execution

Agent Slash Commands

Agents running inside a workflow can output slash commands to signal the broker. These are detected in the agent's PTY output at the broker level — the agent simply prints the command on its own line.

/exit

Signals that the agent has completed its current step and is ready to be released.

/exit

The workflow runner waits for each agent to /exit after delivering a step task. When the broker detects /exit in the agent's output (exact line match after ANSI stripping), it:

  1. Emits an agent_exit frame with reason: "agent_requested"
  2. Triggers graceful PTY shutdown

If an agent does not /exit within the step's timeoutMs, the runner treats the step as timed out. As a safety net, steps with file_exists verification will still pass if the expected file is present despite the timeout.

Best practice: Instruct agents to output /exit when done in your step task descriptions:

steps:
- name: build-apiagent: backendtask: | Build the REST API endpoints for user management. When finished, output /exit.

Idle Agent Detection and Nudging

Interactive agents sometimes finish their task but forget to /exit, sitting idle and blocking downstream steps. The runner can detect idle agents and take action automatically.

Configuration

Add idleNudge to your swarm config:

swarm:
pattern: hub-spokeidleNudge:
nudgeAfterMs: 120000# 2 min before first nudge (default)escalateAfterMs: 120000# 2 min after nudge before force-release (default)maxNudges: 1# Nudges before escalation (default)

All built-in templates include idle nudging with these defaults.

How It Works

  1. Detection: The broker tracks agent output timestamps and emits agent_idle events when an agent goes silent for the configured threshold
  2. Nudge: For hub patterns (hub-spoke, fan-out, hierarchical, etc.), the runner tells the hub agent to check on the idle agent. For non-hub patterns, a system message is injected directly into the agent's PTY
  3. Escalation: If the agent remains idle after maxNudges attempts, the runner force-releases it and captures whatever output was produced
  4. No config: When idleNudge is omitted, the runner uses simple waitForExit (backward compatible)

Events

The runner emits two new events for idle nudging:

EventDescription
step:nudgedFired when a nudge message is sent to an idle agent
step:force-releasedFired when an agent is force-released after exhausting nudges

Automatic Step Owner and Review

For interactive agent steps, the runner uses a point-person-led completion model:

  1. Elects a step owner (prefers lead/coordinator-style agents, falls back to the step agent)
  2. Runs a completion decision pipeline — checks deterministic verification first, then owner judgment, then evidence
  3. Owner can issue structured decisions via OWNER_DECISION: COMPLETE|INCOMPLETE_RETRY|INCOMPLETE_FAIL|NEEDS_CLARIFICATION with optional REASON: <text>
  4. Review parsing is tolerant — accepts "Approved", "Complete", "LGTM", not just exact REVIEW_DECISION: APPROVE
  5. Markers are optional acceleratorsSTEP_COMPLETE:<step-name> still works as a fast-path but is never required
  6. Stores primary output plus review output in the step artifact

Evidence-based completion: The runner collects channel messages, file artifacts, process exit codes, and coordination signals (e.g., WORKER_DONE posted in channel) as completion evidence. When sufficient evidence exists, the step completes without requiring any sentinel marker.

Deterministic and worktree steps are unchanged and do not require owner/review delegation.

Sandbox Execution

By default the runner spawns steps as local child processes. To run them in isolated sandboxes instead, select a provider — the runner still owns command construction, env, cwd, timeout, and the whole DAG/retry/verification pipeline; the provider only supplies "where the command runs".

# Off by default. Unset the flag to get local child processes back.export RELAYFLOWS_SANDBOX_PROVIDER=daytona
export DAYTONA_API_KEY=...
export RELAYFLOWS_SANDBOX_HOME_DIR=/home/daytona # image-specific, requiredexport RELAYFLOWS_SANDBOX_SNAPSHOT=my-snapshot # optional

Or in code:

import{WorkflowRunner}from"@relayflows/core";construnner=newWorkflowRunner({sandbox: {provider: "daytona",homeDir: "/home/daytona"},});
ProviderWhat it gives you
none (default)No sandbox. Local child processes, exactly as before.
daytonaReal remote sandboxes via @agent-relay/sandbox. Needs the optional peer @daytonaio/sdk.
local-processReal local processes in a private per-step directory with its own HOME. Isolates the filesystem root, not the machine — good for development and CI, not a security boundary.

Reversibility.provider: "none" (or an unset RELAYFLOWS_SANDBOX_PROVIDER) produces no backend at all, so nothing about the default path changes. An explicit executor or processBackend still wins over sandbox config, so a host that injects its own backend today keeps it.

Custom providers. Register a runtime under any name, or hand one in directly. This is the seam a host uses to plug in a runtime that does not live in this repo:

import{registerSandboxProvider,WorkflowRunner}from"@relayflows/core";registerSandboxProvider("my-runtime",(config)=>newMyRuntime(config));// ...or skip the registry entirely:newWorkflowRunner({sandbox: {runtime: myRuntime}});

A runtime needs five methods — launch, exec, uploadFile, getHomeDir, destroy — matching @agent-relay/sandbox's WorkflowRuntime.

Schema Validation

A JSON Schema is available at packages/core/src/schema.json for editor autocompletion and validation of relay.yaml files.

Development

npm install
npm run typecheck
npm run test

Requirements

  • Node.js 22+
  • @relayflows/cli installed (npm install -g @relayflows/cli)
  • For Python: Python 3.10+ with pip install agent-relay
  • For TypeScript workflow files: tsx or ts-node installed

License

Apache-2.0 — Copyright 2025 Agent Workforce Incorporated

About

Orchestrate multi-step, multi-agent execution across Agent Relay workers

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

relayflows

npmLicense

Workflow engine and CLI for Agent Relay. Orchestrate multi-agent workflows using YAML, TypeScript, or Python. Define agents, wire up dependencies, and let the runner handle execution, retries, and verification.

Quick Start

CLI

# Run a YAML workflow
relayflows run workflow.yaml
# Run a TypeScript workflow
relayflows run workflow.ts
# Run a Python workflow
relayflows run workflow.py
# Run a specific named workflow from a file
relayflows run workflow.yaml --workflow deploy

TypeScript

import{workflow}from"@relayflows/core";constresult=awaitworkflow("ship-feature").pattern("dag").agent("planner",{cli: "claude",role: "Plans implementation"}).agent("developer",{cli: "codex",role: "Writes code"}).agent("reviewer",{cli: "claude",role: "Reviews code"}).step("plan",{agent: "planner",task: "Create implementation plan for user authentication",}).step("implement",{agent: "developer",task: "Implement the plan",dependsOn: ["plan"],}).step("review",{agent: "reviewer",task: "Review the implementation",dependsOn: ["implement"],}).run();console.log(result.status);// "completed" | "completed_early" | "failed" | "cancelled" | "needs_human"

Python

fromagent_relayimportworkflowresult= (
workflow("ship-feature")
.pattern("dag")
.agent("planner", cli="claude", role="Plans implementation")
.agent("developer", cli="codex", role="Writes code")
.agent("reviewer", cli="claude", role="Reviews code")
.step("plan", agent="planner", task="Create implementation plan for user auth")
.step("implement", agent="developer", task="Implement the plan", depends_on=["plan"])
.step("review", agent="reviewer", task="Review the implementation", depends_on=["implement"])
.run()
)

Watching a Run

When a run starts, the runner mints and prints a link you can open to follow it live:

[workflow 00:02] Workspace created for this workflow.
[workflow 00:02] Observer: https://agentrelay.com/observer?key=ot_live_...
[workflow 00:02] Channel: wf-ship-feature-a1b2c3

Open the Observer: URL and you see messages, agent activity, handoffs, and deliveries in real time. The link carries a scoped observer token (ot_live_): read-only, expiring in 24 hours, and individually revocable.

Minting is best-effort, so a link is not guaranteed — see If no link appears. A failed mint never fails the run.

Treat the link itself as a shared secret. The token is a bearer credential in a query parameter, so anyone who gets the URL can read the stream it covers until it expires or you revoke it. It is far safer than a workspace key — it cannot send, spawn, or administer — but it is not public.

The two ways a run gets a workspace

Which link you get depends on where the run's Relaycast workspace comes from.

No RELAY_API_KEY set — the runner creates a throwaway workspace for this run alone and mints an observer link covering all of it, DMs included. The workspace is anonymous and disappears from your reach when the run ends, so copy the link while the run is going. Nothing persists the underlying key, and there is no way to recover it afterward.

RELAY_API_KEY set — the runner uses your workspace and mints a link scoped to just this run's channel, with agent DMs excluded, so the link does not expose unrelated traffic in a shared workspace. This is the better setup for anything you may want to revisit: the run is in a workspace you own, so you can mint fresh links whenever you like.

# One-time: create a workspace you own and keep the key
curl -sX POST https://api.relaycast.dev/v1/workspaces \
-H 'content-type: application/json' -d '{"name":"my-workflows"}' \
| jq -r '.data.api_key // .api_key'export RELAY_API_KEY=rk_live_... # put this in your shell profile
relayflows run workflow.yaml

If you already use the Agent Relay CLI, agent-relay workspace key --reveal-secrets prints the key of your active workspace. Note it is masked without --reveal-secrets.

Minting more links yourself

With a workspace you own, agent-relay observer mints links on demand:

agent-relay observer # read-only link, 24h, DMs excluded
agent-relay observer --channels wf-ship-a1b2c3 # scope to one run
agent-relay observer --include-dms # include agent DMs
agent-relay observer --expires 7d # longer-lived link
agent-relay observer list # what is outstanding
agent-relay observer revoke <id># cut one off immediately

Never share the workspace key

A workspace key (rk_live_) is an administrative credential — it can send messages, spawn and remove agents, and change workspace settings. Do not put one in an observer URL, a chat message, or a terminal transcript; query strings end up in browser history, referrer headers, and proxy logs. The realtime endpoint rejects it outright, so a link built from one cannot stream anyway.

Workspace key (rk_live_)Observer token (ot_live_)
Read messages and activityyesyes
Send, spawn agents, administeryesno
Expiresnoyes
Revocable individuallynoyes
Scopable to channelsnoyes

The runner only ever prints ot_live_ links, and scrubs rk_live_ values out of channel output.

Configuration

VariablePurpose
RELAY_API_KEYWorkspace key to run against. Unset means a throwaway workspace per run.
RELAY_OBSERVER_URLObserver dashboard base. Defaults to https://agentrelay.com/observer.
RELAY_OBSERVER_EXPIRESLink lifetime as 30m / 24h / 7d. Defaults to 24h; unparseable or over 90d falls back to 24h.
RELAYCAST_BASE_URLRelaycast engine base. Defaults to https://api.relaycast.dev.

If no link appears

  • Observation: unavailable — the token could not be minted (engine unreachable, or it rejected the request). The run is unaffected; minting is best-effort by design and never fails a run.
  • No observer lines at all — the run needed no broker: every step was deterministic, worktree, integration, or waitFor, or an external executor handled agent spawning, or Relaycast was disabled with AGENT_RELAY_WORKFLOW_DISABLE_RELAYCAST=1.
  • Observation: run agent-relay observer — you set RELAY_API_KEY and minting failed. Mint a link by hand with agent-relay observer.

Consumer-Facing Apps + AI SDK Communicate Flows

A good production split is:

  1. AI SDK app handles the user conversation and streaming UI
  2. Communicate / onRelay() lets that point-person coordinate with specialists over Relay
  3. Workflows / runWorkflow() take over when a request needs multi-step execution, verification, or handoffs
import{streamText,wrapLanguageModel}from'ai';import{openai}from'@ai-sdk/openai';import{Relay}from'@agent-relay/sdk/communicate';import{onRelay}from'@agent-relay/sdk/communicate/adapters/ai-sdk';import{runWorkflow}from'@relayflows/core';exportasyncfunctionPOST(req: Request){const{ prompt, escalate, repo }=awaitreq.json();constrelay=newRelay('AppLead');constrelaySession=onRelay({name: 'AppLead',instructions: 'You are the customer-facing lead. Keep the user updated and delegate implementation via Relay when needed.',},relay);constmodel=wrapLanguageModel({model: openai('gpt-4o-mini'),middleware: relaySession.middleware,});if(escalate){constworkflow=awaitrunWorkflow('workflows/feature-dev.yaml',{vars: {task: prompt, repo },});returnResponse.json({status: workflow.status,runId: workflow.runId});}returnstreamText({
model,tools: relaySession.tools,system: 'Answer directly when possible; coordinate internally when the task needs specialists.',
prompt,}).toUIMessageStreamResponse({onFinish(){relaySession.cleanup();voidrelay.close();},});}

That pattern keeps the user experience snappy while still letting longer Relay workflows run with proper ownership, retries, and verification.

YAML Format

Workflows are defined as relay.yaml files:

version: "1.0"name: my-workflowdescription: "Optional description"swarm:
pattern: dag # Execution pattern (see Patterns below)maxConcurrency: 3# Max agents running in paralleltimeoutMs: 3600000# Global timeout (1 hour)channel: my-channel # Relay channel for agent communicationagents:
- name: backendcli: claude # claude | codex | gemini | aider | goose | opencode | droidrole: "Backend engineer"constraints:
model: opustimeoutMs: 600000retries: 2
- name: testercli: codexrole: "Test engineer"interactive: false # Non-interactive: runs as subprocess, no PTY/messaging# A persona replaces cli + role. Its harness, model, standing instructions,# installed skills, MCP servers, and harness settings come from the spec.# Persona agents are interactive-only: do not set cli, preset, or# constraints.model, and do not use interactive: false.
- name: integrationspersona: nango-integrationsworkflows:
- name: build-and-testonError: retry # fail | skip | retrysteps:
- name: build-apiagent: backendtask: "Build the REST API endpoints for user management"verification:
type: file_existsvalue: "src/api/users.ts"retries: 1
- name: write-testsagent: testertask: "Write integration tests for: {{steps.build-api.output}}"dependsOn: [build-api]
- name: run-testsagent: testertask: "Run the test suite and report results"dependsOn: [write-tests]verification:
type: exit_codevalue: "0"errorHandling:
strategy: retrymaxRetries: 2retryDelayMs: 5000repairAgent: testerrepairRetries: 2onExhaustion: needs-humannotifyChannel: my-channel

Template Variables

Use {{variable}} for user-provided values and {{steps.STEP_NAME.output}} for previous step outputs:

steps:
- name: planagent: plannertask: "Plan implementation for: {{task}}"# User variable
- name: implementagent: developerdependsOn: [plan]task: "Implement: {{steps.plan.output}}"# Previous step output

User variables are passed via the CLI or programmatically:

awaitrunWorkflow("workflow.yaml",{vars: {task: "Add OAuth2 support"},});

Blocking Slack Questions

Workflows can pause on a Slack question by adding a Slack integration step with action: askQuestion. The step posts a question, waits for the first human reply in the message thread, exposes that answer through {{steps.<name>.output}}, and can inject it into a running agent when injectToAgent is set.

workflows:
- name: defaultsteps:
- name: ask-humantype: integrationintegration: slackaction: askQuestionparams:
channel: "#engineering"text: "The implementer is blocked on migration strategy. Which path should it take?"waitTimeoutMs: "3600000"injectToAgent: "backend-runtime-name"injectTemplate: "HUMAN_ANSWER: {{answer.text}}"output: '{"format":"text","path":"answer.text"}'
- name: continue-with-answeragent: backenddependsOn: [ask-human]task: "Continue using this human guidance: {{steps.ask-human.output}}"

askQuestion can use the local Slack API runtime, or Relayfile-backed Slack writebacks when the workflow already has a Relayfile Slack integration.

Interactive agent steps can also opt into marker-driven assistance. With humanAssistance.slack enabled, an agent can print a line beginning with HUMAN_QUESTION:. The runner posts that question to Slack, blocks while waiting for a human reply, then injects HUMAN_ANSWER: ... back into that same agent session. If integrations.relayfile is present, Relayflows automatically uses the existing Relayfile/Pear Slack connection; no Slack bot token, Relayfile workspace id, or Relayfile token is required in the workflow.

Runnable TypeScript proof: examples/typescript/slack-human-assistance-e2e.ts.

swarm:
pattern: daghumanAssistance:
slack:
channel: proj-cloudtimeoutMs: 3600000integrations:
relayfile: {}workflows:
- name: defaultsteps:
- name: implementagent: backendtask: "Proceed, but ask for human guidance if the migration strategy is ambiguous."

Relayfile Event Subscriptions

Relayflows can subscribe to Relayfile integration events and inject matching events into active agents. Workflow-level subscriptions live under integrations.subscriptions; agent-level subscriptions use Workforce-style agents[].watch or Relayflows-style agents[].subscriptions.

integrations:
relayfile: {}subscriptions:
- name: pr-feedbackprovider: githubpath: /github/repos/acme/web/pulls/42/**events: [created, updated]agents: [pr-babysitter]agents:
- name: pr-babysittercli: codexwatch:
- paths: [/github/repos/acme/web/pulls/42/reviews/**]events: [created, updated]workflows:
- name: defaultsteps:
- name: babysit-pragent: pr-babysittertask: | Stay active and wait for INTEGRATION_EVENT messages about PR feedback. Read the Relayfile path from the event, address comments until no open feedback remains, then notify the user in Slack.

Verification Checks

Each step can include a verification check. Verification is one input to the runner's completion decision pipeline — when verification passes, the step completes even without a sentinel marker.

TypeDescription
exit_codeAgent must exit with the specified code (preferred for code-editing steps)
file_existsA file must exist at the specified path after the step
output_containsStep output must contain the specified string (optional accelerator)
customNo-op in the runner; handled by external callers
# Preferred — deterministic verificationverification:
type: exit_codevalue: "0"description: "Process exited successfully"# Also valid — output_contains as an optional acceleratorverification:
type: output_containsvalue: "IMPLEMENTATION_COMPLETE"description: "Agent confirms completion (optional fast-path)"

Completion Decision Pipeline

The runner uses a multi-signal pipeline to decide step completion:

  1. Deterministic verification — if a verification check passes, the step completes immediately (completed_verified)
  2. Owner decision — the step owner can issue OWNER_DECISION: COMPLETE|INCOMPLETE_RETRY|INCOMPLETE_FAIL (completed_by_owner_decision)
  3. Evidence-based completion — channel messages, file artifacts, and exit codes are collected as evidence (completed_by_evidence)
  4. Marker fast-pathSTEP_COMPLETE:<step-name> still works as an accelerator but is never required
Completion StateMeaning
completed_verifiedDeterministic verification passed
completed_by_owner_decisionOwner approved the step
completed_by_evidenceEvidence-based completion
retry_requested_by_ownerOwner requested retry
failed_verificationVerification explicitly failed
failed_owner_decisionOwner rejected the step
failed_no_evidenceNo verification, no owner decision, no evidence

Review parsing is tolerant: The runner accepts semantically equivalent outputs like "Approved", "Complete", "LGTM" — not just exact REVIEW_DECISION: APPROVE strings.

Swarm Patterns

The swarm.pattern field controls how agents are coordinated:

Core Patterns

PatternDescription
dagDirected acyclic graph — steps run based on dependency edges (default)
fan-outAll agents run in parallel
pipelineSequential chaining of steps
hub-spokeCentral hub coordinates spoke agents
consensusAgents vote on decisions
meshFull communication graph between agents
handoffSequential handoff between agents
cascadeWaterfall with phase gates
debateAgents propose and counter-argue
hierarchicalMulti-level reporting structure

Data Processing Patterns

PatternDescription
map-reduceSplit work into chunks (mappers), process in parallel, aggregate results (reducers)
scatter-gatherFan out requests to workers, collect and synthesize responses

Supervision & Quality Patterns

PatternDescription
supervisorMonitor agent monitors workers, restarts on failure, manages health
reflectionAgent produces output, critic reviews and provides feedback for iteration
verifierProducer agents submit work to verifier agents for validation

Adversarial & Validation Patterns

PatternDescription
red-teamAttacker agents probe for weaknesses, defender agents respond
auctionAuctioneer broadcasts tasks, agents bid based on capability/cost

Resilience Patterns

PatternDescription
escalationStart with fast/cheap agents, escalate to more capable on failure
sagaDistributed transactions with compensating actions on failure
circuit-breakerPrimary agent with fallback chain, fail fast and recover

Collaborative Patterns

PatternDescription
blackboardShared workspace where agents contribute incrementally to a solution
swarmEmergent behavior from simple agent rules (neighbor communication)

Auto-Selection by Role

When swarm.pattern is omitted, the coordinator auto-selects based on agent roles. Patterns are checked in priority order below (first match wins):

PriorityPatternRequired Roles/Config
1dagSteps with dependsOn
2consensusUses coordination.consensusStrategy config
3map-reducemapper + reducer
4red-team(attacker OR red-team) + (defender OR blue-team)
5reflectioncritic
6escalationtier-1, tier-2, etc.
7auctionauctioneer
8sagasaga-orchestrator OR compensate-handler
9circuit-breakerfallback, backup, OR primary
10blackboardblackboard OR shared-workspace
11swarmhive-mind OR swarm-agent
12verifierverifier
13supervisorsupervisor
14hierarchicallead (with 4+ agents)
15hub-spokehub OR coordinator
16pipelineUnique agents per step, 3+ steps
17fan-outDefault fallback

Error Handling

Step-Level

steps:
- name: risky-stepagent: workertask: "Do something that might fail"retries: 3# Retry up to 3 times on failuretimeoutMs: 300000# 5 minute timeout

Successful early termination

A deterministic gate can explicitly declare exit codes that mean “there is no work to do.” A matching code ends the run with the distinct completed_early status and skips every step that has not started:

steps:
- name: claim-worktype: deterministiccommand: node bin/claim-work.mjsterminalSuccessExitCodes: [78]
- name: process-claimagent: workertask: Process the claimed workdependsOn: [claim-work]

Terminal-capable gates are scheduling barriers, so other ready work does not race the gate. The triggering step is completed with completion reason completed_early_exit; remaining steps are skipped, and the CLI exits 0 while clearly reporting COMPLETED EARLY. Verification still applies, so a verification failure remains a real failure.

This behavior is opt-in. Without terminalSuccessExitCodes, exit 78 and every other non-zero exit retain their existing failure behavior. The new completed_early run status is an additive public API value: consumers with exhaustive status switches, strict validators, database constraints, or terminal-status polling must handle it separately from completed.

Workflow-Level

The onError field on a workflow controls what happens when a step fails:

ValueBehavior
fail / fail-fastStop immediately, skip downstream steps
skip / continueSkip downstream dependents, continue independent steps
retryRetry the step; deterministic gates ask a workflow agent to repair before each retry when an agent is available

Global

errorHandling:
strategy: retrymaxRetries: 2retryDelayMs: 5000repairAgent: testerrepairRetries: 2notifyChannel: alerts

Retry-mode workflows are repair-aware by default. Deterministic step failures, verification gate failures, and malformed agent artifacts are treated as repairable work before terminal failure. The runner chooses errorHandling.repairAgent when set, otherwise it uses the step's owning/upstream agent when possible, then falls back to the best available workflow agent. The selected agent gets the failed command or agent output, working directory, exit information, and captured evidence, then the failed gate or step is retried. Use repairRetries: 0, strategy: fail-fast, or strategy: continue when a workflow intentionally should not invoke repair agents. Set onExhaustion: needs-human to end an exhausted repairable run as needs_human instead of failed.

Built-in Templates

Six pre-built workflow templates are included:

TemplatePatternDescription
feature-devhub-spokePlan, implement, review, and finalize a feature
bug-fixhub-spokeInvestigate, patch, validate, and document a bug fix
code-reviewfan-outParallel multi-reviewer assessment with consolidated findings
security-auditpipelineScan, triage, remediate, and verify security issues
refactorhierarchicalAnalyze, plan, execute, and validate a refactor
documentationhandoffResearch, draft, review, and publish documentation

Using Templates

import{TemplateRegistry,WorkflowRunner}from"@relayflows/core";constregistry=newTemplateRegistry();// List available templatesconsttemplates=awaitregistry.listTemplates();// Load and run a templateconstconfig=awaitregistry.loadTemplate("feature-dev");construnner=newWorkflowRunner();constresult=awaitrunner.execute(config,undefined,{task: "Add WebSocket support to the API",});// Install a custom template from a URLawaitregistry.installExternalTemplate("https://example.com/my-template.yaml","my-template");

TypeScript Builder API

The builder constructs a RelayYamlConfig object and can run it, export it as YAML, or return the raw config.

import{workflow}from"@relayflows/core";// Build and runconstresult=awaitworkflow("my-workflow").pattern("dag").maxConcurrency(3).timeout(60*60*1000).channel("my-channel").agent("backend",{cli: "claude",role: "Backend engineer",model: "opus",retries: 2,}).agent("frontend",{cli: "codex",role: "Frontend engineer",interactive: false,// Non-interactive subprocess mode}).step("api",{agent: "backend",task: "Build REST API",verification: {type: "output_contains",value: "API_READY"},}).step("ui",{agent: "frontend",task: "Build the UI",dependsOn: ["api"],}).onError("retry",{maxRetries: 2,retryDelayMs: 5000}).run();// Or export to YAMLconstyaml=workflow("my-workflow").pattern("dag").agent("worker",{cli: "claude"}).step("task1",{agent: "worker",task: "Do something"}).toYaml();// Or get the raw config objectconstconfig=workflow("my-workflow").pattern("dag").agent("worker",{cli: "claude"}).step("task1",{agent: "worker",task: "Do something"}).toConfig();

Python Builder API

The Python builder ships with @agent-relay/sdk-py:

pip install agent-relay
fromagent_relayimportworkflow, run_yaml# Build and runresult= (
workflow("my-workflow")
.pattern("dag")
.max_concurrency(3)
.timeout(3600000)
.agent("backend", cli="claude", role="Backend engineer")
.agent("frontend", cli="codex", role="Frontend engineer")
.step("api", agent="backend", task="Build REST API")
.step("ui", agent="frontend", task="Build the UI", depends_on=["api"])
.on_error("retry", max_retries=2, retry_delay_ms=5000)
.run()
)
# Run an existing YAML fileresult=run_yaml("workflows/my-workflow.yaml")
# Export to YAML stringyaml_str= (
workflow("my-workflow")
.pattern("dag")
.agent("worker", cli="claude")
.step("task1", agent="worker", task="Do something")
.to_yaml()
)
# Get the raw config dictconfig= (
workflow("my-workflow")
.pattern("dag")
.agent("worker", cli="claude")
.step("task1", agent="worker", task="Do something")
.to_config()
)

Programmatic API

For full control, use the WorkflowRunner directly:

import{WorkflowRunner}from"@relayflows/core";construnner=newWorkflowRunner({cwd: "/path/to/project",// Working directory (default: process.cwd())relay: {port: 3000},// AgentRelay options (optional)});// Listen to events (broker:event fires frequently — filter it out for cleaner output)runner.on((event)=>{if(event.type==='broker:event')return;console.log(event.type,event);});// Parse and executeconstconfig=awaitrunner.parseYamlFile("workflow.yaml");construn=awaitrunner.execute(config,"workflow-name",{task: "Build the feature",});// Pause / resume / abortrunner.pause();runner.unpause();runner.abort();// Resume a failed runconstresumed=awaitrunner.resume(run.id);

Zero-Config Convenience Function

import{runWorkflow}from"@relayflows/core";constresult=awaitrunWorkflow("workflow.yaml",{workflow: "deploy",vars: {environment: "staging"},onEvent: (event)=>{if(event.type!=='broker:event')console.log(event.type);},});

Coordination

Barriers

Synchronization points that wait for specific steps to complete:

coordination:
barriers:
- name: all-reviews-donewaitFor: [review-arch, review-security, review-correctness]timeoutMs: 900000consensusStrategy: majority # majority | unanimous | quorum

Shared State

Agents can share state during execution:

state:
backend: memory # memory | redis | databasettlMs: 86400000namespace: my-workflow

Supported Agent CLIs

CLIDescription
claudeClaude Code (Anthropic)
codexCodex CLI (OpenAI)
geminiGemini CLI (Google)
aiderAider coding assistant
gooseGoose AI assistant
opencodeOpenCode CLI
droidDroid CLI

Non-Interactive Agents

By default, agents run in interactive PTY mode with full relay messaging. For workers that just need to execute a task and return output — common in fan-out, map-reduce, and pipeline patterns — set interactive: false to run them as lightweight subprocesses.

YAML

agents:
- name: leadcli: clauderole: "Coordinates work"# interactive: true (default) — full PTY, relay messaging, /exit detection
- name: workercli: codexrole: "Executes tasks"interactive: false # Runs "codex exec <task>", captures stdout

TypeScript

workflow("fan-out-analysis").pattern("fan-out").agent("lead",{cli: "claude",role: "Coordinator"}).agent("worker-1",{cli: "codex",interactive: false,role: "Analyst"}).agent("worker-2",{cli: "codex",interactive: false,role: "Analyst"}).step("analyze-1",{agent: "worker-1",task: "Analyze module A"}).step("analyze-2",{agent: "worker-2",task: "Analyze module B"}).step("synthesize",{agent: "lead",task: "Combine: {{steps.analyze-1.output}} + {{steps.analyze-2.output}}",dependsOn: ["analyze-1","analyze-2"],}).run();

How It Works

AspectInteractive (default)Non-Interactive
ExecutionFull PTY with stdin/stdoutchild_process.spawn() with piped stdio
CLI invocationStandard interactive sessionOne-shot mode (claude -p, codex exec, etc.)
Relay messagingCan send/receive messagesNo messaging — excluded from topology edges
Self-terminationMust output /exitProcess exits naturally when done
Output capturePTY output bufferstdout capture
OverheadHigher (PTY, echo verification, SIGWINCH)Lower (simple subprocess)

Non-Interactive CLI Commands

CLICommandNotes
claudeclaude -p "<task>"Print mode, exits after response
codexcodex exec "<task>"One-shot execution
geminigemini -p "<task>"Prompt mode
opencodeopencode --prompt "<task>"One-shot prompt
droiddroid exec "<task>"One-shot execution
aideraider --message "<task>" --yes-always --no-gitAuto-approve, skip git
goosegoose run --text "<task>" --no-sessionText mode, no session file

When to Use

  • Fan-out workers that process a task and return results
  • Map-reduce mappers that don't need mid-task communication
  • Pipeline stages that transform input to output
  • Any agent that doesn't need turn-by-turn relay messaging

When NOT to Use

  • Lead/coordinator agents that communicate with others
  • Agents in debate, consensus, or reflection patterns
  • Agents that need to receive messages during execution

Agent Slash Commands

Agents running inside a workflow can output slash commands to signal the broker. These are detected in the agent's PTY output at the broker level — the agent simply prints the command on its own line.

/exit

Signals that the agent has completed its current step and is ready to be released.

/exit

The workflow runner waits for each agent to /exit after delivering a step task. When the broker detects /exit in the agent's output (exact line match after ANSI stripping), it:

  1. Emits an agent_exit frame with reason: "agent_requested"
  2. Triggers graceful PTY shutdown

If an agent does not /exit within the step's timeoutMs, the runner treats the step as timed out. As a safety net, steps with file_exists verification will still pass if the expected file is present despite the timeout.

Best practice: Instruct agents to output /exit when done in your step task descriptions:

steps:
- name: build-apiagent: backendtask: | Build the REST API endpoints for user management. When finished, output /exit.

Idle Agent Detection and Nudging

Interactive agents sometimes finish their task but forget to /exit, sitting idle and blocking downstream steps. The runner can detect idle agents and take action automatically.

Configuration

Add idleNudge to your swarm config:

swarm:
pattern: hub-spokeidleNudge:
nudgeAfterMs: 120000# 2 min before first nudge (default)escalateAfterMs: 120000# 2 min after nudge before force-release (default)maxNudges: 1# Nudges before escalation (default)

All built-in templates include idle nudging with these defaults.

How It Works

  1. Detection: The broker tracks agent output timestamps and emits agent_idle events when an agent goes silent for the configured threshold
  2. Nudge: For hub patterns (hub-spoke, fan-out, hierarchical, etc.), the runner tells the hub agent to check on the idle agent. For non-hub patterns, a system message is injected directly into the agent's PTY
  3. Escalation: If the agent remains idle after maxNudges attempts, the runner force-releases it and captures whatever output was produced
  4. No config: When idleNudge is omitted, the runner uses simple waitForExit (backward compatible)

Events

The runner emits two new events for idle nudging:

EventDescription
step:nudgedFired when a nudge message is sent to an idle agent
step:force-releasedFired when an agent is force-released after exhausting nudges

Automatic Step Owner and Review

For interactive agent steps, the runner uses a point-person-led completion model:

  1. Elects a step owner (prefers lead/coordinator-style agents, falls back to the step agent)
  2. Runs a completion decision pipeline — checks deterministic verification first, then owner judgment, then evidence
  3. Owner can issue structured decisions via OWNER_DECISION: COMPLETE|INCOMPLETE_RETRY|INCOMPLETE_FAIL|NEEDS_CLARIFICATION with optional REASON: <text>
  4. Review parsing is tolerant — accepts "Approved", "Complete", "LGTM", not just exact REVIEW_DECISION: APPROVE
  5. Markers are optional acceleratorsSTEP_COMPLETE:<step-name> still works as a fast-path but is never required
  6. Stores primary output plus review output in the step artifact

Evidence-based completion: The runner collects channel messages, file artifacts, process exit codes, and coordination signals (e.g., WORKER_DONE posted in channel) as completion evidence. When sufficient evidence exists, the step completes without requiring any sentinel marker.

Deterministic and worktree steps are unchanged and do not require owner/review delegation.

Sandbox Execution

By default the runner spawns steps as local child processes. To run them in isolated sandboxes instead, select a provider — the runner still owns command construction, env, cwd, timeout, and the whole DAG/retry/verification pipeline; the provider only supplies "where the command runs".

# Off by default. Unset the flag to get local child processes back.export RELAYFLOWS_SANDBOX_PROVIDER=daytona
export DAYTONA_API_KEY=...
export RELAYFLOWS_SANDBOX_HOME_DIR=/home/daytona # image-specific, requiredexport RELAYFLOWS_SANDBOX_SNAPSHOT=my-snapshot # optional

Or in code:

import{WorkflowRunner}from"@relayflows/core";construnner=newWorkflowRunner({sandbox: {provider: "daytona",homeDir: "/home/daytona"},});
ProviderWhat it gives you
none (default)No sandbox. Local child processes, exactly as before.
daytonaReal remote sandboxes via @agent-relay/sandbox. Needs the optional peer @daytonaio/sdk.
local-processReal local processes in a private per-step directory with its own HOME. Isolates the filesystem root, not the machine — good for development and CI, not a security boundary.

Reversibility.provider: "none" (or an unset RELAYFLOWS_SANDBOX_PROVIDER) produces no backend at all, so nothing about the default path changes. An explicit executor or processBackend still wins over sandbox config, so a host that injects its own backend today keeps it.

Custom providers. Register a runtime under any name, or hand one in directly. This is the seam a host uses to plug in a runtime that does not live in this repo:

import{registerSandboxProvider,WorkflowRunner}from"@relayflows/core";registerSandboxProvider("my-runtime",(config)=>newMyRuntime(config));// ...or skip the registry entirely:newWorkflowRunner({sandbox: {runtime: myRuntime}});

A runtime needs five methods — launch, exec, uploadFile, getHomeDir, destroy — matching @agent-relay/sandbox's WorkflowRuntime.

Schema Validation

A JSON Schema is available at packages/core/src/schema.json for editor autocompletion and validation of relay.yaml files.

Development

npm install
npm run typecheck
npm run test

Requirements

  • Node.js 22+
  • @relayflows/cli installed (npm install -g @relayflows/cli)
  • For Python: Python 3.10+ with pip install agent-relay
  • For TypeScript workflow files: tsx or ts-node installed

License

Apache-2.0 — Copyright 2025 Agent Workforce Incorporated

About

Orchestrate multi-step, multi-agent execution across Agent Relay workers

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

relayflows

npmLicense

Workflow engine and CLI for Agent Relay. Orchestrate multi-agent workflows using YAML, TypeScript, or Python. Define agents, wire up dependencies, and let the runner handle execution, retries, and verification.

Quick Start

CLI

# Run a YAML workflow
relayflows run workflow.yaml
# Run a TypeScript workflow
relayflows run workflow.ts
# Run a Python workflow
relayflows run workflow.py
# Run a specific named workflow from a file
relayflows run workflow.yaml --workflow deploy

TypeScript

import{workflow}from"@relayflows/core";constresult=awaitworkflow("ship-feature").pattern("dag").agent("planner",{cli: "claude",role: "Plans implementation"}).agent("developer",{cli: "codex",role: "Writes code"}).agent("reviewer",{cli: "claude",role: "Reviews code"}).step("plan",{agent: "planner",task: "Create implementation plan for user authentication",}).step("implement",{agent: "developer",task: "Implement the plan",dependsOn: ["plan"],}).step("review",{agent: "reviewer",task: "Review the implementation",dependsOn: ["implement"],}).run();console.log(result.status);// "completed" | "completed_early" | "failed" | "cancelled" | "needs_human"

Python

fromagent_relayimportworkflowresult= (
workflow("ship-feature")
.pattern("dag")
.agent("planner", cli="claude", role="Plans implementation")
.agent("developer", cli="codex", role="Writes code")
.agent("reviewer", cli="claude", role="Reviews code")
.step("plan", agent="planner", task="Create implementation plan for user auth")
.step("implement", agent="developer", task="Implement the plan", depends_on=["plan"])
.step("review", agent="reviewer", task="Review the implementation", depends_on=["implement"])
.run()
)

Watching a Run

When a run starts, the runner mints and prints a link you can open to follow it live:

[workflow 00:02] Workspace created for this workflow.
[workflow 00:02] Observer: https://agentrelay.com/observer?key=ot_live_...
[workflow 00:02] Channel: wf-ship-feature-a1b2c3

Open the Observer: URL and you see messages, agent activity, handoffs, and deliveries in real time. The link carries a scoped observer token (ot_live_): read-only, expiring in 24 hours, and individually revocable.

Minting is best-effort, so a link is not guaranteed — see If no link appears. A failed mint never fails the run.

Treat the link itself as a shared secret. The token is a bearer credential in a query parameter, so anyone who gets the URL can read the stream it covers until it expires or you revoke it. It is far safer than a workspace key — it cannot send, spawn, or administer — but it is not public.

The two ways a run gets a workspace

Which link you get depends on where the run's Relaycast workspace comes from.

No RELAY_API_KEY set — the runner creates a throwaway workspace for this run alone and mints an observer link covering all of it, DMs included. The workspace is anonymous and disappears from your reach when the run ends, so copy the link while the run is going. Nothing persists the underlying key, and there is no way to recover it afterward.

RELAY_API_KEY set — the runner uses your workspace and mints a link scoped to just this run's channel, with agent DMs excluded, so the link does not expose unrelated traffic in a shared workspace. This is the better setup for anything you may want to revisit: the run is in a workspace you own, so you can mint fresh links whenever you like.

# One-time: create a workspace you own and keep the key
curl -sX POST https://api.relaycast.dev/v1/workspaces \
-H 'content-type: application/json' -d '{"name":"my-workflows"}' \
| jq -r '.data.api_key // .api_key'export RELAY_API_KEY=rk_live_... # put this in your shell profile
relayflows run workflow.yaml

If you already use the Agent Relay CLI, agent-relay workspace key --reveal-secrets prints the key of your active workspace. Note it is masked without --reveal-secrets.

Minting more links yourself

With a workspace you own, agent-relay observer mints links on demand:

agent-relay observer # read-only link, 24h, DMs excluded
agent-relay observer --channels wf-ship-a1b2c3 # scope to one run
agent-relay observer --include-dms # include agent DMs
agent-relay observer --expires 7d # longer-lived link
agent-relay observer list # what is outstanding
agent-relay observer revoke <id># cut one off immediately

Never share the workspace key

A workspace key (rk_live_) is an administrative credential — it can send messages, spawn and remove agents, and change workspace settings. Do not put one in an observer URL, a chat message, or a terminal transcript; query strings end up in browser history, referrer headers, and proxy logs. The realtime endpoint rejects it outright, so a link built from one cannot stream anyway.

Workspace key (rk_live_)Observer token (ot_live_)
Read messages and activityyesyes
Send, spawn agents, administeryesno
Expiresnoyes
Revocable individuallynoyes
Scopable to channelsnoyes

The runner only ever prints ot_live_ links, and scrubs rk_live_ values out of channel output.

Configuration

VariablePurpose
RELAY_API_KEYWorkspace key to run against. Unset means a throwaway workspace per run.
RELAY_OBSERVER_URLObserver dashboard base. Defaults to https://agentrelay.com/observer.
RELAY_OBSERVER_EXPIRESLink lifetime as 30m / 24h / 7d. Defaults to 24h; unparseable or over 90d falls back to 24h.
RELAYCAST_BASE_URLRelaycast engine base. Defaults to https://api.relaycast.dev.

If no link appears

  • Observation: unavailable — the token could not be minted (engine unreachable, or it rejected the request). The run is unaffected; minting is best-effort by design and never fails a run.
  • No observer lines at all — the run needed no broker: every step was deterministic, worktree, integration, or waitFor, or an external executor handled agent spawning, or Relaycast was disabled with AGENT_RELAY_WORKFLOW_DISABLE_RELAYCAST=1.
  • Observation: run agent-relay observer — you set RELAY_API_KEY and minting failed. Mint a link by hand with agent-relay observer.

Consumer-Facing Apps + AI SDK Communicate Flows

A good production split is:

  1. AI SDK app handles the user conversation and streaming UI
  2. Communicate / onRelay() lets that point-person coordinate with specialists over Relay
  3. Workflows / runWorkflow() take over when a request needs multi-step execution, verification, or handoffs
import{streamText,wrapLanguageModel}from'ai';import{openai}from'@ai-sdk/openai';import{Relay}from'@agent-relay/sdk/communicate';import{onRelay}from'@agent-relay/sdk/communicate/adapters/ai-sdk';import{runWorkflow}from'@relayflows/core';exportasyncfunctionPOST(req: Request){const{ prompt, escalate, repo }=awaitreq.json();constrelay=newRelay('AppLead');constrelaySession=onRelay({name: 'AppLead',instructions: 'You are the customer-facing lead. Keep the user updated and delegate implementation via Relay when needed.',},relay);constmodel=wrapLanguageModel({model: openai('gpt-4o-mini'),middleware: relaySession.middleware,});if(escalate){constworkflow=awaitrunWorkflow('workflows/feature-dev.yaml',{vars: {task: prompt, repo },});returnResponse.json({status: workflow.status,runId: workflow.runId});}returnstreamText({
model,tools: relaySession.tools,system: 'Answer directly when possible; coordinate internally when the task needs specialists.',
prompt,}).toUIMessageStreamResponse({onFinish(){relaySession.cleanup();voidrelay.close();},});}

That pattern keeps the user experience snappy while still letting longer Relay workflows run with proper ownership, retries, and verification.

YAML Format

Workflows are defined as relay.yaml files:

version: "1.0"name: my-workflowdescription: "Optional description"swarm:
pattern: dag # Execution pattern (see Patterns below)maxConcurrency: 3# Max agents running in paralleltimeoutMs: 3600000# Global timeout (1 hour)channel: my-channel # Relay channel for agent communicationagents:
- name: backendcli: claude # claude | codex | gemini | aider | goose | opencode | droidrole: "Backend engineer"constraints:
model: opustimeoutMs: 600000retries: 2
- name: testercli: codexrole: "Test engineer"interactive: false # Non-interactive: runs as subprocess, no PTY/messaging# A persona replaces cli + role. Its harness, model, standing instructions,# installed skills, MCP servers, and harness settings come from the spec.# Persona agents are interactive-only: do not set cli, preset, or# constraints.model, and do not use interactive: false.
- name: integrationspersona: nango-integrationsworkflows:
- name: build-and-testonError: retry # fail | skip | retrysteps:
- name: build-apiagent: backendtask: "Build the REST API endpoints for user management"verification:
type: file_existsvalue: "src/api/users.ts"retries: 1
- name: write-testsagent: testertask: "Write integration tests for: {{steps.build-api.output}}"dependsOn: [build-api]
- name: run-testsagent: testertask: "Run the test suite and report results"dependsOn: [write-tests]verification:
type: exit_codevalue: "0"errorHandling:
strategy: retrymaxRetries: 2retryDelayMs: 5000repairAgent: testerrepairRetries: 2onExhaustion: needs-humannotifyChannel: my-channel

Template Variables

Use {{variable}} for user-provided values and {{steps.STEP_NAME.output}} for previous step outputs:

steps:
- name: planagent: plannertask: "Plan implementation for: {{task}}"# User variable
- name: implementagent: developerdependsOn: [plan]task: "Implement: {{steps.plan.output}}"# Previous step output

User variables are passed via the CLI or programmatically:

awaitrunWorkflow("workflow.yaml",{vars: {task: "Add OAuth2 support"},});

Blocking Slack Questions

Workflows can pause on a Slack question by adding a Slack integration step with action: askQuestion. The step posts a question, waits for the first human reply in the message thread, exposes that answer through {{steps.<name>.output}}, and can inject it into a running agent when injectToAgent is set.

workflows:
- name: defaultsteps:
- name: ask-humantype: integrationintegration: slackaction: askQuestionparams:
channel: "#engineering"text: "The implementer is blocked on migration strategy. Which path should it take?"waitTimeoutMs: "3600000"injectToAgent: "backend-runtime-name"injectTemplate: "HUMAN_ANSWER: {{answer.text}}"output: '{"format":"text","path":"answer.text"}'
- name: continue-with-answeragent: backenddependsOn: [ask-human]task: "Continue using this human guidance: {{steps.ask-human.output}}"

askQuestion can use the local Slack API runtime, or Relayfile-backed Slack writebacks when the workflow already has a Relayfile Slack integration.

Interactive agent steps can also opt into marker-driven assistance. With humanAssistance.slack enabled, an agent can print a line beginning with HUMAN_QUESTION:. The runner posts that question to Slack, blocks while waiting for a human reply, then injects HUMAN_ANSWER: ... back into that same agent session. If integrations.relayfile is present, Relayflows automatically uses the existing Relayfile/Pear Slack connection; no Slack bot token, Relayfile workspace id, or Relayfile token is required in the workflow.

Runnable TypeScript proof: examples/typescript/slack-human-assistance-e2e.ts.

swarm:
pattern: daghumanAssistance:
slack:
channel: proj-cloudtimeoutMs: 3600000integrations:
relayfile: {}workflows:
- name: defaultsteps:
- name: implementagent: backendtask: "Proceed, but ask for human guidance if the migration strategy is ambiguous."

Relayfile Event Subscriptions

Relayflows can subscribe to Relayfile integration events and inject matching events into active agents. Workflow-level subscriptions live under integrations.subscriptions; agent-level subscriptions use Workforce-style agents[].watch or Relayflows-style agents[].subscriptions.

integrations:
relayfile: {}subscriptions:
- name: pr-feedbackprovider: githubpath: /github/repos/acme/web/pulls/42/**events: [created, updated]agents: [pr-babysitter]agents:
- name: pr-babysittercli: codexwatch:
- paths: [/github/repos/acme/web/pulls/42/reviews/**]events: [created, updated]workflows:
- name: defaultsteps:
- name: babysit-pragent: pr-babysittertask: | Stay active and wait for INTEGRATION_EVENT messages about PR feedback. Read the Relayfile path from the event, address comments until no open feedback remains, then notify the user in Slack.

Verification Checks

Each step can include a verification check. Verification is one input to the runner's completion decision pipeline — when verification passes, the step completes even without a sentinel marker.

TypeDescription
exit_codeAgent must exit with the specified code (preferred for code-editing steps)
file_existsA file must exist at the specified path after the step
output_containsStep output must contain the specified string (optional accelerator)
customNo-op in the runner; handled by external callers
# Preferred — deterministic verificationverification:
type: exit_codevalue: "0"description: "Process exited successfully"# Also valid — output_contains as an optional acceleratorverification:
type: output_containsvalue: "IMPLEMENTATION_COMPLETE"description: "Agent confirms completion (optional fast-path)"

Completion Decision Pipeline

The runner uses a multi-signal pipeline to decide step completion:

  1. Deterministic verification — if a verification check passes, the step completes immediately (completed_verified)
  2. Owner decision — the step owner can issue OWNER_DECISION: COMPLETE|INCOMPLETE_RETRY|INCOMPLETE_FAIL (completed_by_owner_decision)
  3. Evidence-based completion — channel messages, file artifacts, and exit codes are collected as evidence (completed_by_evidence)
  4. Marker fast-pathSTEP_COMPLETE:<step-name> still works as an accelerator but is never required
Completion StateMeaning
completed_verifiedDeterministic verification passed
completed_by_owner_decisionOwner approved the step
completed_by_evidenceEvidence-based completion
retry_requested_by_ownerOwner requested retry
failed_verificationVerification explicitly failed
failed_owner_decisionOwner rejected the step
failed_no_evidenceNo verification, no owner decision, no evidence

Review parsing is tolerant: The runner accepts semantically equivalent outputs like "Approved", "Complete", "LGTM" — not just exact REVIEW_DECISION: APPROVE strings.

Swarm Patterns

The swarm.pattern field controls how agents are coordinated:

Core Patterns

PatternDescription
dagDirected acyclic graph — steps run based on dependency edges (default)
fan-outAll agents run in parallel
pipelineSequential chaining of steps
hub-spokeCentral hub coordinates spoke agents
consensusAgents vote on decisions
meshFull communication graph between agents
handoffSequential handoff between agents
cascadeWaterfall with phase gates
debateAgents propose and counter-argue
hierarchicalMulti-level reporting structure

Data Processing Patterns

PatternDescription
map-reduceSplit work into chunks (mappers), process in parallel, aggregate results (reducers)
scatter-gatherFan out requests to workers, collect and synthesize responses

Supervision & Quality Patterns

PatternDescription
supervisorMonitor agent monitors workers, restarts on failure, manages health
reflectionAgent produces output, critic reviews and provides feedback for iteration
verifierProducer agents submit work to verifier agents for validation

Adversarial & Validation Patterns

PatternDescription
red-teamAttacker agents probe for weaknesses, defender agents respond
auctionAuctioneer broadcasts tasks, agents bid based on capability/cost

Resilience Patterns

PatternDescription
escalationStart with fast/cheap agents, escalate to more capable on failure
sagaDistributed transactions with compensating actions on failure
circuit-breakerPrimary agent with fallback chain, fail fast and recover

Collaborative Patterns

PatternDescription
blackboardShared workspace where agents contribute incrementally to a solution
swarmEmergent behavior from simple agent rules (neighbor communication)

Auto-Selection by Role

When swarm.pattern is omitted, the coordinator auto-selects based on agent roles. Patterns are checked in priority order below (first match wins):

PriorityPatternRequired Roles/Config
1dagSteps with dependsOn
2consensusUses coordination.consensusStrategy config
3map-reducemapper + reducer
4red-team(attacker OR red-team) + (defender OR blue-team)
5reflectioncritic
6escalationtier-1, tier-2, etc.
7auctionauctioneer
8sagasaga-orchestrator OR compensate-handler
9circuit-breakerfallback, backup, OR primary
10blackboardblackboard OR shared-workspace
11swarmhive-mind OR swarm-agent
12verifierverifier
13supervisorsupervisor
14hierarchicallead (with 4+ agents)
15hub-spokehub OR coordinator
16pipelineUnique agents per step, 3+ steps
17fan-outDefault fallback

Error Handling

Step-Level

steps:
- name: risky-stepagent: workertask: "Do something that might fail"retries: 3# Retry up to 3 times on failuretimeoutMs: 300000# 5 minute timeout

Successful early termination

A deterministic gate can explicitly declare exit codes that mean “there is no work to do.” A matching code ends the run with the distinct completed_early status and skips every step that has not started:

steps:
- name: claim-worktype: deterministiccommand: node bin/claim-work.mjsterminalSuccessExitCodes: [78]
- name: process-claimagent: workertask: Process the claimed workdependsOn: [claim-work]

Terminal-capable gates are scheduling barriers, so other ready work does not race the gate. The triggering step is completed with completion reason completed_early_exit; remaining steps are skipped, and the CLI exits 0 while clearly reporting COMPLETED EARLY. Verification still applies, so a verification failure remains a real failure.

This behavior is opt-in. Without terminalSuccessExitCodes, exit 78 and every other non-zero exit retain their existing failure behavior. The new completed_early run status is an additive public API value: consumers with exhaustive status switches, strict validators, database constraints, or terminal-status polling must handle it separately from completed.

Workflow-Level

The onError field on a workflow controls what happens when a step fails:

ValueBehavior
fail / fail-fastStop immediately, skip downstream steps
skip / continueSkip downstream dependents, continue independent steps
retryRetry the step; deterministic gates ask a workflow agent to repair before each retry when an agent is available

Global

errorHandling:
strategy: retrymaxRetries: 2retryDelayMs: 5000repairAgent: testerrepairRetries: 2notifyChannel: alerts

Retry-mode workflows are repair-aware by default. Deterministic step failures, verification gate failures, and malformed agent artifacts are treated as repairable work before terminal failure. The runner chooses errorHandling.repairAgent when set, otherwise it uses the step's owning/upstream agent when possible, then falls back to the best available workflow agent. The selected agent gets the failed command or agent output, working directory, exit information, and captured evidence, then the failed gate or step is retried. Use repairRetries: 0, strategy: fail-fast, or strategy: continue when a workflow intentionally should not invoke repair agents. Set onExhaustion: needs-human to end an exhausted repairable run as needs_human instead of failed.

Built-in Templates

Six pre-built workflow templates are included:

TemplatePatternDescription
feature-devhub-spokePlan, implement, review, and finalize a feature
bug-fixhub-spokeInvestigate, patch, validate, and document a bug fix
code-reviewfan-outParallel multi-reviewer assessment with consolidated findings
security-auditpipelineScan, triage, remediate, and verify security issues
refactorhierarchicalAnalyze, plan, execute, and validate a refactor
documentationhandoffResearch, draft, review, and publish documentation

Using Templates

import{TemplateRegistry,WorkflowRunner}from"@relayflows/core";constregistry=newTemplateRegistry();// List available templatesconsttemplates=awaitregistry.listTemplates();// Load and run a templateconstconfig=awaitregistry.loadTemplate("feature-dev");construnner=newWorkflowRunner();constresult=awaitrunner.execute(config,undefined,{task: "Add WebSocket support to the API",});// Install a custom template from a URLawaitregistry.installExternalTemplate("https://example.com/my-template.yaml","my-template");

TypeScript Builder API

The builder constructs a RelayYamlConfig object and can run it, export it as YAML, or return the raw config.

import{workflow}from"@relayflows/core";// Build and runconstresult=awaitworkflow("my-workflow").pattern("dag").maxConcurrency(3).timeout(60*60*1000).channel("my-channel").agent("backend",{cli: "claude",role: "Backend engineer",model: "opus",retries: 2,}).agent("frontend",{cli: "codex",role: "Frontend engineer",interactive: false,// Non-interactive subprocess mode}).step("api",{agent: "backend",task: "Build REST API",verification: {type: "output_contains",value: "API_READY"},}).step("ui",{agent: "frontend",task: "Build the UI",dependsOn: ["api"],}).onError("retry",{maxRetries: 2,retryDelayMs: 5000}).run();// Or export to YAMLconstyaml=workflow("my-workflow").pattern("dag").agent("worker",{cli: "claude"}).step("task1",{agent: "worker",task: "Do something"}).toYaml();// Or get the raw config objectconstconfig=workflow("my-workflow").pattern("dag").agent("worker",{cli: "claude"}).step("task1",{agent: "worker",task: "Do something"}).toConfig();

Python Builder API

The Python builder ships with @agent-relay/sdk-py:

pip install agent-relay
fromagent_relayimportworkflow, run_yaml# Build and runresult= (
workflow("my-workflow")
.pattern("dag")
.max_concurrency(3)
.timeout(3600000)
.agent("backend", cli="claude", role="Backend engineer")
.agent("frontend", cli="codex", role="Frontend engineer")
.step("api", agent="backend", task="Build REST API")
.step("ui", agent="frontend", task="Build the UI", depends_on=["api"])
.on_error("retry", max_retries=2, retry_delay_ms=5000)
.run()
)
# Run an existing YAML fileresult=run_yaml("workflows/my-workflow.yaml")
# Export to YAML stringyaml_str= (
workflow("my-workflow")
.pattern("dag")
.agent("worker", cli="claude")
.step("task1", agent="worker", task="Do something")
.to_yaml()
)
# Get the raw config dictconfig= (
workflow("my-workflow")
.pattern("dag")
.agent("worker", cli="claude")
.step("task1", agent="worker", task="Do something")
.to_config()
)

Programmatic API

For full control, use the WorkflowRunner directly:

import{WorkflowRunner}from"@relayflows/core";construnner=newWorkflowRunner({cwd: "/path/to/project",// Working directory (default: process.cwd())relay: {port: 3000},// AgentRelay options (optional)});// Listen to events (broker:event fires frequently — filter it out for cleaner output)runner.on((event)=>{if(event.type==='broker:event')return;console.log(event.type,event);});// Parse and executeconstconfig=awaitrunner.parseYamlFile("workflow.yaml");construn=awaitrunner.execute(config,"workflow-name",{task: "Build the feature",});// Pause / resume / abortrunner.pause();runner.unpause();runner.abort();// Resume a failed runconstresumed=awaitrunner.resume(run.id);

Zero-Config Convenience Function

import{runWorkflow}from"@relayflows/core";constresult=awaitrunWorkflow("workflow.yaml",{workflow: "deploy",vars: {environment: "staging"},onEvent: (event)=>{if(event.type!=='broker:event')console.log(event.type);},});

Coordination

Barriers

Synchronization points that wait for specific steps to complete:

coordination:
barriers:
- name: all-reviews-donewaitFor: [review-arch, review-security, review-correctness]timeoutMs: 900000consensusStrategy: majority # majority | unanimous | quorum

Shared State

Agents can share state during execution:

state:
backend: memory # memory | redis | databasettlMs: 86400000namespace: my-workflow

Supported Agent CLIs

CLIDescription
claudeClaude Code (Anthropic)
codexCodex CLI (OpenAI)
geminiGemini CLI (Google)
aiderAider coding assistant
gooseGoose AI assistant
opencodeOpenCode CLI
droidDroid CLI

Non-Interactive Agents

By default, agents run in interactive PTY mode with full relay messaging. For workers that just need to execute a task and return output — common in fan-out, map-reduce, and pipeline patterns — set interactive: false to run them as lightweight subprocesses.

YAML

agents:
- name: leadcli: clauderole: "Coordinates work"# interactive: true (default) — full PTY, relay messaging, /exit detection
- name: workercli: codexrole: "Executes tasks"interactive: false # Runs "codex exec <task>", captures stdout

TypeScript

workflow("fan-out-analysis").pattern("fan-out").agent("lead",{cli: "claude",role: "Coordinator"}).agent("worker-1",{cli: "codex",interactive: false,role: "Analyst"}).agent("worker-2",{cli: "codex",interactive: false,role: "Analyst"}).step("analyze-1",{agent: "worker-1",task: "Analyze module A"}).step("analyze-2",{agent: "worker-2",task: "Analyze module B"}).step("synthesize",{agent: "lead",task: "Combine: {{steps.analyze-1.output}} + {{steps.analyze-2.output}}",dependsOn: ["analyze-1","analyze-2"],}).run();

How It Works

AspectInteractive (default)Non-Interactive
ExecutionFull PTY with stdin/stdoutchild_process.spawn() with piped stdio
CLI invocationStandard interactive sessionOne-shot mode (claude -p, codex exec, etc.)
Relay messagingCan send/receive messagesNo messaging — excluded from topology edges
Self-terminationMust output /exitProcess exits naturally when done
Output capturePTY output bufferstdout capture
OverheadHigher (PTY, echo verification, SIGWINCH)Lower (simple subprocess)

Non-Interactive CLI Commands

CLICommandNotes
claudeclaude -p "<task>"Print mode, exits after response
codexcodex exec "<task>"One-shot execution
geminigemini -p "<task>"Prompt mode
opencodeopencode --prompt "<task>"One-shot prompt
droiddroid exec "<task>"One-shot execution
aideraider --message "<task>" --yes-always --no-gitAuto-approve, skip git
goosegoose run --text "<task>" --no-sessionText mode, no session file

When to Use

  • Fan-out workers that process a task and return results
  • Map-reduce mappers that don't need mid-task communication
  • Pipeline stages that transform input to output
  • Any agent that doesn't need turn-by-turn relay messaging

When NOT to Use

  • Lead/coordinator agents that communicate with others
  • Agents in debate, consensus, or reflection patterns
  • Agents that need to receive messages during execution

Agent Slash Commands

Agents running inside a workflow can output slash commands to signal the broker. These are detected in the agent's PTY output at the broker level — the agent simply prints the command on its own line.

/exit

Signals that the agent has completed its current step and is ready to be released.

/exit

The workflow runner waits for each agent to /exit after delivering a step task. When the broker detects /exit in the agent's output (exact line match after ANSI stripping), it:

  1. Emits an agent_exit frame with reason: "agent_requested"
  2. Triggers graceful PTY shutdown

If an agent does not /exit within the step's timeoutMs, the runner treats the step as timed out. As a safety net, steps with file_exists verification will still pass if the expected file is present despite the timeout.

Best practice: Instruct agents to output /exit when done in your step task descriptions:

steps:
- name: build-apiagent: backendtask: | Build the REST API endpoints for user management. When finished, output /exit.

Idle Agent Detection and Nudging

Interactive agents sometimes finish their task but forget to /exit, sitting idle and blocking downstream steps. The runner can detect idle agents and take action automatically.

Configuration

Add idleNudge to your swarm config:

swarm:
pattern: hub-spokeidleNudge:
nudgeAfterMs: 120000# 2 min before first nudge (default)escalateAfterMs: 120000# 2 min after nudge before force-release (default)maxNudges: 1# Nudges before escalation (default)

All built-in templates include idle nudging with these defaults.

How It Works

  1. Detection: The broker tracks agent output timestamps and emits agent_idle events when an agent goes silent for the configured threshold
  2. Nudge: For hub patterns (hub-spoke, fan-out, hierarchical, etc.), the runner tells the hub agent to check on the idle agent. For non-hub patterns, a system message is injected directly into the agent's PTY
  3. Escalation: If the agent remains idle after maxNudges attempts, the runner force-releases it and captures whatever output was produced
  4. No config: When idleNudge is omitted, the runner uses simple waitForExit (backward compatible)

Events

The runner emits two new events for idle nudging:

EventDescription
step:nudgedFired when a nudge message is sent to an idle agent
step:force-releasedFired when an agent is force-released after exhausting nudges

Automatic Step Owner and Review

For interactive agent steps, the runner uses a point-person-led completion model:

  1. Elects a step owner (prefers lead/coordinator-style agents, falls back to the step agent)
  2. Runs a completion decision pipeline — checks deterministic verification first, then owner judgment, then evidence
  3. Owner can issue structured decisions via OWNER_DECISION: COMPLETE|INCOMPLETE_RETRY|INCOMPLETE_FAIL|NEEDS_CLARIFICATION with optional REASON: <text>
  4. Review parsing is tolerant — accepts "Approved", "Complete", "LGTM", not just exact REVIEW_DECISION: APPROVE
  5. Markers are optional acceleratorsSTEP_COMPLETE:<step-name> still works as a fast-path but is never required
  6. Stores primary output plus review output in the step artifact

Evidence-based completion: The runner collects channel messages, file artifacts, process exit codes, and coordination signals (e.g., WORKER_DONE posted in channel) as completion evidence. When sufficient evidence exists, the step completes without requiring any sentinel marker.

Deterministic and worktree steps are unchanged and do not require owner/review delegation.

Sandbox Execution

By default the runner spawns steps as local child processes. To run them in isolated sandboxes instead, select a provider — the runner still owns command construction, env, cwd, timeout, and the whole DAG/retry/verification pipeline; the provider only supplies "where the command runs".

# Off by default. Unset the flag to get local child processes back.export RELAYFLOWS_SANDBOX_PROVIDER=daytona
export DAYTONA_API_KEY=...
export RELAYFLOWS_SANDBOX_HOME_DIR=/home/daytona # image-specific, requiredexport RELAYFLOWS_SANDBOX_SNAPSHOT=my-snapshot # optional

Or in code:

import{WorkflowRunner}from"@relayflows/core";construnner=newWorkflowRunner({sandbox: {provider: "daytona",homeDir: "/home/daytona"},});
ProviderWhat it gives you
none (default)No sandbox. Local child processes, exactly as before.
daytonaReal remote sandboxes via @agent-relay/sandbox. Needs the optional peer @daytonaio/sdk.
local-processReal local processes in a private per-step directory with its own HOME. Isolates the filesystem root, not the machine — good for development and CI, not a security boundary.

Reversibility.provider: "none" (or an unset RELAYFLOWS_SANDBOX_PROVIDER) produces no backend at all, so nothing about the default path changes. An explicit executor or processBackend still wins over sandbox config, so a host that injects its own backend today keeps it.

Custom providers. Register a runtime under any name, or hand one in directly. This is the seam a host uses to plug in a runtime that does not live in this repo:

import{registerSandboxProvider,WorkflowRunner}from"@relayflows/core";registerSandboxProvider("my-runtime",(config)=>newMyRuntime(config));// ...or skip the registry entirely:newWorkflowRunner({sandbox: {runtime: myRuntime}});

A runtime needs five methods — launch, exec, uploadFile, getHomeDir, destroy — matching @agent-relay/sandbox's WorkflowRuntime.

Schema Validation

A JSON Schema is available at packages/core/src/schema.json for editor autocompletion and validation of relay.yaml files.

Development

npm install
npm run typecheck
npm run test

Requirements

  • Node.js 22+
  • @relayflows/cli installed (npm install -g @relayflows/cli)
  • For Python: Python 3.10+ with pip install agent-relay
  • For TypeScript workflow files: tsx or ts-node installed

License

Apache-2.0 — Copyright 2025 Agent Workforce Incorporated

About

Orchestrate multi-step, multi-agent execution across Agent Relay workers

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

relayflows

npmLicense

Workflow engine and CLI for Agent Relay. Orchestrate multi-agent workflows using YAML, TypeScript, or Python. Define agents, wire up dependencies, and let the runner handle execution, retries, and verification.

Quick Start

CLI

# Run a YAML workflow
relayflows run workflow.yaml
# Run a TypeScript workflow
relayflows run workflow.ts
# Run a Python workflow
relayflows run workflow.py
# Run a specific named workflow from a file
relayflows run workflow.yaml --workflow deploy

TypeScript

import{workflow}from"@relayflows/core";constresult=awaitworkflow("ship-feature").pattern("dag").agent("planner",{cli: "claude",role: "Plans implementation"}).agent("developer",{cli: "codex",role: "Writes code"}).agent("reviewer",{cli: "claude",role: "Reviews code"}).step("plan",{agent: "planner",task: "Create implementation plan for user authentication",}).step("implement",{agent: "developer",task: "Implement the plan",dependsOn: ["plan"],}).step("review",{agent: "reviewer",task: "Review the implementation",dependsOn: ["implement"],}).run();console.log(result.status);// "completed" | "completed_early" | "failed" | "cancelled" | "needs_human"

Python

fromagent_relayimportworkflowresult= (
workflow("ship-feature")
.pattern("dag")
.agent("planner", cli="claude", role="Plans implementation")
.agent("developer", cli="codex", role="Writes code")
.agent("reviewer", cli="claude", role="Reviews code")
.step("plan", agent="planner", task="Create implementation plan for user auth")
.step("implement", agent="developer", task="Implement the plan", depends_on=["plan"])
.step("review", agent="reviewer", task="Review the implementation", depends_on=["implement"])
.run()
)

Watching a Run

When a run starts, the runner mints and prints a link you can open to follow it live:

[workflow 00:02] Workspace created for this workflow.
[workflow 00:02] Observer: https://agentrelay.com/observer?key=ot_live_...
[workflow 00:02] Channel: wf-ship-feature-a1b2c3

Open the Observer: URL and you see messages, agent activity, handoffs, and deliveries in real time. The link carries a scoped observer token (ot_live_): read-only, expiring in 24 hours, and individually revocable.

Minting is best-effort, so a link is not guaranteed — see If no link appears. A failed mint never fails the run.

Treat the link itself as a shared secret. The token is a bearer credential in a query parameter, so anyone who gets the URL can read the stream it covers until it expires or you revoke it. It is far safer than a workspace key — it cannot send, spawn, or administer — but it is not public.

The two ways a run gets a workspace

Which link you get depends on where the run's Relaycast workspace comes from.

No RELAY_API_KEY set — the runner creates a throwaway workspace for this run alone and mints an observer link covering all of it, DMs included. The workspace is anonymous and disappears from your reach when the run ends, so copy the link while the run is going. Nothing persists the underlying key, and there is no way to recover it afterward.

RELAY_API_KEY set — the runner uses your workspace and mints a link scoped to just this run's channel, with agent DMs excluded, so the link does not expose unrelated traffic in a shared workspace. This is the better setup for anything you may want to revisit: the run is in a workspace you own, so you can mint fresh links whenever you like.

# One-time: create a workspace you own and keep the key
curl -sX POST https://api.relaycast.dev/v1/workspaces \
-H 'content-type: application/json' -d '{"name":"my-workflows"}' \
| jq -r '.data.api_key // .api_key'export RELAY_API_KEY=rk_live_... # put this in your shell profile
relayflows run workflow.yaml

If you already use the Agent Relay CLI, agent-relay workspace key --reveal-secrets prints the key of your active workspace. Note it is masked without --reveal-secrets.

Minting more links yourself

With a workspace you own, agent-relay observer mints links on demand:

agent-relay observer # read-only link, 24h, DMs excluded
agent-relay observer --channels wf-ship-a1b2c3 # scope to one run
agent-relay observer --include-dms # include agent DMs
agent-relay observer --expires 7d # longer-lived link
agent-relay observer list # what is outstanding
agent-relay observer revoke <id># cut one off immediately

Never share the workspace key

A workspace key (rk_live_) is an administrative credential — it can send messages, spawn and remove agents, and change workspace settings. Do not put one in an observer URL, a chat message, or a terminal transcript; query strings end up in browser history, referrer headers, and proxy logs. The realtime endpoint rejects it outright, so a link built from one cannot stream anyway.

Workspace key (rk_live_)Observer token (ot_live_)
Read messages and activityyesyes
Send, spawn agents, administeryesno
Expiresnoyes
Revocable individuallynoyes
Scopable to channelsnoyes

The runner only ever prints ot_live_ links, and scrubs rk_live_ values out of channel output.

Configuration

VariablePurpose
RELAY_API_KEYWorkspace key to run against. Unset means a throwaway workspace per run.
RELAY_OBSERVER_URLObserver dashboard base. Defaults to https://agentrelay.com/observer.
RELAY_OBSERVER_EXPIRESLink lifetime as 30m / 24h / 7d. Defaults to 24h; unparseable or over 90d falls back to 24h.
RELAYCAST_BASE_URLRelaycast engine base. Defaults to https://api.relaycast.dev.

If no link appears

  • Observation: unavailable — the token could not be minted (engine unreachable, or it rejected the request). The run is unaffected; minting is best-effort by design and never fails a run.
  • No observer lines at all — the run needed no broker: every step was deterministic, worktree, integration, or waitFor, or an external executor handled agent spawning, or Relaycast was disabled with AGENT_RELAY_WORKFLOW_DISABLE_RELAYCAST=1.
  • Observation: run agent-relay observer — you set RELAY_API_KEY and minting failed. Mint a link by hand with agent-relay observer.

Consumer-Facing Apps + AI SDK Communicate Flows

A good production split is:

  1. AI SDK app handles the user conversation and streaming UI
  2. Communicate / onRelay() lets that point-person coordinate with specialists over Relay
  3. Workflows / runWorkflow() take over when a request needs multi-step execution, verification, or handoffs
import{streamText,wrapLanguageModel}from'ai';import{openai}from'@ai-sdk/openai';import{Relay}from'@agent-relay/sdk/communicate';import{onRelay}from'@agent-relay/sdk/communicate/adapters/ai-sdk';import{runWorkflow}from'@relayflows/core';exportasyncfunctionPOST(req: Request){const{ prompt, escalate, repo }=awaitreq.json();constrelay=newRelay('AppLead');constrelaySession=onRelay({name: 'AppLead',instructions: 'You are the customer-facing lead. Keep the user updated and delegate implementation via Relay when needed.',},relay);constmodel=wrapLanguageModel({model: openai('gpt-4o-mini'),middleware: relaySession.middleware,});if(escalate){constworkflow=awaitrunWorkflow('workflows/feature-dev.yaml',{vars: {task: prompt, repo },});returnResponse.json({status: workflow.status,runId: workflow.runId});}returnstreamText({
model,tools: relaySession.tools,system: 'Answer directly when possible; coordinate internally when the task needs specialists.',
prompt,}).toUIMessageStreamResponse({onFinish(){relaySession.cleanup();voidrelay.close();},});}

That pattern keeps the user experience snappy while still letting longer Relay workflows run with proper ownership, retries, and verification.

YAML Format

Workflows are defined as relay.yaml files:

version: "1.0"name: my-workflowdescription: "Optional description"swarm:
pattern: dag # Execution pattern (see Patterns below)maxConcurrency: 3# Max agents running in paralleltimeoutMs: 3600000# Global timeout (1 hour)channel: my-channel # Relay channel for agent communicationagents:
- name: backendcli: claude # claude | codex | gemini | aider | goose | opencode | droidrole: "Backend engineer"constraints:
model: opustimeoutMs: 600000retries: 2
- name: testercli: codexrole: "Test engineer"interactive: false # Non-interactive: runs as subprocess, no PTY/messaging# A persona replaces cli + role. Its harness, model, standing instructions,# installed skills, MCP servers, and harness settings come from the spec.# Persona agents are interactive-only: do not set cli, preset, or# constraints.model, and do not use interactive: false.
- name: integrationspersona: nango-integrationsworkflows:
- name: build-and-testonError: retry # fail | skip | retrysteps:
- name: build-apiagent: backendtask: "Build the REST API endpoints for user management"verification:
type: file_existsvalue: "src/api/users.ts"retries: 1
- name: write-testsagent: testertask: "Write integration tests for: {{steps.build-api.output}}"dependsOn: [build-api]
- name: run-testsagent: testertask: "Run the test suite and report results"dependsOn: [write-tests]verification:
type: exit_codevalue: "0"errorHandling:
strategy: retrymaxRetries: 2retryDelayMs: 5000repairAgent: testerrepairRetries: 2onExhaustion: needs-humannotifyChannel: my-channel

Template Variables

Use {{variable}} for user-provided values and {{steps.STEP_NAME.output}} for previous step outputs:

steps:
- name: planagent: plannertask: "Plan implementation for: {{task}}"# User variable
- name: implementagent: developerdependsOn: [plan]task: "Implement: {{steps.plan.output}}"# Previous step output

User variables are passed via the CLI or programmatically:

awaitrunWorkflow("workflow.yaml",{vars: {task: "Add OAuth2 support"},});

Blocking Slack Questions

Workflows can pause on a Slack question by adding a Slack integration step with action: askQuestion. The step posts a question, waits for the first human reply in the message thread, exposes that answer through {{steps.<name>.output}}, and can inject it into a running agent when injectToAgent is set.

workflows:
- name: defaultsteps:
- name: ask-humantype: integrationintegration: slackaction: askQuestionparams:
channel: "#engineering"text: "The implementer is blocked on migration strategy. Which path should it take?"waitTimeoutMs: "3600000"injectToAgent: "backend-runtime-name"injectTemplate: "HUMAN_ANSWER: {{answer.text}}"output: '{"format":"text","path":"answer.text"}'
- name: continue-with-answeragent: backenddependsOn: [ask-human]task: "Continue using this human guidance: {{steps.ask-human.output}}"

askQuestion can use the local Slack API runtime, or Relayfile-backed Slack writebacks when the workflow already has a Relayfile Slack integration.

Interactive agent steps can also opt into marker-driven assistance. With humanAssistance.slack enabled, an agent can print a line beginning with HUMAN_QUESTION:. The runner posts that question to Slack, blocks while waiting for a human reply, then injects HUMAN_ANSWER: ... back into that same agent session. If integrations.relayfile is present, Relayflows automatically uses the existing Relayfile/Pear Slack connection; no Slack bot token, Relayfile workspace id, or Relayfile token is required in the workflow.

Runnable TypeScript proof: examples/typescript/slack-human-assistance-e2e.ts.

swarm:
pattern: daghumanAssistance:
slack:
channel: proj-cloudtimeoutMs: 3600000integrations:
relayfile: {}workflows:
- name: defaultsteps:
- name: implementagent: backendtask: "Proceed, but ask for human guidance if the migration strategy is ambiguous."

Relayfile Event Subscriptions

Relayflows can subscribe to Relayfile integration events and inject matching events into active agents. Workflow-level subscriptions live under integrations.subscriptions; agent-level subscriptions use Workforce-style agents[].watch or Relayflows-style agents[].subscriptions.

integrations:
relayfile: {}subscriptions:
- name: pr-feedbackprovider: githubpath: /github/repos/acme/web/pulls/42/**events: [created, updated]agents: [pr-babysitter]agents:
- name: pr-babysittercli: codexwatch:
- paths: [/github/repos/acme/web/pulls/42/reviews/**]events: [created, updated]workflows:
- name: defaultsteps:
- name: babysit-pragent: pr-babysittertask: | Stay active and wait for INTEGRATION_EVENT messages about PR feedback. Read the Relayfile path from the event, address comments until no open feedback remains, then notify the user in Slack.

Verification Checks

Each step can include a verification check. Verification is one input to the runner's completion decision pipeline — when verification passes, the step completes even without a sentinel marker.

TypeDescription
exit_codeAgent must exit with the specified code (preferred for code-editing steps)
file_existsA file must exist at the specified path after the step
output_containsStep output must contain the specified string (optional accelerator)
customNo-op in the runner; handled by external callers
# Preferred — deterministic verificationverification:
type: exit_codevalue: "0"description: "Process exited successfully"# Also valid — output_contains as an optional acceleratorverification:
type: output_containsvalue: "IMPLEMENTATION_COMPLETE"description: "Agent confirms completion (optional fast-path)"

Completion Decision Pipeline

The runner uses a multi-signal pipeline to decide step completion:

  1. Deterministic verification — if a verification check passes, the step completes immediately (completed_verified)
  2. Owner decision — the step owner can issue OWNER_DECISION: COMPLETE|INCOMPLETE_RETRY|INCOMPLETE_FAIL (completed_by_owner_decision)
  3. Evidence-based completion — channel messages, file artifacts, and exit codes are collected as evidence (completed_by_evidence)
  4. Marker fast-pathSTEP_COMPLETE:<step-name> still works as an accelerator but is never required
Completion StateMeaning
completed_verifiedDeterministic verification passed
completed_by_owner_decisionOwner approved the step
completed_by_evidenceEvidence-based completion
retry_requested_by_ownerOwner requested retry
failed_verificationVerification explicitly failed
failed_owner_decisionOwner rejected the step
failed_no_evidenceNo verification, no owner decision, no evidence

Review parsing is tolerant: The runner accepts semantically equivalent outputs like "Approved", "Complete", "LGTM" — not just exact REVIEW_DECISION: APPROVE strings.

Swarm Patterns

The swarm.pattern field controls how agents are coordinated:

Core Patterns

PatternDescription
dagDirected acyclic graph — steps run based on dependency edges (default)
fan-outAll agents run in parallel
pipelineSequential chaining of steps
hub-spokeCentral hub coordinates spoke agents
consensusAgents vote on decisions
meshFull communication graph between agents
handoffSequential handoff between agents
cascadeWaterfall with phase gates
debateAgents propose and counter-argue
hierarchicalMulti-level reporting structure

Data Processing Patterns

PatternDescription
map-reduceSplit work into chunks (mappers), process in parallel, aggregate results (reducers)
scatter-gatherFan out requests to workers, collect and synthesize responses

Supervision & Quality Patterns

PatternDescription
supervisorMonitor agent monitors workers, restarts on failure, manages health
reflectionAgent produces output, critic reviews and provides feedback for iteration
verifierProducer agents submit work to verifier agents for validation

Adversarial & Validation Patterns

PatternDescription
red-teamAttacker agents probe for weaknesses, defender agents respond
auctionAuctioneer broadcasts tasks, agents bid based on capability/cost

Resilience Patterns

PatternDescription
escalationStart with fast/cheap agents, escalate to more capable on failure
sagaDistributed transactions with compensating actions on failure
circuit-breakerPrimary agent with fallback chain, fail fast and recover

Collaborative Patterns

PatternDescription
blackboardShared workspace where agents contribute incrementally to a solution
swarmEmergent behavior from simple agent rules (neighbor communication)

Auto-Selection by Role

When swarm.pattern is omitted, the coordinator auto-selects based on agent roles. Patterns are checked in priority order below (first match wins):

PriorityPatternRequired Roles/Config
1dagSteps with dependsOn
2consensusUses coordination.consensusStrategy config
3map-reducemapper + reducer
4red-team(attacker OR red-team) + (defender OR blue-team)
5reflectioncritic
6escalationtier-1, tier-2, etc.
7auctionauctioneer
8sagasaga-orchestrator OR compensate-handler
9circuit-breakerfallback, backup, OR primary
10blackboardblackboard OR shared-workspace
11swarmhive-mind OR swarm-agent
12verifierverifier
13supervisorsupervisor
14hierarchicallead (with 4+ agents)
15hub-spokehub OR coordinator
16pipelineUnique agents per step, 3+ steps
17fan-outDefault fallback

Error Handling

Step-Level

steps:
- name: risky-stepagent: workertask: "Do something that might fail"retries: 3# Retry up to 3 times on failuretimeoutMs: 300000# 5 minute timeout

Successful early termination

A deterministic gate can explicitly declare exit codes that mean “there is no work to do.” A matching code ends the run with the distinct completed_early status and skips every step that has not started:

steps:
- name: claim-worktype: deterministiccommand: node bin/claim-work.mjsterminalSuccessExitCodes: [78]
- name: process-claimagent: workertask: Process the claimed workdependsOn: [claim-work]

Terminal-capable gates are scheduling barriers, so other ready work does not race the gate. The triggering step is completed with completion reason completed_early_exit; remaining steps are skipped, and the CLI exits 0 while clearly reporting COMPLETED EARLY. Verification still applies, so a verification failure remains a real failure.

This behavior is opt-in. Without terminalSuccessExitCodes, exit 78 and every other non-zero exit retain their existing failure behavior. The new completed_early run status is an additive public API value: consumers with exhaustive status switches, strict validators, database constraints, or terminal-status polling must handle it separately from completed.

Workflow-Level

The onError field on a workflow controls what happens when a step fails:

ValueBehavior
fail / fail-fastStop immediately, skip downstream steps
skip / continueSkip downstream dependents, continue independent steps
retryRetry the step; deterministic gates ask a workflow agent to repair before each retry when an agent is available

Global

errorHandling:
strategy: retrymaxRetries: 2retryDelayMs: 5000repairAgent: testerrepairRetries: 2notifyChannel: alerts

Retry-mode workflows are repair-aware by default. Deterministic step failures, verification gate failures, and malformed agent artifacts are treated as repairable work before terminal failure. The runner chooses errorHandling.repairAgent when set, otherwise it uses the step's owning/upstream agent when possible, then falls back to the best available workflow agent. The selected agent gets the failed command or agent output, working directory, exit information, and captured evidence, then the failed gate or step is retried. Use repairRetries: 0, strategy: fail-fast, or strategy: continue when a workflow intentionally should not invoke repair agents. Set onExhaustion: needs-human to end an exhausted repairable run as needs_human instead of failed.

Built-in Templates

Six pre-built workflow templates are included:

TemplatePatternDescription
feature-devhub-spokePlan, implement, review, and finalize a feature
bug-fixhub-spokeInvestigate, patch, validate, and document a bug fix
code-reviewfan-outParallel multi-reviewer assessment with consolidated findings
security-auditpipelineScan, triage, remediate, and verify security issues
refactorhierarchicalAnalyze, plan, execute, and validate a refactor
documentationhandoffResearch, draft, review, and publish documentation

Using Templates

import{TemplateRegistry,WorkflowRunner}from"@relayflows/core";constregistry=newTemplateRegistry();// List available templatesconsttemplates=awaitregistry.listTemplates();// Load and run a templateconstconfig=awaitregistry.loadTemplate("feature-dev");construnner=newWorkflowRunner();constresult=awaitrunner.execute(config,undefined,{task: "Add WebSocket support to the API",});// Install a custom template from a URLawaitregistry.installExternalTemplate("https://example.com/my-template.yaml","my-template");

TypeScript Builder API

The builder constructs a RelayYamlConfig object and can run it, export it as YAML, or return the raw config.

import{workflow}from"@relayflows/core";// Build and runconstresult=awaitworkflow("my-workflow").pattern("dag").maxConcurrency(3).timeout(60*60*1000).channel("my-channel").agent("backend",{cli: "claude",role: "Backend engineer",model: "opus",retries: 2,}).agent("frontend",{cli: "codex",role: "Frontend engineer",interactive: false,// Non-interactive subprocess mode}).step("api",{agent: "backend",task: "Build REST API",verification: {type: "output_contains",value: "API_READY"},}).step("ui",{agent: "frontend",task: "Build the UI",dependsOn: ["api"],}).onError("retry",{maxRetries: 2,retryDelayMs: 5000}).run();// Or export to YAMLconstyaml=workflow("my-workflow").pattern("dag").agent("worker",{cli: "claude"}).step("task1",{agent: "worker",task: "Do something"}).toYaml();// Or get the raw config objectconstconfig=workflow("my-workflow").pattern("dag").agent("worker",{cli: "claude"}).step("task1",{agent: "worker",task: "Do something"}).toConfig();

Python Builder API

The Python builder ships with @agent-relay/sdk-py:

pip install agent-relay
fromagent_relayimportworkflow, run_yaml# Build and runresult= (
workflow("my-workflow")
.pattern("dag")
.max_concurrency(3)
.timeout(3600000)
.agent("backend", cli="claude", role="Backend engineer")
.agent("frontend", cli="codex", role="Frontend engineer")
.step("api", agent="backend", task="Build REST API")
.step("ui", agent="frontend", task="Build the UI", depends_on=["api"])
.on_error("retry", max_retries=2, retry_delay_ms=5000)
.run()
)
# Run an existing YAML fileresult=run_yaml("workflows/my-workflow.yaml")
# Export to YAML stringyaml_str= (
workflow("my-workflow")
.pattern("dag")
.agent("worker", cli="claude")
.step("task1", agent="worker", task="Do something")
.to_yaml()
)
# Get the raw config dictconfig= (
workflow("my-workflow")
.pattern("dag")
.agent("worker", cli="claude")
.step("task1", agent="worker", task="Do something")
.to_config()
)

Programmatic API

For full control, use the WorkflowRunner directly:

import{WorkflowRunner}from"@relayflows/core";construnner=newWorkflowRunner({cwd: "/path/to/project",// Working directory (default: process.cwd())relay: {port: 3000},// AgentRelay options (optional)});// Listen to events (broker:event fires frequently — filter it out for cleaner output)runner.on((event)=>{if(event.type==='broker:event')return;console.log(event.type,event);});// Parse and executeconstconfig=awaitrunner.parseYamlFile("workflow.yaml");construn=awaitrunner.execute(config,"workflow-name",{task: "Build the feature",});// Pause / resume / abortrunner.pause();runner.unpause();runner.abort();// Resume a failed runconstresumed=awaitrunner.resume(run.id);

Zero-Config Convenience Function

import{runWorkflow}from"@relayflows/core";constresult=awaitrunWorkflow("workflow.yaml",{workflow: "deploy",vars: {environment: "staging"},onEvent: (event)=>{if(event.type!=='broker:event')console.log(event.type);},});

Coordination

Barriers

Synchronization points that wait for specific steps to complete:

coordination:
barriers:
- name: all-reviews-donewaitFor: [review-arch, review-security, review-correctness]timeoutMs: 900000consensusStrategy: majority # majority | unanimous | quorum

Shared State

Agents can share state during execution:

state:
backend: memory # memory | redis | databasettlMs: 86400000namespace: my-workflow

Supported Agent CLIs

CLIDescription
claudeClaude Code (Anthropic)
codexCodex CLI (OpenAI)
geminiGemini CLI (Google)
aiderAider coding assistant
gooseGoose AI assistant
opencodeOpenCode CLI
droidDroid CLI

Non-Interactive Agents

By default, agents run in interactive PTY mode with full relay messaging. For workers that just need to execute a task and return output — common in fan-out, map-reduce, and pipeline patterns — set interactive: false to run them as lightweight subprocesses.

YAML

agents:
- name: leadcli: clauderole: "Coordinates work"# interactive: true (default) — full PTY, relay messaging, /exit detection
- name: workercli: codexrole: "Executes tasks"interactive: false # Runs "codex exec <task>", captures stdout

TypeScript

workflow("fan-out-analysis").pattern("fan-out").agent("lead",{cli: "claude",role: "Coordinator"}).agent("worker-1",{cli: "codex",interactive: false,role: "Analyst"}).agent("worker-2",{cli: "codex",interactive: false,role: "Analyst"}).step("analyze-1",{agent: "worker-1",task: "Analyze module A"}).step("analyze-2",{agent: "worker-2",task: "Analyze module B"}).step("synthesize",{agent: "lead",task: "Combine: {{steps.analyze-1.output}} + {{steps.analyze-2.output}}",dependsOn: ["analyze-1","analyze-2"],}).run();

How It Works

AspectInteractive (default)Non-Interactive
ExecutionFull PTY with stdin/stdoutchild_process.spawn() with piped stdio
CLI invocationStandard interactive sessionOne-shot mode (claude -p, codex exec, etc.)
Relay messagingCan send/receive messagesNo messaging — excluded from topology edges
Self-terminationMust output /exitProcess exits naturally when done
Output capturePTY output bufferstdout capture
OverheadHigher (PTY, echo verification, SIGWINCH)Lower (simple subprocess)

Non-Interactive CLI Commands

CLICommandNotes
claudeclaude -p "<task>"Print mode, exits after response
codexcodex exec "<task>"One-shot execution
geminigemini -p "<task>"Prompt mode
opencodeopencode --prompt "<task>"One-shot prompt
droiddroid exec "<task>"One-shot execution
aideraider --message "<task>" --yes-always --no-gitAuto-approve, skip git
goosegoose run --text "<task>" --no-sessionText mode, no session file

When to Use

  • Fan-out workers that process a task and return results
  • Map-reduce mappers that don't need mid-task communication
  • Pipeline stages that transform input to output
  • Any agent that doesn't need turn-by-turn relay messaging

When NOT to Use

  • Lead/coordinator agents that communicate with others
  • Agents in debate, consensus, or reflection patterns
  • Agents that need to receive messages during execution

Agent Slash Commands

Agents running inside a workflow can output slash commands to signal the broker. These are detected in the agent's PTY output at the broker level — the agent simply prints the command on its own line.

/exit

Signals that the agent has completed its current step and is ready to be released.

/exit

The workflow runner waits for each agent to /exit after delivering a step task. When the broker detects /exit in the agent's output (exact line match after ANSI stripping), it:

  1. Emits an agent_exit frame with reason: "agent_requested"
  2. Triggers graceful PTY shutdown

If an agent does not /exit within the step's timeoutMs, the runner treats the step as timed out. As a safety net, steps with file_exists verification will still pass if the expected file is present despite the timeout.

Best practice: Instruct agents to output /exit when done in your step task descriptions:

steps:
- name: build-apiagent: backendtask: | Build the REST API endpoints for user management. When finished, output /exit.

Idle Agent Detection and Nudging

Interactive agents sometimes finish their task but forget to /exit, sitting idle and blocking downstream steps. The runner can detect idle agents and take action automatically.

Configuration

Add idleNudge to your swarm config:

swarm:
pattern: hub-spokeidleNudge:
nudgeAfterMs: 120000# 2 min before first nudge (default)escalateAfterMs: 120000# 2 min after nudge before force-release (default)maxNudges: 1# Nudges before escalation (default)

All built-in templates include idle nudging with these defaults.

How It Works

  1. Detection: The broker tracks agent output timestamps and emits agent_idle events when an agent goes silent for the configured threshold
  2. Nudge: For hub patterns (hub-spoke, fan-out, hierarchical, etc.), the runner tells the hub agent to check on the idle agent. For non-hub patterns, a system message is injected directly into the agent's PTY
  3. Escalation: If the agent remains idle after maxNudges attempts, the runner force-releases it and captures whatever output was produced
  4. No config: When idleNudge is omitted, the runner uses simple waitForExit (backward compatible)

Events

The runner emits two new events for idle nudging:

EventDescription
step:nudgedFired when a nudge message is sent to an idle agent
step:force-releasedFired when an agent is force-released after exhausting nudges

Automatic Step Owner and Review

For interactive agent steps, the runner uses a point-person-led completion model:

  1. Elects a step owner (prefers lead/coordinator-style agents, falls back to the step agent)
  2. Runs a completion decision pipeline — checks deterministic verification first, then owner judgment, then evidence
  3. Owner can issue structured decisions via OWNER_DECISION: COMPLETE|INCOMPLETE_RETRY|INCOMPLETE_FAIL|NEEDS_CLARIFICATION with optional REASON: <text>
  4. Review parsing is tolerant — accepts "Approved", "Complete", "LGTM", not just exact REVIEW_DECISION: APPROVE
  5. Markers are optional acceleratorsSTEP_COMPLETE:<step-name> still works as a fast-path but is never required
  6. Stores primary output plus review output in the step artifact

Evidence-based completion: The runner collects channel messages, file artifacts, process exit codes, and coordination signals (e.g., WORKER_DONE posted in channel) as completion evidence. When sufficient evidence exists, the step completes without requiring any sentinel marker.

Deterministic and worktree steps are unchanged and do not require owner/review delegation.

Sandbox Execution

By default the runner spawns steps as local child processes. To run them in isolated sandboxes instead, select a provider — the runner still owns command construction, env, cwd, timeout, and the whole DAG/retry/verification pipeline; the provider only supplies "where the command runs".

# Off by default. Unset the flag to get local child processes back.export RELAYFLOWS_SANDBOX_PROVIDER=daytona
export DAYTONA_API_KEY=...
export RELAYFLOWS_SANDBOX_HOME_DIR=/home/daytona # image-specific, requiredexport RELAYFLOWS_SANDBOX_SNAPSHOT=my-snapshot # optional

Or in code:

import{WorkflowRunner}from"@relayflows/core";construnner=newWorkflowRunner({sandbox: {provider: "daytona",homeDir: "/home/daytona"},});
ProviderWhat it gives you
none (default)No sandbox. Local child processes, exactly as before.
daytonaReal remote sandboxes via @agent-relay/sandbox. Needs the optional peer @daytonaio/sdk.
local-processReal local processes in a private per-step directory with its own HOME. Isolates the filesystem root, not the machine — good for development and CI, not a security boundary.

Reversibility.provider: "none" (or an unset RELAYFLOWS_SANDBOX_PROVIDER) produces no backend at all, so nothing about the default path changes. An explicit executor or processBackend still wins over sandbox config, so a host that injects its own backend today keeps it.

Custom providers. Register a runtime under any name, or hand one in directly. This is the seam a host uses to plug in a runtime that does not live in this repo:

import{registerSandboxProvider,WorkflowRunner}from"@relayflows/core";registerSandboxProvider("my-runtime",(config)=>newMyRuntime(config));// ...or skip the registry entirely:newWorkflowRunner({sandbox: {runtime: myRuntime}});

A runtime needs five methods — launch, exec, uploadFile, getHomeDir, destroy — matching @agent-relay/sandbox's WorkflowRuntime.

Schema Validation

A JSON Schema is available at packages/core/src/schema.json for editor autocompletion and validation of relay.yaml files.

Development

npm install
npm run typecheck
npm run test

Requirements

  • Node.js 22+
  • @relayflows/cli installed (npm install -g @relayflows/cli)
  • For Python: Python 3.10+ with pip install agent-relay
  • For TypeScript workflow files: tsx or ts-node installed

License

Apache-2.0 — Copyright 2025 Agent Workforce Incorporated

About

Orchestrate multi-step, multi-agent execution across Agent Relay workers

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages