Use when building multi-agent workflows with @relayflows/core. Covers conversation vs pipeline coordination, WorkflowBuilder/DAG steps, agents, {{steps.X.output}} chaining, repairable verification gates, evidence-based completion, mandatory Claude-then-Codex fresh-eyes review/fix loops with test hardening, channels, chat-native recipes, error handling, event listeners, step sizing, lead+workers teams, and parallel waves.
npx skills add https://github.com/AgentWorkforce/relay --skill writing-agent-relay-workflows
The @relayflows/core workflow system orchestrates multiple AI agents (Claude, Codex, Gemini, Aider, Goose) through typed DAG-based workflows. Workflows can be written in TypeScript (preferred), Python, or YAML.
Language preference: TypeScript > Python > YAML. Use TypeScript unless the project is Python-only or a simple config-driven workflow suits YAML.
Pattern selection: Do not default to dag blindly. If the job needs a different swarm/workflow type, consult the choosing-swarm-patterns skill when available and select the pattern that best matches the coordination problem.
Every generated workflow should satisfy this checklist before it is considered complete:
failOnError: false, hand it to a repair owner, then rerun the same check.BLOCKED_NO_COMMIT with exact evidence and skip commit/PR creation instead of crashing the workflow.captureOutput: true.failOnError: false for intermediate validation gates so the workflow can pass the output to a repair agent.{{steps.<gate>.output}}, fixes source/tests/config, reruns the same command locally, and exits only after the gate is green or the blocker is external.FAILED..reliable() or .repairable() on SDK versions that support it, especially for product-contract workflows. As of AgentWorkforce/relay#827, retry-mode workflows with agents are repair-aware by default, repair agents run before retrying malformed/failed agent steps, and the SDK covers DAG, pipeline, fan-out, worktree-backed, deterministic-only, and agent-plus-gate shapes.verdict: FINDINGS | NO_ISSUES_FOUND | BLOCKED
finding_id: short stable id
severity: blocker | high | medium | low
file: path/to/file
issue: what is wrong
fix_required: concrete change needed
test_required: test, fixture, assertion, or proof command needed
status: open | fixed | wontfix | blocked
evidence: commands run, file paths, or blocker details
Before writing the workflow, decide _how the agents will coordinate_. The relay primitive supports two very different shapes, and picking the wrong one wastes the most valuable thing the SDK gives you.
| Shape | What it is | Use when |
| ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Conversation (chat-native) | Interactive agents share a channel; messages, @-mentions, and ambient awareness drive coordination. Lead and workers spawn in parallel and self-organize. The relay is the coordination layer, not just transport. | Multi-file work, peer review loops, cross-agent feedback, dynamic re-planning, multi-PR coordination, anything with a human-in-the-loop escape, swarms where workers pick up each other's output. |
| Pipeline (one-shot DAG) | Each step runs as a one-shot subprocess (claude -p, codex exec); steps hand off via {{steps.X.output}} text injection. No agents are alive at the same time; no chat happens. | Linear, well-specified transformations; deterministic data passing; no live agent-to-agent coordination during implementation. The mandatory final Claude-then-Codex review/fix loops still apply. |
Default to Conversation for any non-trivial work. Pipeline DAGs are simpler to reason about but they do not exercise the relay primitive — they are a Unix pipe with extra steps. If you would happily write the same task as a single shell pipeline, pipeline-shape is fine. Otherwise, you almost certainly want a Conversation shape.
The two shapes can mix within one workflow: pipeline-style deterministic preflight → conversation in the middle → pipeline-style commit-and-PR at the end. See Quick Reference (Conversation) below and Common Patterns → Interactive Team for the canonical recipe.
> A blunt rule of thumb: if your workflow only uses agent steps with preset: 'worker' chained by {{steps.X.output}}, you are not using the relay — you are using claude -p | codex exec. That may still be the right answer; just make it a deliberate choice.
import { workflow } from '@relayflows/core';
async function runWorkflow() {
const result = await workflow('my-workflow')
.description('What this workflow does')
.pattern('dag') // or 'pipeline', 'fan-out', etc.
.channel('wf-my-workflow') // dedicated channel (auto-generated if omitted)
.maxConcurrency(3)
.timeout(3_600_000) // global timeout (ms)
.repairable()
.agent('lead', { cli: 'claude', role: 'Architect', retries: 2 })
.agent('worker', { cli: 'codex', role: 'Implementer', retries: 2 })
.agent('claude-reviewer', {
cli: 'claude',
role: 'First-pass fresh-eyes reviewer',
retries: 1,
preset: 'reviewer',
})
.agent('claude-fixer', { cli: 'claude', role: 'First-pass review-finding fixer', retries: 2 })
.agent('codex-reviewer', {
cli: 'codex',
role: 'Second-pass fresh-eyes reviewer',
retries: 1,
preset: 'reviewer',
})
.agent('codex-fixer', { cli: 'codex', role: 'Review-finding fixer', retries: 2 })
.step('preflight', {
type: 'deterministic',
command: 'git rev-parse --show-toplevel >/dev/null && echo PREFLIGHT_OK',
captureOutput: true,
failOnError: true,
})
.step('plan', {
agent: 'lead',
dependsOn: ['preflight'],
task: `Analyze the codebase and produce a plan.`,
retries: 2,
verification: { type: 'output_contains', value: 'PLAN_COMPLETE' },
})
.step('implement', {
agent: 'worker',
task: `Implement based on this plan:\n{{steps.plan.output}}`,
dependsOn: ['plan'],
verification: { type: 'exit_code' },
})
.step('claude-review', {
agent: 'claude-reviewer',
dependsOn: ['implement'],
task: `Fresh-eyes review the completed workflow output. Read the actual files, diff, repo rules, and available evidence.
Write findings to .workflow-artifacts/my-workflow/claude-review.md.
If there are no actionable issues, write NO_ISSUES_FOUND.`,
verification: { type: 'exit_code' },
})
.step('claude-fix', {
agent: 'claude-fixer',
dependsOn: ['claude-review'],
task: `Read .workflow-artifacts/my-workflow/claude-review.md.
Fix every valid issue, add or update appropriate tests/proofs for the fix, rerun relevant checks, and update .workflow-artifacts/my-workflow/claude-fix.md.
If the review says NO_ISSUES_FOUND, record that no fix was needed.`,
verification: { type: 'exit_code' },
})
.step('claude-review-final', {
agent: 'claude-reviewer',
dependsOn: ['claude-fix'],
task: `Fresh-eyes review the post-fix state from scratch. Do not rely on the prior review or fix summary.
Write .workflow-artifacts/my-workflow/claude-review-final.md with either actionable findings or NO_ISSUES_FOUND.`,
verification: { type: 'exit_code' },
})
.step('claude-fix-final', {
agent: 'claude-fixer',
dependsOn: ['claude-review-final'],
task: `If .workflow-artifacts/my-workflow/claude-review-final.md contains findings, fix them, add or update appropriate tests/proofs, and rerun relevant checks.
If no fix is possible, write .workflow-artifacts/my-workflow/BLOCKED_NO_COMMIT.md with exact evidence.
If it says NO_ISSUES_FOUND, record Claude review signoff.`,
verification: { type: 'exit_code' },
})
.step('codex-review', {
agent: 'codex-reviewer',
dependsOn: ['claude-fix-final'],
task: `Second-pass fresh-eyes review of the post-Claude-fix state. Read the actual files, diff, repo rules, and available evidence.
Write findings to .workflow-artifacts/my-workflow/codex-review.md.
If there are no actionable issues, write NO_ISSUES_FOUND.`,
verification: { type: 'exit_code' },
})
.step('codex-fix', {
agent: 'codex-fixer',
dependsOn: ['codex-review'],
task: `Read .workflow-artifacts/my-workflow/codex-review.md.
Fix every valid issue, add or update appropriate tests/proofs for the fix, rerun relevant checks, and update .workflow-artifacts/my-workflow/codex-fix.md.
If the review says NO_ISSUES_FOUND, record that no fix was needed.`,
verification: { type: 'exit_code' },
})
.step('codex-review-final', {
agent: 'codex-reviewer',
dependsOn: ['codex-fix'],
task: `Fresh-eyes review the post-Codex-fix state from scratch. Do not rely on the prior review or fix summary.
Write .workflow-artifacts/my-workflow/codex-review-final.md with either actionable findings or NO_ISSUES_FOUND.`,
verification: { type: 'exit_code' },
})
.step('codex-fix-final', {
agent: 'codex-fixer',
dependsOn: ['codex-review-final'],
task: `If .workflow-artifacts/my-workflow/codex-review-final.md contains findings, fix them, add or update appropriate tests/proofs, and rerun relevant checks.
If no fix is possible, write .workflow-artifacts/my-workflow/BLOCKED_NO_COMMIT.md with exact evidence.
If it says NO_ISSUES_FOUND, record final review signoff.`,
verification: { type: 'exit_code' },
})
.step('acceptance-after-review', {
type: 'deterministic',
dependsOn: ['codex-fix-final'],
command: 'test ! -f .workflow-artifacts/my-workflow/BLOCKED_NO_COMMIT.md && echo ACCEPTANCE_OK',
captureOutput: true,
failOnError: true,
})
.onError('retry', { maxRetries: 2, retryDelayMs: 10_000 })
.run({ cwd: process.cwd() });
console.log('Result:', result.status);
}
runWorkflow().catch((error) => {
console.error(error);
process.exit(1);
});
import { workflow } from '@relayflows/core';
import { ClaudeModels, CodexModels } from '@agent-relay/config';
async function runWorkflow() {
const result = await workflow('my-workflow')
.description('Multi-file change with peer review')
.pattern('dag')
.channel('wf-my-feature') // dedicated channel — agents share it
.maxConcurrency(4)
.timeout(3_600_000)
.repairable()
// Interactive agents — no preset, they live on the channel
.agent('lead', {
cli: 'claude',
model: ClaudeModels.OPUS,
role: 'Architect + reviewer. Plans, assigns, reviews, posts feedback.',
retries: 1,
})
.agent('impl-a', {
cli: 'codex',
model: CodexModels.GPT_5_4,
role: 'Implementer. Listens on channel for assignments and feedback.',
retries: 2,
})
.agent('impl-b', {
cli: 'codex',
model: CodexModels.GPT_5_4,
role: 'Implementer. Listens on channel for assignments and feedback.',
retries: 2,
})
.agent('claude-reviewer', {
cli: 'claude',
model: ClaudeModels.OPUS,
preset: 'reviewer',
role: 'First-pass fresh-eyes reviewer. Reads the final diff and artifacts from scratch.',
retries: 1,
})
.agent('claude-fixer', {
cli: 'claude',
model: ClaudeModels.SONNET,
role: 'First-pass review-finding fixer. Repairs valid findings, adds tests/proofs, and reruns checks.',
retries: 2,
})
.agent('codex-reviewer', {
cli: 'codex',
model: CodexModels.GPT_5_4,
preset: 'reviewer',
role: 'Second-pass fresh-eyes reviewer. Reviews the post-Claude-fix state from scratch.',
retries: 1,
})
.agent('codex-fixer', {
cli: 'codex',
model: CodexModels.GPT_5_4,
role: 'Review-finding fixer. Repairs valid findings, adds tests/proofs, and reruns checks.',
retries: 2,
})
// Deterministic context — pre-reads files once, posts to the channel for everyone
.step('preflight', {
type: 'deterministic',
command: 'git rev-parse --show-toplevel >/dev/null && echo PREFLIGHT_OK',
captureOutput: true,
failOnError: true,
})
.step('context', {
type: 'deterministic',
dependsOn: ['preflight'],
command: 'git ls-files src/',
captureOutput: true,
})
// Lead and workers all depend on `context` — they start CONCURRENTLY.
// They coordinate over #wf-my-feature, not via {{steps.X.output}}.
.step('lead-coordinate', {
agent: 'lead',
dependsOn: ['context'],
task: `You are the lead on #wf-my-feature. Workers: impl-a, impl-b.
Post the plan. Assign files. Review their PRs/diffs. Post feedback in-channel.
Workers iterate based on your feedback. Exit when both files pass review.`,
})
.step('impl-a-work', {
agent: 'impl-a',
dependsOn: ['context'], // SAME dep as lead → starts in parallel, no deadlock
task: `You are impl-a on #wf-my-feature. Wait for the lead's plan.
Implement your assigned file. Post a completion message. Address feedback.`,
})
.step('impl-b-work', {
agent: 'impl-b',
dependsOn: ['context'], // SAME dep as lead
task: `You are impl-b on #wf-my-feature. Wait for the lead's plan.
Implement your assigned file. Post a completion message. Address feedback.`,
})
// Downstream gates on the lead — lead exits when satisfied.
// Capture failures, then hand them to an agent for repair.
.step('verify', {
type: 'deterministic',
dependsOn: ['lead-coordinate'],
command: 'npm run typecheck && npm test 2>&1',
captureOutput: true,
failOnError: false,
})
.step('repair-verify', {
agent: 'lead',
dependsOn: ['verify'],
task: `If verification passed, summarize evidence.
If it failed, use this output to assign and fix issues, then rerun the command until green:
{{steps.verify.output}}`,
verification: { type: 'exit_code' },
})
.step('verify-final', {
type: 'deterministic',
dependsOn: ['repair-verify'],
command: 'npm run typecheck && npm test 2>&1',
captureOutput: true,
failOnError: false,
})
.step('claude-review', {
agent: 'claude-reviewer',
dependsOn: ['verify-final'],
task: `First-pass fresh-eyes review of the post-implementation state.
Read the actual changed files, git diff, repo instructions, task spec, and verification output:
{{steps.verify-final.output}}
Write .workflow-artifacts/my-feature/claude-review.md with:
- actionable findings, each with file paths and required fix
- or NO_ISSUES_FOUND if there are no remaining issues`,
verification: { type: 'exit_code' },
})
.step('claude-fix', {
agent: 'claude-fixer',
dependsOn: ['claude-review'],
task: `Read .workflow-artifacts/my-feature/claude-review.md.
If there are findings, fix every valid one and add or update appropriate tests/proofs. After each fix, rerun the relevant check and review the changed files again.
Keep iterating locally until this round has no remaining valid issues.
Write .workflow-artifacts/my-feature/claude-fix.md with fixes and commands run.
If the review says NO_ISSUES_FOUND, write that no fix was needed.`,
verification: { type: 'exit_code' },
})
.step('claude-review-final', {
agent: 'claude-reviewer',
dependsOn: ['claude-fix'],
task: `Perform a fresh post-fix review from scratch. Do not rely on previous review text or the fixer's summary.
Read files, diff, repo rules, task spec, and evidence. Write .workflow-artifacts/my-feature/claude-review-final.md.
Use NO_ISSUES_FOUND only if there are no actionable issues left.`,
verification: { type: 'exit_code' },
})
.step('claude-fix-final', {
agent: 'claude-fixer',
dependsOn: ['claude-review-final'],
task: `If the final Claude review found issues, fix them, add or update appropriate tests/proofs, and rerun the relevant checks until green.
If no fix is possible, write .workflow-artifacts/my-feature/BLOCKED_NO_COMMIT.md with exact evidence and do not commit.
If the final review says NO_ISSUES_FOUND, record signoff in .workflow-artifacts/my-feature/claude-signoff.md.`,
verification: { type: 'exit_code' },
})
.step('verify-after-claude-review', {
type: 'deterministic',
dependsOn: ['claude-fix-final'],
command:
'test ! -f .workflow-artifacts/my-feature/BLOCKED_NO_COMMIT.md && npm run typecheck && npm test 2>&1',
captureOutput: true,
failOnError: false,
})
.step('codex-review', {
agent: 'codex-reviewer',
dependsOn: ['verify-after-claude-review'],
task: `Second-pass fresh-eyes review of the post-Claude-fix state.
Read the actual changed files, git diff, repo instructions, task spec, and verification output:
{{steps.verify-after-claude-review.output}}
Write .workflow-artifacts/my-feature/codex-review.md with:
- actionable findings, each with file paths and required fix
- or NO_ISSUES_FOUND if there are no remaining issues`,
verification: { type: 'exit_code' },
})
.step('codex-fix', {
agent: 'codex-fixer',
dependsOn: ['codex-review'],
task: `Read .workflow-artifacts/my-feature/codex-review.md.
If there are findings, fix every valid one and add or update appropriate tests/proofs. After each fix, rerun the relevant check and review the changed files again.
Keep iterating locally until this round has no remaining valid issues.
Write .workflow-artifacts/my-feature/codex-fix.md with fixes and commands run.
If the review says NO_ISSUES_FOUND, write that no fix was needed.`,
verification: { type: 'exit_code' },
})
.step('codex-review-final', {
agent: 'codex-reviewer',
dependsOn: ['codex-fix'],
task: `Perform a fresh post-Codex-fix review from scratch. Do not rely on previous review text or the fixer's summary.
Read files, diff, repo rules, task spec, and evidence. Write .workflow-artifacts/my-feature/codex-review-final.md.
Use NO_ISSUES_FOUND only if there are no actionable issues left.`,
verification: { type: 'exit_code' },
})
.step('codex-fix-final', {
agent: 'codex-fixer',
dependsOn: ['codex-review-final'],
task: `If the final Codex review found issues, fix them, add or update appropriate tests/proofs, and rerun the relevant checks until green.
If no fix is possible, write .workflow-artifacts/my-feature/BLOCKED_NO_COMMIT.md with exact evidence and do not commit.
If the final review says NO_ISSUES_FOUND, record signoff in .workflow-artifacts/my-feature/codex-signoff.md.`,
verification: { type: 'exit_code' },
})
.step('verify-after-review', {
type: 'deterministic',
dependsOn: ['codex-fix-final'],
command:
'test ! -f .workflow-artifacts/my-feature/BLOCKED_NO_COMMIT.md && npm run typecheck && npm test 2>&1',
captureOutput: true,
failOnError: true,
})
.onError('retry', { maxRetries: 2, retryDelayMs: 10_000 })
.run({ cwd: process.cwd() });
console.log('Result:', result.status);
}
runWorkflow().catch((error) => {
console.error(error);
process.exit(1);
});
package.json for "type": "module" — if ESM, use import; if CJS, use require(). In both cases, wrap execution in an async function instead of raw top-level await.agent-relay node workflow run <file.ts> executes the file as a standalone subprocess — it does NOT inspect exports. The file MUST call .run()..run({ cwd: process.cwd() }) — createWorkflowRenderer does not exist.run({ dryRun: true, cwd: process.cwd() }) or runWorkflow(path, { dryRun: true }) from TypeScript. Use agent-relay node workflow run <file> for execution.# BAD — sequential (14 hours for 27 workflows at ~30 min each)
agent-relay node workflow run workflows/34-sst-wiring.ts
agent-relay node workflow run workflows/35-env-config.ts
agent-relay node workflow run workflows/36-loading-states.ts
# ... one at a time
# GOOD — parallel waves (3-4 hours for 27 workflows)
# Wave 1: independent infra (parallel)
agent-relay node workflow run workflows/34-sst-wiring.ts &
agent-relay node workflow run workflows/35-env-config.ts &
agent-relay node workflow run workflows/36-loading-states.ts &
agent-relay node workflow run workflows/37-responsive.ts &
wait
git add -A && git commit -m "Wave 1"
# Wave 2: testing (parallel — independent test suites)
agent-relay node workflow run workflows/40-unit-tests.ts &
agent-relay node workflow run workflows/41-integration-tests.ts &
agent-relay node workflow run workflows/42-e2e-tests.ts &
wait
git add -A && git commit -m "Wave 2"
workflow('48-comparison-mode')
.packages(['web', 'core']) // monorepo packages touched
.isolatedFrom(['49-feedback-system']) // explicitly safe to parallelize
.requiresBefore(['46-admin-dashboard']); // explicit ordering constraint
// BAD — unnecessary sequential chain
.step('fix-component-a', { agent: 'worker', dependsOn: ['review'] })
.step('fix-component-b', { agent: 'worker', dependsOn: ['fix-component-a'] }) // why wait?
// GOOD — parallel fan-out, merge at the end
.step('fix-component-a', { agent: 'impl-1', dependsOn: ['review'] })
.step('fix-component-b', { agent: 'impl-2', dependsOn: ['review'] }) // same dep = parallel
.step('verify-all', { agent: 'reviewer', dependsOn: ['fix-component-a', 'fix-component-b'] })
awaitasync function runWorkflow() {
const result = await workflow('my-workflow')
// ...
.run({ cwd: process.cwd() });
console.log('Workflow status:', result.status);
}
runWorkflow().catch((error) => {
console.error(error);
process.exit(1);
});
.step('preflight', {
type: 'deterministic',
command: [
'set -e',
'BRANCH=$(git rev-parse --abbrev-ref HEAD)',
'echo "branch: $BRANCH"',
'if [ "$BRANCH" != "fix/your-branch-name" ]; then echo "ERROR: wrong branch"; exit 1; fi',
// Files the workflow is allowed to find dirty on entry:
// - package-lock.json: npm install is idempotent and often touches it
// - every file the workflow's edit steps will rewrite: a prior partial
// run may have left them dirty, and the edit step will rewrite
// them cleanly before commit
// Everything else is unexpected drift and must fail preflight.
'ALLOWED_DIRTY="package-lock.json|path/to/file1\\\\.ts|path/to/file2\\\\.ts"',
'DIRTY=$(git diff --name-only | grep -vE "^(${ALLOWED_DIRTY})$" || true)',
'if [ -n "$DIRTY" ]; then echo "ERROR: unexpected tracked drift:"; echo "$DIRTY"; exit 1; fi',
'if ! git diff --cached --quiet; then echo "ERROR: staging area is dirty"; git diff --cached --stat; exit 1; fi',
'gh auth status >/dev/null 2>&1 || (echo "ERROR: gh CLI not authenticated"; exit 1)',
'echo PREFLIGHT_OK',
].join(' && '),
captureOutput: true,
failOnError: true,
}),
.join() for multi-line shell commandscommand: [
'set -e',
'HITS=$(grep -c diag src/cli/commands/setup.ts || true)',
'if [ "$HITS" -lt 6 ]; then echo "FAIL"; exit 1; fi',
'echo OK',
].join(' && '),
grep -Eq "foo|bar|baz" file.ts
/opt/homebrew/bin/bash workflows/your-workflow/execute.sh --wave 2
// workflows/lib/cloud-repo-setup.ts
export interface CloudRepoSetupOptions {
branch: string;
committerName?: string;
extraSetupCommands?: string[];
skipWorkspaceBuild?: boolean;
}
export function applyCloudRepoSetup<T>(wf: T, opts: CloudRepoSetupOptions): T {
// adds two steps: setup-branch, install-deps
// install-deps runs: npm install + workspace prebuilds (build:platform, build:core, etc.)
// ...
}
Current @relayflows/core does not provide createGitHubStep. Model shipping
as deterministic workflow steps. Locally, use git plus gh after a preflight
that proves gh auth status works. When a runtime adapter is required, write a
small deterministic script that imports GitHubClient from
@relayflows/github-primitive and calls createBranch, createFile or
updateFile, and createPR.
import { workflow } from '@relayflows/core';
const BRANCH = `agent-relay/run-${Date.now()}`;
async function runWorkflow() {
await workflow('feature-x')
// ... your real implementation, repair, review loops, and final acceptance ...
.step('write-marker', {
type: 'deterministic',
command: `echo "fix landed at $(date -u)" >> CHANGELOG.md`,
})
.step('create-branch', {
type: 'deterministic',
dependsOn: ['write-marker'],
command: `git switch -c ${BRANCH}`,
})
.step('commit-change', {
type: 'deterministic',
dependsOn: ['create-branch'],
command: 'git add CHANGELOG.md && git commit -m "chore: changelog entry"',
})
.step('push-branch', {
type: 'deterministic',
dependsOn: ['commit-change'],
command: `git push -u origin ${BRANCH}`,
})
.step('open-pr', {
type: 'deterministic',
dependsOn: ['push-branch'],
command: `gh pr create --base main --head ${BRANCH} --title "feat: ship feature X" --body-file .workflow-artifacts/feature-x/pr-body.md`,
verification: { type: 'pr_url', value: 'AgentWorkforce/cloud' },
})
.run({ cwd: process.cwd() });
}
runWorkflow().catch((error) => {
console.error(error);
process.exit(1);
});
verification: { type: 'exit_code' } // preferred for code-editing steps
verification: { type: 'output_contains', value: 'DONE' } // optional accelerator
verification: { type: 'file_exists', value: 'src/out.ts' } // deterministic file check
verification: { type: 'pr_url', value: 'owner/repo' } // step must leave behind a PR
.step('fix-types', { agent: 'worker', dependsOn: ['review'], ... })
.step('fix-tests', { agent: 'worker', dependsOn: ['review'], ... })
.step('final', { agent: 'lead', dependsOn: ['fix-types', 'fix-tests'], ... })
// Subscribe an agent to additional channels post-spawn
relay.subscribe({ agent: 'security-auditor', channels: ['review-pr-456'] });
// Unsubscribe — agent leaves the channel entirely
relay.unsubscribe({ agent: 'security-auditor', channels: ['general'] });
// Mute — agent stays subscribed (history access) but messages are NOT injected into PTY
relay.mute({ agent: 'security-auditor', channel: 'review-pr-123' });
// Unmute — resume PTY injection
relay.unmute({ agent: 'security-auditor', channel: 'review-pr-123' });
relay.onChannelSubscribed = (agent, channels) => {
/* ... */
};
relay.onChannelUnsubscribed = (agent, channels) => {
/* ... */
};
relay.onChannelMuted = (agent, channel) => {
/* ... */
};
relay.onChannelUnmuted = (agent, channel) => {
/* ... */
};
`typescript.agent('name', {
cli: 'claude' | 'codex' | 'gemini' | 'aider' | 'goose' | 'opencode' | 'droid',
role?: string,
preset?: 'lead' | 'worker' | 'reviewer' | 'analyst',
retries?: number,
model?: string,
interactive?: boolean, // default: true
})
import { ClaudeModels, CodexModels, GeminiModels } from '@agent-relay/config';
.agent('planner', { cli: 'claude', model: ClaudeModels.OPUS }) // not 'opus'
.agent('worker', { cli: 'claude', model: ClaudeModels.SONNET }) // not 'sonnet'
.agent('coder', { cli: 'codex', model: CodexModels.GPT_5_4 }) // not 'gpt-5.4'
.step('name', {
agent: string,
task: string, // supports {{var}} and {{steps.NAME.output}}
dependsOn?: string[],
verification?: VerificationCheck,
retries?: number,
})
.step('verify-files', {
type: 'deterministic',
command: 'test -f src/auth.ts && echo "FILE_EXISTS"',
dependsOn: ['implement'],
captureOutput: true,
failOnError: false,
})
.step('repair-files', {
agent: 'worker',
dependsOn: ['verify-files'],
task: `If verify-files failed, create or fix the missing file and rerun the check.
Output:
{{steps.verify-files.output}}`,
verification: { type: 'exit_code' },
})
.step('verify-files-final', {
type: 'deterministic',
command: 'test -f src/auth.ts && echo "FILE_EXISTS"',
dependsOn: ['repair-files'],
captureOutput: true,
failOnError: true,
})
.agent('claude-reviewer', {
cli: 'claude',
preset: 'reviewer',
role: 'First-pass fresh-eyes reviewer. Reads actual files, diffs, rules, and evidence from scratch.',
retries: 1,
})
.agent('claude-fixer', {
cli: 'claude',
role: 'Fixer for valid Claude review findings. Adds or updates tests/proofs for each fix.',
retries: 2,
})
.agent('codex-reviewer', {
cli: 'codex',
preset: 'reviewer',
role: 'Second-pass fresh-eyes reviewer. Reviews the post-Claude-fix state from scratch.',
retries: 1,
})
.agent('codex-fixer', {
cli: 'codex',
role: 'Fixer for valid Codex review findings. Adds or updates tests/proofs for each fix.',
retries: 2,
})
.step('claude-review', {
agent: 'claude-reviewer',
dependsOn: ['verify-final'],
task: `First-pass fresh-eyes review.
Read the task spec, AGENTS.md / CLAUDE.md, changed files, final diff, artifacts, and verification evidence:
{{steps.verify-final.output}}
Write .workflow-artifacts/<workflow>/claude-review.md.
Use actionable findings with file paths, severity, and required fixes.
If there are no issues, write NO_ISSUES_FOUND.`,
verification: { type: 'exit_code' },
})
.step('claude-fix', {
agent: 'claude-fixer',
dependsOn: ['claude-review'],
task: `Read .workflow-artifacts/<workflow>/claude-review.md.
If it contains findings, fix every valid issue and add or update appropriate tests/proofs. After each fix, rerun targeted checks and review the touched files again.
Keep iterating locally until this round has no remaining valid issues.
Write .workflow-artifacts/<workflow>/claude-fix.md with fixes and commands run.
If the review says NO_ISSUES_FOUND, record that no fix was needed.`,
verification: { type: 'exit_code' },
})
.step('claude-review-final', {
agent: 'claude-reviewer',
dependsOn: ['claude-fix'],
task: `Review the post-Claude-fix state from scratch. Do not rely on prior review text or fixer summaries.
Read the files, diff, rules, spec, and evidence. Write .workflow-artifacts/<workflow>/claude-review-final.md.
Use NO_ISSUES_FOUND only if there are no actionable issues left.`,
verification: { type: 'exit_code' },
})
.step('claude-fix-final', {
agent: 'claude-fixer',
dependsOn: ['claude-review-final'],
task: `If the final Claude review contains findings, fix them, add or update appropriate tests/proofs, rerun relevant checks, and write .workflow-artifacts/<workflow>/claude-fix-final.md.
If a finding cannot be fixed, write .workflow-artifacts/<workflow>/BLOCKED_NO_COMMIT.md with exact evidence.
If the final review says NO_ISSUES_FOUND, write .workflow-artifacts/<workflow>/claude-signoff.md.`,
verification: { type: 'exit_code' },
})
.step('verify-after-claude-review', {
type: 'deterministic',
dependsOn: ['claude-fix-final'],
command: 'test ! -f .workflow-artifacts/<workflow>/BLOCKED_NO_COMMIT.md && npm run typecheck && npm test 2>&1',
captureOutput: true,
failOnError: false,
})
.step('codex-review', {
agent: 'codex-reviewer',
dependsOn: ['verify-after-claude-review'],
task: `Second-pass fresh-eyes review of the post-Claude-fix state.
Read the task spec, AGENTS.md / CLAUDE.md, changed files, final diff, artifacts, and verification evidence:
{{steps.verify-after-claude-review.output}}
Write .workflow-artifacts/<workflow>/codex-review.md.
Use actionable findings with file paths, severity, and required fixes.
If there are no issues, write NO_ISSUES_FOUND.`,
verification: { type: 'exit_code' },
})
.step('codex-fix', {
agent: 'codex-fixer',
dependsOn: ['codex-review'],
task: `Read .workflow-artifacts/<workflow>/codex-review.md.
If it contains findings, fix every valid issue and add or update appropriate tests/proofs. After each fix, rerun targeted checks and review the touched files again.
Keep iterating locally until this round has no remaining valid issues.
Write .workflow-artifacts/<workflow>/codex-fix.md with fixes and commands run.
If the review says NO_ISSUES_FOUND, record that no fix was needed.`,
verification: { type: 'exit_code' },
})
.step('codex-review-final', {
agent: 'codex-reviewer',
dependsOn: ['codex-fix'],
task: `Review the post-fix state from scratch. Do not rely on prior review text or fixer summaries.
Read the files, diff, rules, spec, and evidence. Write .workflow-artifacts/<workflow>/codex-review-final.md.
Use NO_ISSUES_FOUND only if there are no actionable issues left.`,
verification: { type: 'exit_code' },
})
.step('codex-fix-final', {
agent: 'codex-fixer',
dependsOn: ['codex-review-final'],
task: `If the final review contains findings, fix them, add or update appropriate tests/proofs, rerun relevant checks, and write .workflow-artifacts/<workflow>/codex-fix-final.md.
If a finding cannot be fixed, write .workflow-artifacts/<workflow>/BLOCKED_NO_COMMIT.md with exact evidence.
If the final review says NO_ISSUES_FOUND, write .workflow-artifacts/<workflow>/codex-signoff.md.`,
verification: { type: 'exit_code' },
})
.step('acceptance-after-codex-review', {
type: 'deterministic',
dependsOn: ['codex-fix-final'],
command: 'test ! -f .workflow-artifacts/<workflow>/BLOCKED_NO_COMMIT.md && npm run typecheck && npm test 2>&1',
captureOutput: true,
failOnError: true,
})
.agent('lead', {
cli: 'claude',
model: ClaudeModels.OPUS,
role: 'Architect and reviewer — assigns work, reviews, posts feedback',
retries: 1,
// No preset — interactive by default
})
.agent('impl-new', {
cli: 'codex',
model: CodexModels.GPT_5_4,
role: 'Creates new files. Listens on channel for assignments and feedback.',
retries: 2,
// No preset — interactive, receives channel messages
})
.agent('impl-modify', {
cli: 'codex',
model: CodexModels.GPT_5_4,
role: 'Edits existing files. Listens on channel for assignments and feedback.',
retries: 2,
})
// All three share the same dependsOn — they start concurrently (no deadlock)
.step('lead-coordinate', {
agent: 'lead',
dependsOn: ['context'],
task: `You are the lead on #channel. Workers: impl-new, impl-modify.
Post the plan. Assign files. Review their work. Post feedback if needed.
Workers iterate based on your feedback. Exit when all files are correct.`,
})
.step('impl-new-work', {
agent: 'impl-new',
dependsOn: ['context'], // same dep as lead = parallel start
task: `You are impl-new on #channel. Wait for the lead's plan.
Create files as assigned. Report completion. Fix issues from feedback.`,
})
.step('impl-modify-work', {
agent: 'impl-modify',
dependsOn: ['context'], // same dep as lead = parallel start
task: `You are impl-modify on #channel. Wait for the lead's plan.
Edit files as assigned. Report completion. Fix issues from feedback.`,
})
// Downstream gates on lead (lead exits when satisfied)
.step('verify', { type: 'deterministic', dependsOn: ['lead-coordinate'], ... })
.step('integrate', {
agent: 'integrator',
dependsOn: ['context'],
task: `You are the integrator on #wf-feature.
Before writing code, post a direct question to @schema-owner asking which
table owns the new field. Do NOT proceed until @schema-owner replies in
channel. If no reply arrives in 5 minutes, @-mention the lead.`,
})
.step('lead-coordinate', {
agent: 'lead',
dependsOn: ['context'],
task: `Post the plan to #wf-feature, then @impl-a @impl-b @impl-c.
Wait for each to reply with "ACK <agent-name>" before issuing assignments.
If any worker hasn't acked in 3 minutes, re-post and ping again.
Only after all three have acked, post per-worker assignments.`,
})
.step('impl-a-work', {
agent: 'impl-a',
dependsOn: ['context'],
task: `Implement src/foo.ts per the lead's assignment.
When done, post to #wf-feature: "@reviewer ready: src/foo.ts" — include the
commit SHA. Then wait for @reviewer's verdict in channel.
- If "APPROVED", you're done.
- If "CHANGES_REQUESTED <notes>", apply the notes and re-post.
- If no verdict in 5 min, @-mention the lead.`,
})
.step('lead-coordinate', {
agent: 'lead',
task: `... coordinate the team ...
Every 10 minutes, post a status probe: "@impl-a @impl-b status?"
Each worker should reply with one of:
- "RUNNING <step>" (still working)
- "BLOCKED <reason>" (@-mention the lead with the blocker)
- "DONE <artifact>" (ready for review)
If a worker is silent for two probes in a row, mark them stalled and
reassign their work to a peer.`,
})
.step('impl-a-work', {
agent: 'impl-a',
task: `... finish your part ...
When done, post a handoff to #wf-feature targeting the next worker:
"@impl-b HANDOFF: src/foo.ts ready. Touched: <files>. Open question: <if any>.
Tests: <pass/fail summary>. Commit: <sha>."`,
})
.pattern('pipeline')
.step('analyze', { agent: 'analyst', task: '...' })
.step('implement', { agent: 'dev', task: '{{steps.analyze.output}}', dependsOn: ['analyze'] })
.step('test', { agent: 'tester', task: '{{steps.implement.output}}', dependsOn: ['implement'] })
.onError('fail-fast') // stop on first failure (default)
.onError('continue') // skip failed branches, continue others
.onError('retry', { maxRetries: 3, retryDelayMs: 5000 })
steps:
- name: read-types
type: deterministic
command: cat src/types.ts
captureOutput: true
- name: edit-types
agent: dev
dependsOn: [read-types]
task: |
Edit src/types.ts. Current contents:
{{steps.read-types.output}}
Add 'pending' to the Status union type.
Only edit this one file.
verification:
type: exit_code
- name: verify-types
type: deterministic
dependsOn: [edit-types]
command: 'if git diff --quiet src/types.ts; then echo "NOT MODIFIED"; exit 1; fi; echo "OK"'
captureOutput: true
failOnError: false
- name: fix-types-verification
agent: dev
dependsOn: [verify-types]
task: |
If verify-types failed, fix src/types.ts and rerun the verify command.
Output:
{{steps.verify-types.output}}
verification:
type: exit_code
- name: verify-types-final
type: deterministic
dependsOn: [fix-types-verification]
command: 'if git diff --quiet src/types.ts; then echo "NOT MODIFIED"; exit 1; fi; echo "OK"'
captureOutput: true
failOnError: true
- name: read-service
type: deterministic
dependsOn: [verify-types-final]
command: cat src/service.ts
captureOutput: true
- name: edit-service
agent: dev
dependsOn: [read-service]
task: |
Edit src/service.ts. Current contents:
{{steps.read-service.output}}
Add a handlePending() method.
Only edit this one file.
verification:
type: exit_code
- name: verify-service
type: deterministic
dependsOn: [edit-service]
command: 'if git diff --quiet src/service.ts; then echo "NOT MODIFIED"; exit 1; fi; echo "OK"'
captureOutput: true
failOnError: false
- name: fix-service-verification
agent: dev
dependsOn: [verify-service]
task: |
If verify-service failed, fix src/service.ts and rerun the verify command.
Output:
{{steps.verify-service.output}}
verification:
type: exit_code
- name: verify-service-final
type: deterministic
dependsOn: [fix-service-verification]
command: 'if git diff --quiet src/service.ts; then echo "NOT MODIFIED"; exit 1; fi; echo "OK"'
captureOutput: true
failOnError: true
# Deterministic commit — never rely on agents to commit
- name: commit
type: deterministic
dependsOn: [verify-service-final]
command: npm run typecheck && npm test && git add src/types.ts src/service.ts && git commit -m "feat: add pending status"
captureOutput: true
failOnError: false
- name: repair-commit
agent: dev
dependsOn: [commit]
task: |
If commit failed, fix the blocker, rerun npm run typecheck && npm test, and create the commit.
If commit passed, confirm the commit subject.
Output:
{{steps.commit.output}}
verification:
type: exit_code
- name: verify-commit-created
type: deterministic
dependsOn: [repair-commit]
command: 'git log -1 --pretty=%s | grep -q "^feat: add pending status$" && echo "COMMIT_OK" || (echo "COMMIT_MISSING"; exit 1)'
captureOutput: true
failOnError: true
file_exists check before proceeding. Non-interactive agents may exit 0 without writing anything (wrong cwd, stdout instead of disk).- name: verify-files
type: deterministic
dependsOn: [impl-auth, impl-storage]
command: |
missing=0
for f in src/auth/credentials.ts src/storage/client.ts; do
if [ ! -f "$f" ]; then echo "MISSING: $f"; missing=$((missing+1)); fi
done
if [ $missing -gt 0 ]; then echo "$missing files missing"; exit 1; fi
echo "All files present"
captureOutput: true
failOnError: false
- name: fix-missing-files
agent: impl-auth
dependsOn: [verify-files]
task: |
If verify-files found missing files, create/fix them and rerun the check.
Output:
{{steps.verify-files.output}}
verification:
type: exit_code
- name: verify-files-final
type: deterministic
dependsOn: [fix-missing-files]
command: |
missing=0
for f in src/auth/credentials.ts src/storage/client.ts; do
if [ ! -f "$f" ]; then echo "MISSING: $f"; missing=$((missing+1)); fi
done
if [ $missing -gt 0 ]; then echo "$missing files missing"; exit 1; fi
echo "All files present"
captureOutput: true
failOnError: true
- name: provider-edit-gate-capture
type: deterministic
dependsOn: [implement-providers]
command: |
if [ -z "$(git status --short -- packages/new-provider .workflow-artifacts/my-flow)" ]; then
echo "NO_PROVIDER_CHANGES"
exit 1
fi
echo "PROVIDER_EDIT_GATE_OK"
captureOutput: true
failOnError: false
- name: repair-edit-gate
agent: provider-worker
dependsOn: [provider-edit-gate-capture]
task: |
If provider-edit-gate-capture reported NO_PROVIDER_CHANGES, inspect git
status including untracked files and add the missing provider artifacts.
If it already passed, do nothing.
verification:
type: exit_code
- name: provider-edit-gate-final
type: deterministic
dependsOn: [repair-edit-gate]
command: |
if [ -z "$(git status --short -- packages/new-provider .workflow-artifacts/my-flow)" ]; then
echo "NO_PROVIDER_CHANGES"
exit 1
fi
echo "PROVIDER_EDIT_GATE_FINAL_OK"
captureOutput: true
failOnError: false
- name: repair-provider-edit-gate-final
agent: provider-worker
dependsOn: [provider-edit-gate-final]
task: |
If provider-edit-gate-final is still red, repair the missing provider
artifacts and rerun the check. If repair is impossible, write
.workflow-artifacts/my-flow/BLOCKED_NO_COMMIT.md with exact evidence and
do not commit.
Output:
{{steps.provider-edit-gate-final.output}}
verification:
type: exit_code
.step('runtime-implementation', {
agent: 'impl-runtime',
dependsOn: ['context'],
task: 'Implement the runtime slice and write .workflow-artifacts/runtime.md',
})
.step('adapter-implementation', {
agent: 'impl-adapters',
dependsOn: ['context'],
task: 'Implement adapter wiring and write .workflow-artifacts/adapters.md',
})
.step('implementation-reconcile', {
type: 'deterministic',
dependsOn: ['context'],
command: `git status --short -- packages/core packages/*/src/writeback.ts scripts tests .workflow-artifacts
test -f scripts/verify-e2e.mjs || echo "MISSING_E2E"
test -f packages/core/src/runtime/router.ts || echo "MISSING_ROUTER"`,
captureOutput: true,
failOnError: false,
})
.step('repair-implementation-reconcile', {
agent: 'qa',
dependsOn: ['implementation-reconcile'],
task: `Finish anything missing before gates run:\n{{steps.implementation-reconcile.output}}`,
verification: { type: 'exit_code' },
})
.step('run-e2e', {
type: 'deterministic',
dependsOn: ['repair-implementation-reconcile'],
command: 'npm run verify:e2e',
captureOutput: true,
failOnError: false,
})
`yaml# WRONG — deadlock: coordinate depends on context, work-a depends on coordinate
steps:
- name: coordinate
dependsOn: [context] # lead waits for WORKER_DONE...
- name: work-a
dependsOn: [coordinate] # ...but work-a can't start until coordinate finishes
# RIGHT — workers and lead start in parallel
steps:
- name: context
type: deterministic
- name: work-a
dependsOn: [context] # starts with lead
- name: coordinate
dependsOn: [context] # starts with workers
- name: merge
dependsOn: [work-a, coordinate]
# Team pattern: lead + workers on a shared channel
steps:
- name: track-lead-coord
agent: track-lead
dependsOn: [prior-step]
task: |
Lead the track on #my-track. Workers: track-worker-1, track-worker-2.
Post assignments to the channel. Review worker output.
- name: track-worker-1-impl
agent: track-worker-1
dependsOn: [prior-step] # same dep as lead — starts concurrently
task: |
Join #my-track. track-lead will post your assignment.
Implement the file as directed.
verification:
type: exit_code
- name: next-step
dependsOn: [track-lead-coord] # downstream depends on lead, not workers
When you set .pattern('supervisor') (or hub-spoke, fan-out), the runner auto-assigns a supervisor agent as owner for worker steps. The supervisor monitors progress, nudges idle workers, and issues OWNER_DECISION.
Auto-hardening only activates for hub patterns — not pipeline or dag.
| Use case | Pattern | Why |
| ------------------------- | ------------------- | -------------------------------- |
| Sequential, no monitoring | pipeline | Simple, no overhead |
| Workers need oversight | supervisor | Auto-owner monitors |
| Local/small models | supervisor | Supervisor catches stuck workers |
| All non-interactive | pipeline or dag | No PTY = no supervision needed |
Cap maxConcurrency at 4-6. Spawning 10+ agents simultaneously causes broker timeouts.
| Parallel agents | maxConcurrency |
| --------------- | ---------------- |
| 2-4 | 4 (default safe) |
| 5-10 | 5 |
| 10+ | 6-8 max |
| Mistake | Fix |
| ---------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Treating relay as transport, not as a coordination layer (every step is preset: 'worker', every handoff is {{steps.X.output}}) | Default to Conversation shape for non-trivial work — interactive agents on a shared channel. Pipeline-shape is only correct when the work could be expressed as a bash \| bash \| bash pipe. |
| Interactive agents on a channel whose task strings don't tell them to talk to each other | Pick a Chat-Native Coordination Recipe (Q/A, Broadcast/Ack, Peer Review, Standup, Hand-Off) and bake it into the task prompt — otherwise you're paying for a chat substrate you're not using |
| All workflows run sequentially | Group independent workflows into parallel waves (4-7x speedup) |
| Every step depends on the previous one | Only add dependsOn when there's a real data dependency |
| Self-review step with no timeout | Set timeout: 300_000 (5 min) — Codex hangs in non-interactive review |
| One giant workflow per feature | Split into smaller workflows that can run in parallel waves |
| Adding exit instructions to tasks | Runner handles self-termination automatically |
| Interactive PTY Codex for one-shot artifact steps | Use preset: 'worker' plus file_exists or custom verification |
| Setting timeoutMs on agents/steps | Use global .timeout() only |
| Using general channel | Set .channel('wf-name') for isolation |
| {{steps.X.output}} without dependsOn: ['X'] | Output won't be available yet |
| Requiring exact sentinel as only completion gate | Use exit_code or file_exists verification |
| Writing 100-line task prompts | Split into lead + workers on a channel |
| maxConcurrency: 16 with many parallel steps | Cap at 5-6 |
Complete development kit for Microsoft 365 Copilot declarative agents with three comprehensive workflows (basic, advanced, validation), TypeSpec support, and Microsoft 365 Agents Toolkit integration
Format and structurally validate local treatment-plan documentation after clinical decisions have already been supplied and verified by authorized licensed professionals. Use for source traceability, clinician-authored intervention records, goals and checkpoints, shared-decision records, reconciliation handoffs, and release gates—not for clinical decision-making.
> provider/change budget/修改卖家/修改预算/draft/草稿/我的任务/my tasks/what am I working on/关闭/取消任务/决策列表/decision list/指定服务商/browse (sender.role = COUNTERPARTY, not you); (3) literal "Read the okx-ai skill" (or legacy "Read the okx-agent-task skill") in the envelope.
Automate payer review of prior authorization (PA) requests. This skill should be used when users say "Review this PA request", "Process prior authorization for [procedure]", "Assess medical necessity", "Generate PA decision", or when processing clinical documentation for coverage policy validation and authorization decisions.
Expert in designing and building autonomous AI agents. Masters tool use, memory systems, planning strategies, and multi-agent orchestration.
Autonomous agents are AI systems that can independently decompose goals, plan actions, execute tools, and self-correct without constant human guidance. The challenge isn't making them capable - it's making them reliable. Every extra decision multiplies failure probability.
Orchestrates design workflows by routing work through brainstorming, multi-agent review, and execution readiness in the correct order.
Structured persuasion for tech leads, PMs, and founders—not activity logs. Five scenarios (kickoff, status update, wrap-up, investor pitch, solution selling) on one 5-part framework (Hook→Context→Proposal→Evidence→Ask). AI prompts for missing materials and audience context; pre-submit checklist. Claude Code plugin; Cursor, Codex, and chat via prompts.
Take agentworkforce/writing-agent-relay-workflows from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.
The instructions reference npm.
Without those the skill loads but fails at the first command.