# Previewing and Replaying a Change

Preview an agent that charges a fee, run it for real, then replay its recorded turn against a new version of the tool before you switch to it.

A tool that writes to the ledger is risky to change: once a caller uses the new version, every call commits. This tutorial changes such a tool safely. You build a tool that charges a monthly fee by posting a transaction, and an agent that calls it. You preview the agent to see the transaction it would post without posting it, run it for real, and then change the fee. Before the agent uses the new fee, you replay its recorded turn against the new version, compare the two runs, and only then pin the new version in the agent.

> **Note:**
>
> You need a running [local environment](https://www.twisp.com/docs/infrastructure/local-environment.md). Open GraphiQL in the local console and run each step as its own request.

The agent uses the `fixture` model, so every run is deterministic and calls no model. The UUIDs below are examples; if you have run this tutorial before, choose fresh ones, and fresh account and tran code codes.

## 1. Set up the accounts and tran code

Create a customer account, a fee income account, and a tran code that moves a fee from one to the other:

```graphql
mutation SetUpFees {
  customer: createAccount(
    input: {
      accountId: "457b3aa9-015a-4424-a689-b5d75861dde8"
      code: "PREVIEW-REPLAY-CUSTOMER"
      name: "Preview and replay customer"
      normalBalanceType: CREDIT
    }
  ) {
    accountId
  }
  feeIncome: createAccount(
    input: {
      accountId: "bc39941f-2b87-4370-bcba-fa535bc34ade"
      code: "PREVIEW-REPLAY-FEE-INCOME"
      name: "Preview and replay fee income"
      normalBalanceType: CREDIT
    }
  ) {
    accountId
  }
  feeCode: createTranCode(
    input: {
      tranCodeId: "16d4024c-8f31-479e-a1d7-6af863dcbb10"
      code: "PREVIEW_REPLAY_FEE"
      description: "Charge a customer a fee."
      params: [{ name: "account", type: UUID }, { name: "amount", type: DECIMAL }]
      transaction: { description: "'Account fee'" }
      entries: [
        {
          accountId: "params.account"
          units: "params.amount"
          currency: "'USD'"
          entryType: "'FEE_DR'"
          direction: "DEBIT"
          layer: "SETTLED"
        }
        {
          accountId: "uuid('bc39941f-2b87-4370-bcba-fa535bc34ade')"
          units: "params.amount"
          currency: "'USD'"
          entryType: "'FEE_CR'"
          direction: "CREDIT"
          layer: "SETTLED"
        }
      ]
    }
  ) {
    tranCodeId
  }
}
```

## 2. Create the fee tool

The tool's program posts one `PREVIEW_REPLAY_FEE` transaction for the account it is given, with a fee of 2.00, and returns the transaction ID and the fee.

```graphql
mutation CreateChargeFee {
  actions {
    createAction(
      id: "db3f21d6-d7a4-459e-aa87-33f27529f40d"
      input: {
        name: "preview-and-replay-charge-fee"
        description: "Charge the monthly account fee"
        kind: "tool"
        tool: {
          name: "charge_fee"
          description: "Charge the monthly account fee to one customer account."
          inputSchema: {
            type: "object"
            properties: { account_id: { type: "string", format: "uuid" } }
            required: ["account_id"]
            additionalProperties: false
          }
          outputSchema: { type: "object" }
        }
        policies: [{ effect: ALLOW, actions: [SELECT, INSERT, UPDATE], resources: ["financial.*"] }]
        config: {
          runtime: "starlark"
          src: """
          FEE = "2.00"

          def run(event):
              transaction_id = str(uuid.New())
              graphql(
                  "mutation Charge($id: UUID!, $params: JSON!) { postTransaction(input: {transactionId: $id, tranCode: \"PREVIEW_REPLAY_FEE\", params: $params}) { transactionId } }",
                  {"id": transaction_id, "params": {"account": event["data"]["account_id"], "amount": FEE}},
                  operation_name="Charge",
              )
              return {"transaction_id": transaction_id, "fee": FEE}
          """
        }
      }
    ) {
      actionId
      version
      status
      pending
    }
  }
}
```

The response shows `version: 1`, `status: UNPUBLISHED`, and `pending: ACTIVE`. Poll until the tool is published:

```graphql
query ReadChargeFee {
  actions {
    action(id: "db3f21d6-d7a4-459e-aa87-33f27529f40d") {
      version
      status
      pending
    }
  }
}
```

```json
{ "version": 2, "status": "ACTIVE", "pending": null }
```

Publication advances the version. Version 2 is the published fee tool, and it is the version the agent will call.

## 3. Create the agent in preview mode

The agent is [callable](https://www.twisp.com/docs/reference/actions/agents.md#callable-agents): its `tool` block lets you call it directly with an account ID. Its tool reference pins the fee tool at `version: 2`, so later changes to the tool never reach it until you change the pin, and fixes `account_id` to the agent's own input. `config.mode: "preview"` makes the agent run every call as it would live and discard the writes.

The fixture script answers the agent's first model call with a call to `charge_fee` and its second with a reply.

```graphql
mutation CreateFeeAgent {
  actions {
    createAction(
      id: "a9cdf240-dfef-4c50-8a27-e4f6d04291f3"
      input: {
        name: "preview-and-replay-fee-agent"
        description: "Charges the monthly fee for one account"
        kind: "agent"
        agent: {
          prompt: "Charge the monthly account fee for the account you are given, then confirm."
        }
        tool: {
          name: "fee_agent"
          description: "Charge the monthly fee for one account."
          inputSchema: {
            type: "object"
            properties: { account_id: { type: "string", format: "uuid" } }
            required: ["account_id"]
            additionalProperties: false
          }
        }
        config: {
          runtime: "strands"
          mode: "preview"
          model: {
            provider: "fixture"
            script: [
              {
                responses: [
                  [{ toolUse: { name: "charge_fee", input: {} } }]
                  [{ text: "Charged the monthly fee." }]
                ]
              }
            ]
          }
        }
        tools: [
          {
            name: "preview-and-replay-charge-fee"
            version: 2
            input: { account_id: "tool.input.account_id" }
          }
        ]
        policies: [{ effect: ALLOW, actions: [SELECT, INSERT, UPDATE], resources: ["financial.*"] }]
      }
    ) {
      actionId
      version
      status
      pending
    }
  }
}
```

Poll it as in step 2, with the agent's ID, until it is `ACTIVE` with `pending: null`. It publishes as version 2.

## 4. Preview a charge

Call the agent with `invokeAction`. The call runs the agent's whole turn synchronously:

```graphql
mutation PreviewFee {
  actions {
    invokeAction(
      id: "a9cdf240-dfef-4c50-8a27-e4f6d04291f3"
      input: {
        executionId: "7a0c0b54-8122-4e61-a712-580a8967c07f"
        actionVersion: 2
        input: { account_id: "457b3aa9-015a-4424-a689-b5d75861dde8" }
      }
    ) {
      executionId
      status
      preview
      invocation {
        output
      }
    }
  }
}
```

```json
{
  "executionId": "7a0c0b54-8122-4e61-a712-580a8967c07f",
  "status": "SUCCEEDED",
  "preview": true,
  "invocation": { "output": "Charged the monthly fee." }
}
```

`preview: true` marks an execution whose writes were discarded. The agent's execution and its transcript are kept; the transaction is not.

## 5. Read what the preview would have written

The agent's `charge_fee` call is a child of its execution. A preview call lists in `writeSet` every record version it would have written:

```graphql
query FeeCalls($id: UUID!) {
  actions {
    execution(id: $id) {
      status
      preview
      children(first: 10) {
        nodes {
          actionVersion
          preview
          invocation {
            output
          }
          writeSet {
            recordType
            recordVersion
            data
          }
        }
      }
    }
  }
}
```

With the variables `{ "id": "7a0c0b54-8122-4e61-a712-580a8967c07f" }`:

```json
{
  "status": "SUCCEEDED",
  "preview": true,
  "children": {
    "nodes": [
      {
        "actionVersion": 2,
        "preview": true,
        "invocation": {
          "output": { "fee": "2.00", "transaction_id": "11dfd354-d8ad-43de-b5a3-a80027da565c" }
        },
        "writeSet": [
          {
            "recordType": "Transaction",
            "recordVersion": 1,
            "data": {
              "description": "Account fee",
              "tran_code_id": "16d4024c-8f31-479e-a1d7-6af863dcbb10",
              "…": "…"
            }
          },
          {
            "recordType": "Balance",
            "recordVersion": 1,
            "data": {
              "account_id": "457b3aa9-015a-4424-a689-b5d75861dde8",
              "settled": { "dr_balance": "2.00 USD", "…": "…" },
              "…": "…"
            }
          },
          {
            "recordType": "Balance",
            "recordVersion": 1,
            "data": {
              "account_id": "bc39941f-2b87-4370-bcba-fa535bc34ade",
              "settled": { "cr_balance": "2.00 USD", "…": "…" },
              "…": "…"
            }
          },
          {
            "recordType": "Entry",
            "recordVersion": 1,
            "data": {
              "account_id": "457b3aa9-015a-4424-a689-b5d75861dde8",
              "amount": "2.00 USD",
              "entry_type": "FEE_DR",
              "…": "…"
            }
          },
          {
            "recordType": "Entry",
            "recordVersion": 1,
            "data": {
              "account_id": "bc39941f-2b87-4370-bcba-fa535bc34ade",
              "amount": "2.00 USD",
              "entry_type": "FEE_CR",
              "…": "…"
            }
          }
        ]
      }
    ]
  }
}
```

The write set is the transaction, its two entries, and the two balances they would have changed, each in the form an event's `data` takes. Confirm that none of it committed:

```graphql
query CustomerFees {
  account(id: "457b3aa9-015a-4424-a689-b5d75861dde8") {
    balance {
      settled {
        drBalance {
          formatted(as: { locale: "en-US" })
        }
      }
    }
  }
}
```

`balance` is `null`: the account has never been posted to.

## 6. Go live and charge for real

Satisfied with the preview, switch the agent to `live`. A supplied `config` replaces the whole config, so send all of it:

```graphql
mutation GoLive {
  actions {
    updateAction(
      id: "a9cdf240-dfef-4c50-8a27-e4f6d04291f3"
      input: {
        config: {
          runtime: "strands"
          mode: "live"
          model: {
            provider: "fixture"
            script: [
              {
                responses: [
                  [{ toolUse: { name: "charge_fee", input: {} } }]
                  [{ text: "Charged the monthly fee." }]
                ]
              }
            ]
          }
        }
      }
    ) {
      version
      status
      pending
    }
  }
}
```

Poll until the agent is published again, at version 4, then call it:

```graphql
mutation ChargeFee {
  actions {
    invokeAction(
      id: "a9cdf240-dfef-4c50-8a27-e4f6d04291f3"
      input: {
        executionId: "b5238447-33fb-4157-a822-be9a419d42fb"
        actionVersion: 4
        input: { account_id: "457b3aa9-015a-4424-a689-b5d75861dde8" }
      }
    ) {
      executionId
      status
      preview
      invocation {
        output
      }
    }
  }
}
```

This time `preview` is `false`, and `CustomerFees` returns `"formatted": "$2.00"`. Running `FeeCalls` with this execution's ID shows the `charge_fee` call with `preview: false` and an empty `writeSet`, which is recorded for preview calls only. This execution is the recorded turn you replay in step 8.

## 7. Change the fee

Raise the fee to 2.50. Updating the tool writes a new version:

```graphql
mutation RaiseFee {
  actions {
    updateAction(
      id: "db3f21d6-d7a4-459e-aa87-33f27529f40d"
      input: {
        config: {
          runtime: "starlark"
          src: """
          FEE = "2.50"

          def run(event):
              transaction_id = str(uuid.New())
              graphql(
                  "mutation Charge($id: UUID!, $params: JSON!) { postTransaction(input: {transactionId: $id, tranCode: \"PREVIEW_REPLAY_FEE\", params: $params}) { transactionId } }",
                  {"id": transaction_id, "params": {"account": event["data"]["account_id"], "amount": FEE}},
                  operation_name="Charge",
              )
              return {"transaction_id": transaction_id, "fee": FEE}
          """
        }
      }
    ) {
      version
      status
      pending
    }
  }
}
```

Poll `ReadChargeFee` until it shows `version: 4` and `pending: null`. The agent still calls version 2, because its reference pins it. A reference without a `version` would have called the new fee on the agent's next run.

## 8. Replay the charge against the new fee

`replayExecution` reruns the recorded turn from step 6 and records the result as a new execution beside it. Its `definition` is an unpublished definition of the agent to run instead of a stored version: here, the live agent with its reference pinned to the fee tool's version 4. `model: RECORDED` answers the model calls with the outputs the original turn recorded, so the only difference between the two runs is the tool.

```graphql
mutation ReplayWithNewFee {
  actions {
    replayExecution(
      id: "b5238447-33fb-4157-a822-be9a419d42fb"
      input: {
        executionId: "cb3b9768-9f2c-46fc-b83b-a8e5beab8b19"
        model: RECORDED
        definition: {
          name: "preview-and-replay-fee-agent"
          description: "Charges the monthly fee for one account"
          kind: "agent"
          agent: {
            prompt: "Charge the monthly account fee for the account you are given, then confirm."
          }
          tool: {
            name: "fee_agent"
            description: "Charge the monthly fee for one account."
            inputSchema: {
              type: "object"
              properties: { account_id: { type: "string", format: "uuid" } }
              required: ["account_id"]
              additionalProperties: false
            }
          }
          config: {
            runtime: "strands"
            mode: "live"
            model: {
              provider: "fixture"
              script: [
                {
                  responses: [
                    [{ toolUse: { name: "charge_fee", input: {} } }]
                    [{ text: "Charged the monthly fee." }]
                  ]
                }
              ]
            }
          }
          tools: [
            {
              name: "preview-and-replay-charge-fee"
              version: 4
              input: { account_id: "tool.input.account_id" }
            }
          ]
          policies: [
            { effect: ALLOW, actions: [SELECT, INSERT, UPDATE], resources: ["financial.*"] }
          ]
        }
      }
    ) {
      executionId
      status
      replayOfId
    }
  }
}
```

```json
{
  "executionId": "cb3b9768-9f2c-46fc-b83b-a8e5beab8b19",
  "status": "QUEUED",
  "replayOfId": "b5238447-33fb-4157-a822-be9a419d42fb"
}
```

The replay runs on its own and finishes within a moment. Whatever the agent's definition says about `mode`, a replay commits nothing.

> **Note:**
>
> To replay a stored version instead, pass `actionVersion`. A replay calls the tool versions the original turn used, unless the replayed definition pins others, so testing a new tool version needs a definition or version that pins it.

## 9. Compare the two runs

Read the replay, its calls and their write sets, and the original's calls through `replayOf`:

```graphql
query CompareReplay {
  actions {
    execution(id: "cb3b9768-9f2c-46fc-b83b-a8e5beab8b19") {
      status
      preview
      replay {
        model
        data
      }
      invocation {
        output
      }
      children(first: 10) {
        nodes {
          actionVersion
          invocation {
            output
          }
          writeSet {
            recordType
            data
          }
        }
      }
      replayOf {
        children(first: 10) {
          nodes {
            actionVersion
            invocation {
              output
            }
          }
        }
      }
    }
  }
}
```

```json
{
  "status": "SUCCEEDED",
  "preview": true,
  "replay": { "model": "RECORDED", "data": "CURRENT" },
  "invocation": { "output": "Charged the monthly fee." },
  "children": {
    "nodes": [
      {
        "actionVersion": 4,
        "invocation": {
          "output": { "fee": "2.50", "transaction_id": "8eff84e0-a72c-477b-b47d-8a8afb278327" }
        },
        "writeSet": [
          { "recordType": "Transaction", "data": { "…": "…" } },
          {
            "recordType": "Balance",
            "data": {
              "account_id": "bc39941f-2b87-4370-bcba-fa535bc34ade",
              "settled": { "cr_balance": "4.50 USD", "…": "…" },
              "…": "…"
            }
          },
          {
            "recordType": "Balance",
            "data": {
              "account_id": "457b3aa9-015a-4424-a689-b5d75861dde8",
              "settled": { "dr_balance": "4.50 USD", "…": "…" },
              "…": "…"
            }
          },
          {
            "recordType": "Entry",
            "data": {
              "account_id": "bc39941f-2b87-4370-bcba-fa535bc34ade",
              "amount": "2.50 USD",
              "entry_type": "FEE_CR",
              "…": "…"
            }
          },
          {
            "recordType": "Entry",
            "data": {
              "account_id": "457b3aa9-015a-4424-a689-b5d75861dde8",
              "amount": "2.50 USD",
              "entry_type": "FEE_DR",
              "…": "…"
            }
          }
        ]
      }
    ]
  },
  "replayOf": {
    "children": {
      "nodes": [
        {
          "actionVersion": 2,
          "invocation": {
            "output": { "fee": "2.00", "transaction_id": "4cfe0849-18e9-49cd-a026-1f91d367c2d1" }
          }
        }
      ]
    }
  }
}
```

The agent's reply is the same, the original call ran tool version 2 and charged 2.00, and the replay ran version 4 and would have charged 2.50. The balances in the write set read 4.50 because `data: CURRENT`: the replay reads the ledger as it is now, which already holds the 2.00 posted in step 6. The transaction IDs differ because the program generates a new one on every run. Run `CustomerFees` again: the balance is still `$2.00`.

## 10. Contrast a rerun

`rerunExecution` is not a test. It runs the source again for real, at the source's pinned agent version, as a new start by you:

```graphql
mutation RerunCharge {
  actions {
    rerunExecution(
      id: "b5238447-33fb-4157-a822-be9a419d42fb"
      input: { executionId: "cfb2f9cf-a1fb-41d9-a372-3f7c37bff2e7" }
    ) {
      executionId
      status
      actionVersion
      preview
      rerunOf {
        executionId
      }
    }
  }
}
```

```json
{
  "executionId": "cfb2f9cf-a1fb-41d9-a372-3f7c37bff2e7",
  "status": "QUEUED",
  "actionVersion": 4,
  "preview": false,
  "rerunOf": { "executionId": "b5238447-33fb-4157-a822-be9a419d42fb" }
}
```

Once it succeeds, `FeeCalls` with its ID shows a call to tool version 2 that charged 2.00, and `CustomerFees` returns `$4.00`: the customer has been charged twice. Use a rerun to retry work that should happen, such as a charge that failed, and a replay to see what a change would do.

## 11. Pin the new version

The replay showed the new fee doing what you intended. Point the agent at it by updating its reference. A supplied `tools` list replaces the whole list:

```graphql
mutation PinNewFee {
  actions {
    updateAction(
      id: "a9cdf240-dfef-4c50-8a27-e4f6d04291f3"
      input: {
        tools: [
          {
            name: "preview-and-replay-charge-fee"
            version: 4
            input: { account_id: "tool.input.account_id" }
          }
        ]
      }
    ) {
      version
      status
      pending
    }
  }
}
```

Poll until the agent is published at version 6, then run `ChargeFee` again with `actionVersion: 6` and a fresh `executionId`, such as `dc4937ff-b529-4c3b-b254-13c1e7918f8f`. `FeeCalls` shows the call ran tool version 4 with `"fee": "2.50"`, and `CustomerFees` returns `$6.50`.

## Troubleshooting

| Symptom                                              | Check                                                                                                                             |
| ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `children` is empty in the `invokeAction` response   | Read the execution again with `FeeCalls` once the request has returned.                                                           |
| Step 1 fails with a unique constraint violation      | You have run the tutorial before. Choose fresh UUIDs and codes, and use the new tran code's `code` in the tool's program.         |
| The replay ran the old fee                           | A replay calls the tool versions the original turn used. Pin the new version in the replay's `definition`, as in step 8.          |
| `replayExecution` reports missing `kind` or `config` | `definition` is a complete definition, validated as `createAction` validates one. Send every field, not only the one you changed. |
| `rerunExecution` of the replay is rejected           | A replay cannot be rerun. Replay the original again with a new `executionId`.                                                     |

Read [Preview and replay](https://www.twisp.com/docs/actions/preview-and-replay.md) for when to use each, and the reference for [preview calls](https://www.twisp.com/docs/reference/actions/executions.md#preview-calls), [replays](https://www.twisp.com/docs/reference/actions/executions.md#replays), [manual reruns](https://www.twisp.com/docs/reference/actions/executions.md#manual-reruns), and [tool versions](https://www.twisp.com/docs/reference/actions/tools.md#versions). An [MCP server](https://www.twisp.com/docs/reference/actions/mcp.md#preview-mode) has its own preview mode, and its requests replay the same way.
