# How to Build Multi-Step Agent Workflows with Deterministic Control

> Learn to build multi step agent workflows with deterministic control by separating LLM reasoning from execution. Output JSON intents for a dispatch layer to route to functions. Enable interruptible loops with persistent state.

- Repository: [HumanLayer/12-factor-agents](https://github.com/humanlayer/12-factor-agents)
- Tags: tutorial
- Published: 2026-05-19

---

**To build multi-step agent workflows with deterministic control, separate LLM reasoning from deterministic execution by having the LLM output structured JSON intents that a dispatch layer routes to concrete functions, enabling interruptible loops with persistent state.**

The humanlayer/12-factor-agents repository provides an architectural blueprint for reliable AI systems. When you build multi-step agent workflows with deterministic control, you ensure that business logic executes predictably while the LLM focuses solely on high-level decision-making.

## Separate LLM Reasoning from Deterministic Execution

The foundation of deterministic control lies in treating the LLM as an intent generator rather than an executor. According to [`content/factor-01-natural-language-to-tool-calls.md`](https://github.com/humanlayer/12-factor-agents/blob/main/content/factor-01-natural-language-to-tool-calls.md), the model receives the current thread context and returns a JSON payload describing the next action, such as `request_clarification`, `fetch_git_tags`, or `deploy_backend`.

### JSON Intent Structure

This payload is pure data, not code. It contains no executable logic—only structured descriptions of what the deterministic layer should do next. This separation ensures that the LLM cannot accidentally trigger unintended side effects or execute arbitrary functions.

### The Deterministic Dispatch Layer

As detailed in [`content/factor-04-tools-are-structured-outputs.md`](https://github.com/humanlayer/12-factor-agents/blob/main/content/factor-04-tools-are-structured-outputs.md), a thin dispatcher reads the JSON intent and routes execution to concrete functions written in regular Python. Because this dispatcher is ordinary code, it supports logging, retries, rate-limiting, and metrics without nondeterministic behavior.

## Own Your Control Flow with Break and Continue

The [`content/factor-08-own-your-control-flow.md`](https://github.com/humanlayer/12-factor-agents/blob/main/content/factor-08-own-your-control-flow.md) file describes how to implement interruptible workflow loops using standard control-flow primitives. The deterministic driver loop evaluates each intent and decides whether to pause (break) or proceed (continue).

When the agent requires human input or must wait for a long-running job, the loop **breaks**, persists the thread state, and awaits an external signal. For synchronous operations that merely enrich context, the loop **continues** immediately.

```python

# core loop – deterministic driver for a multi‑step agent

async def handle_next_step(thread: Thread):
    while True:
        # 1️⃣ LLM decides the next intent (pure JSON output)

        next_step = await determine_next_step(thread_to_prompt(thread))

        # 2️⃣ Dispatch based on intent – fully deterministic code

        if next_step.intent == "request_clarification":
            # pause → wait for human reply via webhook

            thread.events.append({"type": "request_clarification", "data": next_step})
            await send_message_to_human(next_step)
            await db.save_thread(thread)
            break      # <-- async break, resumed later

        elif next_step.intent == "fetch_git_tags":
            # synchronous – enrich context then continue

            tags = await git_client.list_tags()
            thread.events.append({"type": "git_tags", "data": tags})
            continue   # <-- stay in loop with new context

        elif next_step.intent == "deploy_backend":
            # pause → human must approve deployment

            thread.events.append({"type": "deploy_request", "data": next_step})
            await request_human_approval(next_step)
            await db.save_thread(thread)
            break      # <-- will be resumed after approval webhook

```

This pattern ensures that the workflow can pause for hours or days without losing context, then resume exactly where it left off.

## Unify Execution State for Auditability

Per [`content/factor-05-unify-execution-state.md`](https://github.com/humanlayer/12-factor-agents/blob/main/content/factor-05-unify-execution-state.md), all events—including tool calls, human approvals, and external API results—append to a single `thread.events` list. This unified business state lives alongside the LLM's context window, allowing the deterministic layer to replay or audit any step without re-running the model.

The `thread.events` structure serves as the source of truth for both the agent's memory and external observability systems.

## Launch, Pause, and Resume via HTTP API

External systems need a control surface to manage workflow lifecycle without touching LLM code. The [`content/factor-06-launch-pause-resume.md`](https://github.com/humanlayer/12-factor-agents/blob/main/content/factor-06-launch-pause-resume.md) specification recommends HTTP endpoints that manipulate thread persistence.

```python
from fastapi import FastAPI, HTTPException

app = FastAPI()

@app.post("/threads/{tid}/resume")
async def resume_thread(tid: str):
    thread = await db.load_thread(tid)
    if not thread:
        raise HTTPException(status_code=404, detail="Thread not found")
    # resume by re‑invoking the deterministic loop

    await handle_next_step(thread)
    return {"status": "resumed"}

```

This API enables CI pipelines, webhooks, or UI dashboards to resume workflows after human approvals or external job completion.

## Compose Micro-Agents into Deterministic DAGs

As outlined in [`content/factor-10-small-focused-agents.md`](https://github.com/humanlayer/12-factor-agents/blob/main/content/factor-10-small-focused-agents.md), complex workflows emerge from chaining single-responsibility micro-agents. Each micro-agent handles one task—such as fetching Git tags or creating issues—producing a mostly deterministic directed acyclic graph (DAG).

In this architecture, only the LLM intent nodes introduce nondeterminism, while the execution graph itself remains predictable, testable, and scalable.

## Summary

- **Separate concerns**: Keep LLM reasoning in JSON intents and business logic in deterministic dispatch functions.
- **Control the loop**: Use `break` to pause for human input or async jobs, and `continue` for synchronous context enrichment.
- **Persist state**: Store all events in `thread.events` to enable durable workflows that survive interruptions.
- **Expose APIs**: Implement HTTP endpoints for pause and resume operations to allow external workflow orchestration.
- **Chain agents**: Build complex systems by composing small, focused agents into deterministic DAGs as described in [`packages/create-12-factor-agent/template/README.md`](https://github.com/humanlayer/12-factor-agents/blob/main/packages/create-12-factor-agent/template/README.md).

## Frequently Asked Questions

### What makes an agent workflow "deterministic"?

A deterministic agent workflow ensures that all business logic, state transitions, and side effects execute through predictable code paths rather than LLM-generated logic. The LLM only produces structured intents; the deterministic dispatch layer in [`content/factor-04-tools-are-structured-outputs.md`](https://github.com/humanlayer/12-factor-agents/blob/main/content/factor-04-tools-are-structured-outputs.md) handles all execution, making behavior repeatable and testable.

### How do you handle long-running operations in agent workflows?

Long-running operations trigger a `break` in the control loop, as shown in [`content/factor-08-own-your-control-flow.md`](https://github.com/humanlayer/12-factor-agents/blob/main/content/factor-08-own-your-control-flow.md). The workflow persists the current `thread` state to the database and exits. An external webhook or API call to `POST /threads/:id/resume` later reloads the state and re-enters the loop exactly where it paused.

### Can I test deterministic agent steps without calling the LLM?

Yes. Because the dispatch layer operates on pure JSON intents, you can unit-test deterministic functions by passing mock intent objects directly. The separation described in [`content/factor-01-natural-language-to-tool-calls.md`](https://github.com/humanlayer/12-factor-agents/blob/main/content/factor-01-natural-language-to-tool-calls.md) means the LLM is only needed for integration tests, while business logic tests run in isolation.

### How does the 12-Factor Agents approach differ from LangChain or similar frameworks?

Unlike monolithic frameworks that often mix LLM calls with execution logic, the 12-Factor Agents approach—documented across factors 1, 4, 5, and 8—explicitly separates intent generation from execution. This yields simpler debugging, clearer ownership of control flow, and the ability to pause/resume workflows without framework-specific abstractions.