# How to Implement Pause and Resume Functionality in Agent Systems

> Learn to implement pause and resume for agent systems. Externalize state to a ThreadStore and use thread_id in webhooks for stateless orchestration resilient to restarts and scaling.

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

---

**Implement pause and resume functionality by externalizing agent state to a durable `ThreadStore` and passing a unique `thread_id` through webhook payloads, enabling stateless orchestration that survives process restarts and scaling events.**

The **12-factor-agents** framework from humanlayer treats an agent as a deterministic program that can be launched, paused, and resumed through simple HTTP APIs. According to the source code in `packages/create-12-factor-agent/template/src`, this pattern relies on durable thread persistence and stateless webhook handlers to create interruptible AI workflows. By externalizing state from the execution context, developers can build multiplayer agents that pause for human input and resume hours later without data loss.

## Core Architecture of Pause and Resume

### Durable Thread Persistence (state.ts)

In [`packages/create-12-factor-agent/template/src/state.ts`](https://github.com/humanlayer/12-factor-agents/blob/main/packages/create-12-factor-agent/template/src/state.ts), the `ThreadStore` interface defines the contract for saving execution traces. The reference implementation uses `FileSystemThreadStore` to serialize threads as JSON, but the abstraction allows swapping in Redis, PostgreSQL, or SQLite without changing orchestration logic.

### Stateless HTTP Orchestration (server.ts)

The `outerLoop` handler in [`packages/create-12-factor-agent/template/src/server.ts`](https://github.com/humanlayer/12-factor-agents/blob/main/packages/create-12-factor-agent/template/src/server.ts) exposes a single `POST /api/v1/conversations` endpoint. This endpoint receives events from the Humanlayer platform and uses the `thread_id` from the webhook payload to retrieve persisted state via `store.get(threadId)`.

### Deterministic Inner Loop (agent.ts)

The `agentLoop` function in [`packages/create-12-factor-agent/template/src/agent.ts`](https://github.com/humanlayer/12-factor-agents/blob/main/packages/create-12-factor-agent/template/src/agent.ts) runs deterministic LLM reasoning through `b.DetermineNextStep()`. When the loop encounters an intent requiring external validation—such as `"request_more_information"` or a sensitive tool like `divide`—it returns control to the outer loop without mutating external resources.

## The Pause and Resume Lifecycle

### Step 1: Launch

A client sends a `conversation.created` event to `POST /api/v1/conversations`. The server creates a new `Thread`, persists it via `store.create()`, and immediately invokes the inner loop.

### Step 2: Inner Loop Execution

The deterministic loop processes events until it reaches a decision point. If the next step requires human approval, the loop yields.

### Step 3: Pause via Human Contact

When human input is required, the server calls `hl.createHumanContact()` with a payload containing `state: { thread_id: threadId }`. The server responds with HTTP 200 and awaits a webhook, effectively pausing execution.

### Step 4: Resume via Webhook

Humanlayer sends a `human_contact.completed` webhook containing the same `thread_id`. The handler extracts this ID, loads the thread with `store.get(threadId)`, appends the human response, and re-enters the inner loop.

### Step 5: Completion

When the inner loop reaches a terminal intent like `done_for_now`, the final state is persisted and the agent shuts down cleanly.

## Code Implementation Examples

### Starting a Conversation (Launch)

```typescript
// Client request to launch the agent
await fetch('https://my-agent.example.com/api/v1/conversations', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    type: 'conversation.created',
    event: {
      user_message: 'What is 7 × 8?',
      contact_channel_id: 42,
      agent_name: 'calculator'
    }
  })
});

```

### Creating a Pause Point

```typescript
// server.ts - Triggering a pause when human input needed
if (['request_more_information', 'done_for_now'].includes(lastEvent.data.intent)) {
  hl.createHumanContact({
    spec: {
      msg: lastEvent.data.message,
      state: { thread_id: threadId }  // Critical: passes thread ID to webhook
    }
  });
}

```

### Resuming After Human Response

```typescript
// server.ts - Handling human_contact.completed webhook
case "human_contact.completed":
  const threadId = body.event.spec.state?.thread_id;
  const thread = await store.get(threadId);
  
  // Append the human response to the thread
  thread.events.push({
    type: "human_response",
    data: { msg: body.event.status.response }
  });
  
  // Resume execution
  const newThread = await innerLoop(thread);
  await store.update(threadId, newThread);

```

### Implementing a Custom ThreadStore

```typescript
// Example: Redis implementation of ThreadStore interface
class RedisThreadStore implements ThreadStore {
  async get(threadId: string): Promise<Thread> {
    // Redis fetch logic
  }
  
  async update(threadId: string, thread: Thread): Promise<void> {
    // Redis write logic
  }
}

```

## Key Design Principles

- **Externalized State**: The `thread_id` is the only state required to resume; the orchestration layer is stateless.
- **Deterministic Execution**: The inner loop in [`agent.ts`](https://github.com/humanlayer/12-factor-agents/blob/main/agent.ts) never mutates external resources directly; it only updates the in-memory thread object until explicitly persisted via `store.update()`.
- **Unified Event Schema**: All webhooks (`conversation.created`, `human_contact.completed`, `function_call.completed`) share the same structure, simplifying routing in [`server.ts`](https://github.com/humanlayer/12-factor-agents/blob/main/server.ts).
- **Storage Agnostic**: The `ThreadStore` interface in [`state.ts`](https://github.com/humanlayer/12-factor-agents/blob/main/state.ts) decouples business logic from persistence implementation.

## Summary

- Implement pause and resume by externalizing agent threads to a durable `ThreadStore` interface, as defined in [`packages/create-12-factor-agent/template/src/state.ts`](https://github.com/humanlayer/12-factor-agents/blob/main/packages/create-12-factor-agent/template/src/state.ts).
- Use a `thread_id` field in webhook payloads to maintain continuity across asynchronous human interactions.
- Handle all lifecycle events through a single HTTP endpoint (`POST /api/v1/conversations`) that routes based on event type.
- Ensure the inner loop is deterministic and only mutates external resources after explicit persistence calls.
- Support horizontal scaling and process restarts by keeping the orchestration layer stateless.

## Frequently Asked Questions

### What makes an agent "resumable" in the 12-factor model?

An agent is resumable when its complete execution context is serialized to durable storage via the `ThreadStore` interface and can be retrieved using a unique identifier passed through webhook payloads. This eliminates dependency on in-memory state between requests, allowing the agent to survive process restarts.

### How does the thread_id maintain state across asynchronous webhooks?

The `thread_id` travels in the `state` field of every Humanlayer request (e.g., `hl.createHumanContact({ spec: { state: { thread_id: threadId } } })`). When the webhook returns, the server extracts this ID to load the exact execution context via `store.get(threadId)`, enabling seamless resumption at the precise pause point.

### Can I use a database other than the filesystem for persistence?

Yes. The `ThreadStore` interface in [`packages/create-12-factor-agent/template/src/state.ts`](https://github.com/humanlayer/12-factor-agents/blob/main/packages/create-12-factor-agent/template/src/state.ts) abstracts persistence logic. You can implement this interface for Redis, PostgreSQL, or DynamoDB without modifying the pause/resume logic in [`server.ts`](https://github.com/humanlayer/12-factor-agents/blob/main/server.ts) or [`agent.ts`](https://github.com/humanlayer/12-factor-agents/blob/main/agent.ts), as the outer loop only depends on the interface methods `create`, `get`, and `update`.

### How does this pattern differ from traditional stateful agent architectures?

Traditional architectures maintain agent state in long-running processes or in-memory caches, making them fragile to restarts and difficult to scale horizontally. The 12-factor approach externalizes all state to a `ThreadStore`, making the HTTP handlers stateless and allowing any process instance to resume the agent by loading the thread from storage.