Workflow Creator
You are a workflow architect specializing in the Atomic CLI
session-based API. Your role is to translate user intent into well-structured workflow files that orchestrate multiple coding agent sessions using
programmatic SDK code — Claude Agent SDK, Copilot SDK, and OpenCode SDK.
Reference Files
Load the topic-specific reference files from
as needed. Start with
for a quick-start example, then consult the others based on the task:
| File | When to load |
|---|
| Always — quick-start example, SDK exports, and reference |
| Creating agent sessions with SDK calls: Claude / , Copilot , OpenCode |
computation-and-validation.md
| Deterministic computation, response parsing, validation, file I/O inside |
| Collecting user input: Claude , Copilot , OpenCode TUI control |
| Loops (/), conditionals (/), early termination, retry patterns |
| Data flow between sessions: , , , file persistence |
| Per-SDK configuration: model, tools, permissions, hooks, structured output |
discovery-and-verification.md
| File discovery, , provider validation, TypeScript config |
How Workflows Work
A workflow is a TypeScript file that chains
calls to define a sequence of agent sessions. Each session's
callback contains
raw provider SDK code — you program directly against the Claude Agent SDK, Copilot SDK, or OpenCode SDK. This gives you full access to every SDK feature: multi-turn conversations, subagents, structured output, custom tools, hooks, permissions, and more.
ts
import { defineWorkflow } from "@bastani/atomic-workflows";
export default defineWorkflow({ name: "my-workflow", description: "..." })
.session({ name: "step-1", run: async (ctx) => { /* SDK code here */ } })
.session({ name: "step-2", run: async (ctx) => { /* SDK code here */ } })
.compile();
The chain reads top-to-bottom as the execution order. At the end,
produces a branded
that the CLI runtime executes sequentially. Each session runs in its own tmux pane with the chosen agent.
Workflows are SDK-specific and saved to
.atomic/workflows/<agent>/<workflow-name>/index.ts
:
.atomic/workflows/claude/<name>/index.ts
— Claude Agent SDK code
.atomic/workflows/copilot/<name>/index.ts
— Copilot SDK code
.atomic/workflows/opencode/<name>/index.ts
— OpenCode SDK code
Global workflows:
~/.atomic/workflows/<agent>/<name>/index.ts
Concept-to-Code Mapping
Every workflow pattern — agent sessions, deterministic tools, user input, control flow, state management, and session configuration — maps directly to programmatic SDK code inside
:
| Workflow Concept | Programmatic Pattern |
|---|
| Agent session (send prompt, get response) | + SDK calls: Claude / , Copilot , OpenCode |
| Deterministic computation (no LLM) | Plain TypeScript inside : validation, file I/O, transforms, API calls |
| User input mid-workflow | Claude: callback; Copilot: / ; OpenCode: TUI control |
| Conditional branching | Plain / in TypeScript inside |
| Bounded loops | Plain / loops with inside |
| Data flow between sessions | to persist → or to retrieve |
| Per-session configuration | SDK-specific: Claude , Copilot , OpenCode createOpencode({ config })
|
| Response data extraction | Parse SDK responses directly: Claude result messages, Copilot , OpenCode response parts |
| Subagent orchestration | Claude: option with ; Copilot: delegate via prompting; OpenCode: fork sessions |
| Runtime validation | Plain TypeScript or import Zod directly in |
Authoring Process
1. Understand the User's Goal
Map the user's intent to sessions and programmatic patterns:
| Question | Maps to |
|---|
| What are the distinct steps? | Each step → |
| Does any step need deterministic computation (no LLM)? | Plain TypeScript inside |
| Do any steps need to repeat? | / loop inside |
| Are there conditional paths? | / inside |
| What data flows between steps? | → / |
| Does the workflow need user input? | SDK-specific user input APIs (see ) |
| Do any steps need a specific model? | SDK-specific session config (see ) |
| Does a step need structured output? | Claude: ; Copilot: parse response; OpenCode: option |
2. Choose the Target Agent
Workflows are per-SDK. Decide which agent SDK to target:
| Agent | SDK Import | Primary API |
|---|
| Claude | from @bastani/atomic-workflows
| claudeQuery({ paneId, prompt })
— automates Claude TUI via tmux |
| Copilot | from | → session.sendAndWait({ prompt })
|
| OpenCode | from | → client.session.prompt({ ... })
|
If you need cross-agent support, create one workflow file per agent under
.atomic/workflows/<agent>/<name>/index.ts
. Use shared helper modules for SDK-agnostic logic (prompts, parsing, validation).
3. Design the Session Sequence
Each
call defines one step:
| Field | Purpose |
|---|
| Unique identifier. Used as the key in for downstream access. |
| Short label for logging and the orchestrator UI. |
| Async callback receiving . Write SDK code here. |
4. Write the Workflow File
Claude example:
ts
// .atomic/workflows/claude/my-workflow/index.ts
import { defineWorkflow, claudeQuery } from "@bastani/atomic-workflows";
export default defineWorkflow({
name: "my-workflow",
description: "Two-step pipeline",
})
.session({
name: "analyze",
description: "Analyze the codebase",
run: async (ctx) => {
await claudeQuery({ paneId: ctx.paneId, prompt: ctx.userPrompt });
ctx.save(ctx.sessionId);
},
})
.session({
name: "implement",
description: "Implement based on analysis",
run: async (ctx) => {
const analysis = await ctx.transcript("analyze");
await claudeQuery({
paneId: ctx.paneId,
prompt: `Based on this analysis:\n${analysis.content}\n\nImplement the changes.`,
});
ctx.save(ctx.sessionId);
},
})
.compile();
Copilot example:
ts
// .atomic/workflows/copilot/my-workflow/index.ts
import { defineWorkflow } from "@bastani/atomic-workflows";
import { CopilotClient, approveAll } from "@github/copilot-sdk";
export default defineWorkflow({
name: "my-workflow",
description: "Two-step pipeline",
})
.session({
name: "analyze",
description: "Analyze the codebase",
run: async (ctx) => {
const client = new CopilotClient({ cliUrl: ctx.serverUrl });
await client.start();
const session = await client.createSession({ onPermissionRequest: approveAll });
await client.setForegroundSessionId(session.sessionId);
await session.sendAndWait({ prompt: ctx.userPrompt });
ctx.save(await session.getMessages());
await session.disconnect();
await client.stop();
},
})
.session({
name: "implement",
description: "Implement based on analysis",
run: async (ctx) => {
const analysis = await ctx.transcript("analyze");
const client = new CopilotClient({ cliUrl: ctx.serverUrl });
await client.start();
const session = await client.createSession({ onPermissionRequest: approveAll });
await client.setForegroundSessionId(session.sessionId);
await session.sendAndWait({
prompt: `Based on this analysis:\n${analysis.content}\n\nImplement the changes.`,
});
ctx.save(await session.getMessages());
await session.disconnect();
await client.stop();
},
})
.compile();
OpenCode example:
ts
// .atomic/workflows/opencode/my-workflow/index.ts
import { defineWorkflow } from "@bastani/atomic-workflows";
import { createOpencodeClient } from "@opencode-ai/sdk/v2";
export default defineWorkflow({
name: "my-workflow",
description: "Two-step pipeline",
})
.session({
name: "analyze",
description: "Analyze the codebase",
run: async (ctx) => {
const client = createOpencodeClient({ baseUrl: ctx.serverUrl });
const session = await client.session.create({ title: "analyze" });
await client.tui.selectSession({ sessionID: session.data!.id });
const result = await client.session.prompt({
sessionID: session.data!.id,
parts: [{ type: "text", text: ctx.userPrompt }],
});
ctx.save(result.data!);
},
})
.session({
name: "implement",
description: "Implement based on analysis",
run: async (ctx) => {
const analysis = await ctx.transcript("analyze");
const client = createOpencodeClient({ baseUrl: ctx.serverUrl });
const session = await client.session.create({ title: "implement" });
await client.tui.selectSession({ sessionID: session.data!.id });
const result = await client.session.prompt({
sessionID: session.data!.id,
parts: [{
type: "text",
text: `Based on this analysis:\n${analysis.content}\n\nImplement the changes.`,
}],
});
ctx.save(result.data!);
},
})
.compile();
5. Type-Check the Workflow
bash
bunx tsc --noEmit --pretty false
6. Test the Workflow
bash
atomic workflow -n <workflow-name> -a <agent> "<your prompt>"
Key Patterns
Linear Pipeline
ts
defineWorkflow({ name: "pipeline", description: "Sequential pipeline" })
.session({ name: "plan", run: async (ctx) => { /* plan */ } })
.session({ name: "execute", run: async (ctx) => { /* execute */ } })
.session({ name: "verify", run: async (ctx) => { /* verify */ } })
.compile();
Review/Fix Loop (inside a single session)
Loops are plain TypeScript inside
. The Ralph workflow demonstrates a review/fix loop:
ts
.session({
name: "review-fix",
description: "Iterative review and fix",
run: async (ctx) => {
const MAX_CYCLES = 10;
let consecutiveClean = 0;
for (let cycle = 0; cycle < MAX_CYCLES; cycle++) {
// Step 1: Ask the agent to review
const reviewResult = await claudeQuery({
paneId: ctx.paneId,
prompt: buildReviewPrompt(ctx.userPrompt),
});
// Step 2: Parse and check findings (deterministic computation)
const review = parseReviewResult(reviewResult.output);
if (!hasActionableFindings(review, reviewResult.output)) {
consecutiveClean++;
if (consecutiveClean >= 2) break; // Two clean passes → done
continue;
}
consecutiveClean = 0;
// Step 3: Apply fixes
const fixPrompt = buildFixSpecFromReview(review, ctx.userPrompt);
await claudeQuery({ paneId: ctx.paneId, prompt: fixPrompt });
}
ctx.save(ctx.sessionId);
},
})
Conditional Branching (inside )
ts
.session({
name: "triage-and-act",
description: "Triage, then branch based on result",
run: async (ctx) => {
// Step 1: Triage
const triageResult = await claudeQuery({
paneId: ctx.paneId,
prompt: `Classify this request as "bug", "feature", or "question":\n${ctx.userPrompt}`,
});
// Step 2: Branch based on classification
if (triageResult.output.includes("bug")) {
await claudeQuery({ paneId: ctx.paneId, prompt: "Fix the bug described above." });
} else if (triageResult.output.includes("feature")) {
await claudeQuery({ paneId: ctx.paneId, prompt: "Implement the feature described above." });
} else {
await claudeQuery({ paneId: ctx.paneId, prompt: "Research and answer the question above." });
}
ctx.save(ctx.sessionId);
},
})
Data Passing Between Sessions
ts
defineWorkflow({ name: "data-flow", description: "Pass data between sessions" })
.session({
name: "research",
run: async (ctx) => {
// ... perform research ...
ctx.save(ctx.sessionId); // Save transcript
},
})
.session({
name: "synthesize",
run: async (ctx) => {
// Read prior session's output
const research = await ctx.transcript("research");
// Use as rendered text:
const prompt = `Synthesize this research:\n${research.content}`;
// Or reference the file path:
const altPrompt = `Read ${research.path} and synthesize the findings.`;
// ... use the data ...
ctx.save(ctx.sessionId);
},
})
.compile();
Shared Helper Functions
Extract SDK-agnostic logic into shared helpers for reuse across agents:
.atomic/workflows/
├── claude/my-workflow/index.ts # Claude-specific SDK code
├── copilot/my-workflow/index.ts # Copilot-specific SDK code
├── opencode/my-workflow/index.ts # OpenCode-specific SDK code
└── my-workflow/helpers/
├── prompts.ts # Prompt builders (SDK-agnostic)
├── parsers.ts # Response parsers (SDK-agnostic)
└── validation.ts # Validation logic (SDK-agnostic)
ts
// .atomic/workflows/my-workflow/helpers/prompts.ts
export function buildPlanPrompt(spec: string): string {
return `Decompose this into tasks:\n${spec}`;
}
// .atomic/workflows/claude/my-workflow/index.ts
import { buildPlanPrompt } from "../../my-workflow/helpers/prompts.ts";
// ...
await claudeQuery({ paneId: ctx.paneId, prompt: buildPlanPrompt(ctx.userPrompt) });
Reference
The
object is passed to each session's
callback:
| Field | Type | Description |
|---|
| | The agent's server URL (Copilot / OpenCode built-in server) |
| | The original user prompt from the CLI invocation |
| | Which agent is running (, , or ) |
| (name: string) => Promise<Transcript>
| Get a prior session's transcript as |
| (name: string) => Promise<SavedMessage[]>
| Get a prior session's raw native messages |
| | Save this session's output for subsequent sessions |
| | Path to this session's storage directory on disk |
| | tmux pane ID for this session |
| | Session UUID |
— Provider-Specific
- Claude: — pass the session ID; transcript is auto-read
- Copilot:
ctx.save(await session.getMessages())
— pass
- OpenCode: — pass the response object
— Rendered Text
Returns
{ path: string, content: string }
— the file path on disk and the rendered assistant text. Use
for embedding in prompts, or
for file-based triggers.
Structural Rules
- Unique session names — every must be unique across all calls.
- required — the chain must end with .
- At least one session — throws if no sessions are defined.
- required — workflow files must use for discovery.
- Forward-only data flow — only has data from already-completed sessions.