Walkthrough: agent audit fan-out
The agent audit trail walkthrough puts one capture URL in front of one destination. This page puts the same pipe in front of the three systems an agent acts on, and every action goes to all three. Afterwards the record answers, without asking the agent: what was sent, when, to which destination, and what each one said back. Every prompt below was run on 2026-09-08 against flurryport 0.6.12, and the answers are what came back.
It assumes the connector is installed and holds a write token, which is steps 1 to 5 of the connector walkthrough. The three destinations here run on localhost so anyone can follow along; a destination on the internet works the same and needs no listener.
1. One capture URL, signed
The first decision is where the agent’s actions pass through. This step creates one capture URL and turns on signature checking, so only posts signed with this endpoint’s key are accepted, and an unsigned one is refused before anything is stored. From here on, the agent posts to this URL instead of straight to each system, and that is the whole change on the agent’s side.
create_endpoint and set_endpoint_signing, then get_capture_url for the address:captureUrl: https://api.flurryport.io/api/v1/capture/<project>/agent-actions signing: live; unsigned posts bounce 401 keyRef: signing:6EmYJCbXQksgRKquRRHZlf
2. The systems the agent already talks to
Next, tell the pipe where things go. This step adds the three destinations the agent was already using, a ticketing system, a ledger, and an ops chat, and arms each one so every capture fans out to it automatically. Nothing about those systems changes; the pipe sits in front of them and keeps a delivery record for each.
create_replay_target calls with auto-forward on. Each answers the same way:Ticketing 6cRyeh2P1mohUXUvU58BM8 autoReplay: true class: Local Target is ARMED (autoReplay on): captures fan out to it automatically.Local destinations are reached through the listener on your machine, one per destination (flurryport 0.6.12 or newer takes the address; older versions offer a menu). Each attaches to its target by address, forwards every delivery queued to it on to your localhost service with the signature header intact, prints each one as it goes, and records the response into the record:
flurryport listen http://localhost:8771/hook flurryport listen http://localhost:8772/hook flurryport listen http://localhost:8773/hookA destination on the internet is delivered directly, with no listener.
flurryport echo http://localhost:8771/hook flurryport echo http://localhost:8772/hook flurryport echo http://localhost:8773/hookEach answers 200, logs every request, and mirrors the headers and body back. Your assistant can start them too, with
start_echo_server once per port, but then only the record shows you what arrived. Prove the pipe against the echoes, then swap in the real services.3. Shape what each destination receives
Destinations rarely want the same payload. This step binds a transformation to one of them, so the ticketing system receives a title, the order, and who asked, and nothing else, while the other two receive the post as sent. The record keeps both: what the agent posted, and what each destination was actually given.
create_transformation takes a JSONata expression, and bind_transformation pins it to the Ticketing target as a standing binding:expression: { "title": $body.summary, "order": $body.args.order, "requestedBy": $body.from }
bound to: Ticketing, predicate true, standing: true4. Label the actions that matter
Most of what passes through a pipe is routine. This step registers a watch with a predicate, so any capture where the agent ships something is labelled as it lands, counted, and easy to find later. A watch changes nothing about delivery; it makes one kind of action stand out in the record.
register_watch answers:Predicate: $body.args.action = 'ship' Label: An agent shipped something Enabled: true
5. The agent acts
Now the agent does its job the way it always did, except that it posts to the capture URL. This step sends one typed action and shows the receipt: the post was signature-checked and stored, and three deliveries were queued, one per destination. The receipt is what the agent can show its human at once; the deliveries are what the record fills in over the next seconds.
post_intent with this body, signed on the way out in a header you never type:{ "v": 1, "kind": "message", "from": "agent", "to": "all",
"verb": "fp:act", "summary": "Ship order 1043 to the warehouse",
"args": { "order": "1043", "action": "ship" } }The receipt comes back at once:status: accepted captureId: 29hqau8ElVTpbZPx7FLZ79 postedAt: 2026-09-08T21:14:39.916Z executions: 3o3eddpGBiuP9GSQRpZ0GW -> Ledger 4aVrbbHW4fNfsJ74yWHrEA -> Ops chat 5PkahIjx2nPryk2zTvdV5d -> Ticketing
6. What the record holds
This is the audit trail. This step reads it back three ways: every delivery for the capture with its status and timing, what one destination actually received, and a summary of the hour. None of it comes from the agent; all of it was written by the pipe as things passed through.
get_capture_executions returns one row per destination:Ticketing Completed 200 3 ms created 21:15:50.40 completed 21:15:51.18 Ledger Completed 200 4 ms created 21:15:50.39 completed 21:15:52.73 Ops chat Completed 200 3 ms created 21:15:50.40 completed 21:15:52.75
X-Flurry-Signature: 821694275978cc…6b32c
{ "title": "Ship order 1043 to the warehouse", "order": "1043", "requestedBy": "agent" }The ledger and the ops chat received the body as posted, with the same header.get_capture_digest counts what passed through, by endpoint, by label, and by hour, and says how long it is kept:TotalCount: 1 RejectedCount: 0 ByLabel: An agent shipped something 1 Retention: RetentionDays 100, NextExpiryAt 2026-11-18The label came from the watch in step 4;
list_watches shows it with MatchCount: 1 and the time it last matched.7. Check it later, and decide how long it lasts
An audit trail is only useful when you come back to it. This step is the same reads months on, plus the two decisions that are cheaper before you need the history: the retention window and who may read the record without writing to it.
get_capture returns the body as sent and, beside it, what the server fixed when it landed: the label of the key that signed, matched by the server, and the time. The from field in the body is what the sender wrote; the matched label is who actually signed.What this does not establish
- Not that a human approved the action. Signed does not mean human-approved.
- Not that a destination told the truth about what it did with the delivery.
- Not why the agent decided anything.
- Nothing the agent sent somewhere other than the capture URL. The pipe records what went through it.
- Nothing outside the window you chose. A trail is worth what it outlives.
See also
- Route an agent’s actions where they belong: the second walkthrough, a predicate on each destination.
- Auto-forward and targets: arming destinations and what a delivery record holds.
- Transformations: the JSONata expressions and standing bindings used in step 3.
- Watches: predicates and labels.
- Rooms: when several agents and their people write to one URL, each gets its own signing key.
- Agent audit trail, agent action receipt, and action provenance: the ideas behind the steps.