> ## Documentation Index
> Fetch the complete documentation index at: https://checkfu.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Migrate from your own agent loop

> Move a hand-rolled while-loop agent onto Checkfu: what you stop maintaining, what maps to what, and the before/after code.

Most teams arrive here running a loop they built: call the model, execute the tool calls, append to a history array, decide when to stop, hope the process doesn't die mid-task. This guide maps that loop onto Checkfu piece by piece.

## What you stop maintaining

| You maintain today                                      | On Checkfu                                                                                                                                      |
| ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| A history array you assemble per model call             | The Session's durable event log; the harness rebuilds context from it                                                                           |
| A `while` loop deciding when the task is done           | The Run settles itself; a non-`requires_action` idle event ends the turn, or an [Outcome](/concepts/outcomes) grades "done" against your rubric |
| Tool-call plumbing and per-tool credentials in env vars | [Connections](/concepts/capabilities) with vault custody; the harness calls one MCP endpoint and never sees a credential                        |
| "Is it safe?" if-statements before dangerous calls      | [PermissionAssignments, ActionPolicies, and ActionApprovals](/concepts/tenancy-and-governance): governance the caller can't skip                |
| A process that must not die mid-task                    | Runs survive worker death; the log is the truth and execution resumes from it                                                                   |
| A cron container for scheduled jobs                     | [Automations](/concepts/automations): schedule, inbound webhook, or subscription triggers                                                       |
| Per-user token accounting you bolt on later             | The usage ledger, attributed per [Principal](/reference/access) from day one                                                                    |

The deeper shift: your loop's *state* was trapped in process memory. On Checkfu the state is the log, so anything your loop did implicitly (retries, resume, audit, cost tracking) becomes a platform property you read instead of code you own.

## The loop, before and after

**Before**, the shape of every hand-rolled agent:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const history: Message[] = []
history.push({ role: "user", content: task })
while (true) {
  const reply = await model.chat(history, tools)
  history.push(reply)
  if (!reply.toolCalls?.length) break            // done? who knows
  for (const call of reply.toolCalls) {
    const result = await runTool(call)           // your plumbing, your creds
    history.push({ role: "tool", content: result })
  }
}                                                 // process dies → task gone
```

**After**, create a Session once, drive it, and read the settled reply.

`CHECKFU_API_KEY` and `CHECKFU_WORKSPACE_ID` come from [Get access](/reference/access#the-three-variables-ready).

<CodeGroup>
  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  SESSION=$(curl -s --request POST https://api.checkfu.com/v1/sessions \
    --header "Authorization: Bearer $CHECKFU_API_KEY" \
    --header "Checkfu-Version: 2026-08-27" \
    --header "Idempotency-Key: sess-$TASK_ID" \
    --header "Content-Type: application/json" \
    --data '{ "agent": "'"$AGENT_ID"'", "principal": "'"$PRINCIPAL_ID"'" }' | jq -r .id)

  curl -s --request POST "https://api.checkfu.com/v1/sessions/$SESSION/events" \
    --header "Authorization: Bearer $CHECKFU_API_KEY" \
    --header "Checkfu-Version: 2026-08-27" \
    --header "Idempotency-Key: msg-$TASK_ID" \
    --header "Content-Type: application/json" \
    --data '{
      "type": "user.message",
      "payload": {
        "content": "'"$TASK"'",
        "authored_by": "'"$PRINCIPAL_ID"'",
        "caused_by": { "kind": "api" }
      }
    }'

  # Then tail GET /v1/sessions/$SESSION/events/stream for the reply.
  ```

  ```ts TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
  import { Checkfu } from "@checkfu/sdk"

  const checkfu = new Checkfu({
    apiKey: process.env.CHECKFU_API_KEY,
  })

  const session = await checkfu.sessions.create(
    { agent: agentId, principal: userPrincipalId },
    { idempotencyKey: `sess-${taskId}` },
  )

  await checkfu.sessions.events.send(
    session.id,
    {
      type: "user.message",
      payload: { content: task, authored_by: userPrincipalId, caused_by: { kind: "api" } },
    },
    { idempotencyKey: `msg-${taskId}` },
  )

  let reply: string | undefined
  for await (const event of checkfu.sessions.events.stream(session.id)) {
    // An agent.message can arrive with its content withheld by a delivery
    // screen, so narrow before reading it.
    if (event.type === "agent.message" && "content" in event.payload) {
      reply = event.payload.content
    }
    const terminal =
      event.type === "session.status_completed" ||
      event.type === "session.status_failed" ||
      event.type === "session.status_canceled"
    const idleAfterTurn =
      event.type === "session.status_idle" &&
      event.payload.stop_reason !== "requires_action"
    if (terminal || idleAfterTurn) break
  }
  ```
