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.

1
"Create an endpoint called Agent actions in my project and turn on signing." The assistant runs 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.

1
"Add three destinations to Agent actions and forward every capture to each: Ticketing at http://localhost:8771/hook, Ledger at http://localhost:8772/hook, Ops chat at http://localhost:8773/hook." Three 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/hook
A destination on the internet is delivered directly, with no listener.
2
No ticketing, ledger, or chat service on hand? Run echo servers yourself, one terminal each, so you can watch what lands at each:
flurryport echo http://localhost:8771/hook
flurryport echo http://localhost:8772/hook
flurryport echo http://localhost:8773/hook
Each 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.

1
"The ticketing system only wants a title, the order, and who asked. Make a transformation for that and bind it to Ticketing so it runs on every capture." 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: true

4. 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.

1
"Watch this endpoint for any capture where args.action is ship, and label it: An agent shipped something." 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.

1
"Post to Agent actions: ship order 1043 to the warehouse." The assistant runs 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.

1
"Show me every delivery for that capture." 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
2
"What did the ticketing system receive?" The transformed body, carrying the endpoint’s signature header so the destination can check it too:
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.
3
"Summarize the last hour on Agent actions." 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-18
The 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.

1
"Show me capture 29hqau8ElVTpbZPx7FLZ79 and who signed 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.
2
Captures are kept for your plan’s window, and the window is on the plans and limits page. A question you will ask in three weeks needs a window longer than three weeks. Reading does not need write access: an invite with the monitor role reads the record and cannot post, and on that scoped credential personal data in what is read back is masked.

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