Workflow as Code
Definition
Workflow as Code is the practice of defining agentic workflows using deterministic programming languages (like TypeScript or Python) rather than natural language prompts.
It treats the Agent as a function call within a larger, strongly-typed system, rather than treating the System as a tool available to a chatty agent.
When to Use
Use this practice when:
- Building repetitive production processes (CI/CD, release workflows)
- Implementing complex branching logic with multiple decision points
- Operating high-reliability pipelines where failure consequences are significant
- Orchestrating multi-step agent tasks that require verification checkpoints
Skip this practice when:
- Exploratory tasks with undefined outcomes
- Simple, linear command sequences
- Ad-hoc queries or one-off investigations
- Low-stakes prototyping where speed matters more than reliability
Why It Matters
When complex workflows are driven entirely by an LLM loop (“Here is a goal, figure it out”), the system suffers from Context Pollution. As the agent accumulates history—observations, tool outputs, internal monologue—its attention degrades.
Nick Tune describes this as the agent becoming “tipsy wobbling from side-to-side”: it loses focus on strict process adherence because its context window is overflowing with implementation details.
Process
Step 1: Identify Deterministic vs Probabilistic Tasks
Audit your workflow. Separate mechanical tasks (running builds, conditional logic, file operations) from intelligence tasks (code review, summarization, decision-making under ambiguity).
Deterministic (Code):
- Run build/test commands
- Parse structured output
- Branch on conditions
- Read/write files
- Make API calls
Probabilistic (Agent):
- Review code against spec
- Summarize findings
- Generate implementation
- Assess quality
Step 2: Define Typed Step Abstraction
Create a common interface for workflow steps:
export type WorkflowContext = {
workDir: string;
spec: string;
history: StepResult[];
};
export type StepResult =
| { type: 'success'; data: unknown }
| { type: 'failure'; reason: string; recoverable: boolean };
export type Step = (ctx: WorkflowContext) => Promise<StepResult>;
This enables:
- Composition: Reassemble steps into new workflows
- Type Safety: Validate data passing between steps
- Testability: Unit test orchestration without invoking an LLM
Step 3: Implement the Orchestration Shell
Write the control flow in code. The LLM only appears where intelligence is required:
async function runDevWorkflow(ctx: WorkflowContext) {
// Deterministic: Run build
const buildResult = await runBuild(ctx);
if (buildResult.type === 'failure') {
return handleBuildError(buildResult);
}
// Probabilistic: Agent reviews the diff
const reviewResult = await runAgentReview({
diff: await git.getDiff(),
spec: ctx.spec
});
// Deterministic: Act on structured result
if (reviewResult.verdict === 'PASS') {
await git.commit();
await github.createPR();
}
}
Step 4: Implement Opaque Commands
From the agent’s perspective, workflow steps should be “Black Boxes.” The agent invokes a high-level command and acts on the structured result—it doesn’t need to know implementation details.
Define the interface:
type VerifyWorkResult = {
status: 'passed' | 'failed';
errors?: { file: string; line: number; message: string }[];
};
async function verifyWork(ctx: WorkflowContext): Promise<VerifyWorkResult> {
// Implementation hidden from agent
const lint = await runLint(ctx.workDir);
const types = await runTypeCheck(ctx.workDir);
const tests = await runTests(ctx.workDir);
return aggregateResults([lint, types, tests]);
}
This reduces token usage and prevents the agent from hallucinating incorrect shell commands.
Step 5: Add Enforcement Hooks
Agents will sometimes try to bypass the workflow. Implement hard boundaries using client-side hooks:
# .claude/hooks/pre-tool-use.sh
if [[ "$TOOL" == "Bash" && "$COMMAND" =~ "git push" ]]; then
echo "Blocked: Use 'submit-pr' tool which runs verification first."
exit 1
fi
This shifts enforcement from “Instructions in the System Prompt” (which can be ignored) to “Code in the Environment” (which cannot).
Template
Minimal workflow orchestrator structure:
// workflows/dev-workflow.ts
import type { Step, WorkflowContext, StepResult } from './types';
const steps: Step[] = [
runBuild,
runLint,
runAgentReview, // Only probabilistic step
commitChanges,
createPR,
];
export async function execute(ctx: WorkflowContext): Promise<StepResult> {
for (const step of steps) {
const result = await step(ctx);
if (result.type === 'failure' && !result.recoverable) {
return result;
}
ctx.history.push(result);
}
return { type: 'success', data: ctx.history };
}
Workflows as Persona Carriers
Persona Injection via Workflow
Workflows are the natural home for session-scoped persona injection. Rather than loading all persona definitions into agents.md on every session, define the persona as part of the workflow context — it gets injected precisely when needed and is absent when it isn’t.
A code review workflow injects the Critic persona. An implementation workflow injects the Dev persona. A spec workflow injects the Lead persona. This is more precise than always-on loading, and avoids the cost of agents following instructions that are irrelevant to the current task.
Example: Review workflow with Critic persona
# .claude/workflows/review.yaml
name: Constitutional Review
trigger: "@review"
context:
- .claude/skills/critic.md # Critic persona — injected here, not in agents.md
- docs/backlog/current-pbi.md # The spec being reviewed
- AGENTS.md # Project-level judgment boundaries
steps:
- validate_against_spec
- constitutional_review
- produce_report
Example: Implementation workflow with Dev persona
# .claude/workflows/implement.yaml
name: Implementation
trigger: "@implement"
context:
- .claude/skills/dev.md # Dev persona — only loaded for implementation tasks
- docs/backlog/current-pbi.md # The PBI being implemented
- AGENTS.md # Project-level judgment boundaries
steps:
- review_pbi
- plan
- implement
- run_tests
- update_pbi_status
The key property: AGENTS.md contains only project-level judgment. The persona is carried by the workflow and injected at invocation. This keeps agents.md stable and minimal, while delivering the right behavioral context for each task type.
Common Mistakes
The God Prompt
Problem: Entire workflow described in a single system prompt, expecting the agent to “figure it out.”
Solution: Extract deterministic logic into code. The agent should only handle tasks requiring intelligence.
Leaky Abstractions
Problem: Agent sees raw command output (500 lines of test failures) instead of structured results.
Solution: Parse outputs into typed results before passing to the agent. Summarize, don’t dump.
Missing Enforcement
Problem: Workflow relies on the agent “following instructions” without hard boundaries.
Solution: Implement hooks that block unauthorized actions. Trust code, not compliance.
Over-Agentification
Problem: Using an LLM to run npm install or parse JSON—tasks with zero ambiguity.
Solution: Reserve agent calls for genuinely probabilistic tasks. Everything else is code.
Related Patterns
- Ralph Loop — Implements the “Loop” part of the workflow using code-based persistence
- Context Gates — Architectural checkpoints that Workflow as Code enforces programmatically
- Model Routing — Assigning different models to different steps within the code-based workflow
References
- (2026).
Dev Workflows as Code
.
Accessed January 18, 2026.
Foundational article describing the shift from prompt-based to code-based orchestration.