</CodeGroup>

A non-`requires_action` `session.status_idle` is your ordinary "done" signal, and it lives in the log rather than on the connection, so a process that dies before reading it finds it waiting on restart. An idle event with `stop_reason: "requires_action"` is an intermediate Run boundary; keep following so you do not miss the admitted continuation. Idle ends a turn, not the Session; [the settlement table](/guides/resume-a-stream#settlement) covers the terminal statuses and continuation case.

The loop body did not move into a replacement in your code. It moved into the **harness**, which you pick per agent: managed Claude Agent or Codex, a packaged image, or your own ACP binary (see [Harnesses and models](/concepts/harnesses-and-models)). Your code's job shrinks to three things: start sessions, send messages, and react to events.

## Concept mapping for things your loop had names for

| Your concept                         | Checkfu name                            | Where                                              |
| ------------------------------------ | --------------------------------------- | -------------------------------------------------- |
| "conversation" / thread state        | Session                                 | [Sessions and runs](/concepts/sessions-and-runs)   |
| one task execution                   | Run                                     | [Sessions and runs](/concepts/sessions-and-runs)   |
| system prompt + tools + model config | Agent definition (published, versioned) | [Agents](/concepts/agents)                         |
| the user you're acting for           | Principal                               | [Get access](/reference/access)                    |
| tool credentials                     | Connection + custody                    | [Capabilities](/concepts/capabilities)             |
| "ask a human first"                  | ActionApproval                          | [ActionApprovals](/concepts/action-approvals)      |
| RAG store / notes file               | Memory store                            | [Memory](/concepts/memory)                         |
| repo checkout in the container       | Project mount                           | [Projects](/concepts/projects)                     |
| sub-agents you spawn                 | Multiagent Session threads              | [Multiagent threads](/concepts/multiagent-threads) |
| "is the output actually good?"       | Outcome with a rubric                   | [Outcomes](/concepts/outcomes)                     |

## Migration order that works

1. **Lift the definition.** Port your system prompt and model choice into an Agent, publish v1, and run it with the [quickstart](/getting-started/quickstart) flow. No tools yet.
2. **Move the tools.** One Connection at a time, under the [default policy](/concepts/tenancy-and-governance) (reads allowed, writes require approval). Your riskiest tool goes last, once you trust the approval flow.
3. **Delete the loop.** Swap your entry point to create-session-and-drive, keeping your old loop behind a flag until the event stream feels boring.
4. **Adopt what your loop never had.** Per-user [Budgets](/guides/attribute-usage), [webhooks](/guides/receive-webhooks) instead of polling, and [exports](/guides/export-your-log) for compliance.

## Next steps

<CardGroup cols={2}>
  <Card title="Run your first session" icon="rocket" href="/getting-started/quickstart">
    The 10-minute version of steps 1 and 3.
  </Card>

  <Card title="Add an agent to your app" icon="plug" href="/guides/add-an-agent-to-your-app">
    Embedding on a surface your users already use.
  </Card>
</CardGroup>
