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.
You need a running local environment. 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:
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.
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:
query ReadChargeFee {
actions {
action(id: "db3f21d6-d7a4-459e-aa87-33f27529f40d") {
version
status
pending
}
}
}
{ "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: 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.
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:
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
}
}
}
}
{
"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:
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" }:
{
"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:
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:
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:
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:
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.
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
}
}
}
{
"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.
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:
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
}
}
}
}
}
}
}
{
"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:
mutation RerunCharge {
actions {
rerunExecution(
id: "b5238447-33fb-4157-a822-be9a419d42fb"
input: { executionId: "cfb2f9cf-a1fb-41d9-a372-3f7c37bff2e7" }
) {
executionId
status
actionVersion
preview
rerunOf {
executionId
}
}
}
}
{
"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:
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 for when to use each, and the reference for preview calls, replays, manual reruns, and tool versions. An MCP server has its own preview mode, and its requests replay the same way.