Walkthrough: agent audit routing
The agent audit trail walkthrough sends every action to one destination, and fan-out sends every action to every destination. This one keeps the same pipe and the same three systems, but gives each destination a rule: a shipment goes to the ticketing system, a charge to the ledger, an escalation to the ops chat, and nothing goes where its rule is false. The agent still posts to one URL. The pipe decides, and the record shows what it decided. 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 holds a write token (steps 1 to 5 of the connector walkthrough) and that you have read the first walkthrough, which explains the capture URL, signing, and the listener.
1. One capture URL and three destinations
The setup is the first walkthrough’s, done once more on a fresh endpoint so the two records do not mix. One capture URL with signing on, and the three destinations armed for auto-forward. Arming alone would send everything everywhere; the next step is what changes that.
create_endpoint, set_endpoint_signing, and three create_replay_target calls. Each destination gets an id the bindings will name:Ticketing 28WiGa4cA9z9Gz90Ld4zDr armed Ledger 6iNfnJ0izQEl0p2zhiZBvQ armed Ops chat 1yphn1bX9IVvDReBdFj7Iy armed
2. A rule and a shape for each destination
This is the step that turns fan-out into routing. Each destination gets a binding with two parts: a predicate over the incoming body or headers that says whether this capture is for it, and a transformation that says what it receives when the answer is yes. A destination whose predicate is false is skipped for that capture, and nothing raw leaks through.
create_transformation calls and three bind_transformation calls. What is bound:Ticketing predicate: $body.args.action = 'ship'
shape: { "title": $body.summary, "order": $body.args.order, "requestedBy": $body.from }
Ledger predicate: $body.args.action = 'charge'
shape: { "entry": $body.summary, "amount": $body.args.amount, "order": $body.args.order }
Ops chat predicate: $body.args.action = 'escalate'
shape: { "text": $body.summary & " (order " & $body.args.order & ")" }A predicate can read headers too: $headers."x-agent-role" = "billing" routes on a header the agent sets, which is useful when the same body should go to different places for different callers.3. A watch, and a listener per destination
Routing decides where things go; a watch decides what gets flagged. This step labels every escalation as it lands, so the record can count them and a person can find them without reading everything else. Then it starts the three listeners, since these destinations are on your machine, so that the deliveries in the next step complete and their responses land in the record.
register_watch answers with the predicate, the label, and Enabled: true.flurryport listen http://localhost:8771/hook flurryport listen http://localhost:8772/hook flurryport listen http://localhost:8773/hookA destination on the internet needs 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.4. Three actions, three receipts
Now the agent posts three different actions to the one URL, exactly as it would in a day’s work. Each receipt names the destinations the capture was queued to, which is the routing decision made visible at the moment of posting.
post_intent calls with args.action set to ship, charge, and escalate. The three receipts:ship captureId jYT9yVPB4vHO2ZxGTgaMg queued: Ticketing charge captureId 2sOryFD40EWvFJpn61LsTC queued: Ledger escalate captureId 4Id2r1HGreO1qnlOBnxue8 queued: Ops chatOne destination each. The two whose predicates were false for that capture are not in the list, and no delivery was created for them.
5. What the record holds
This is the audit trail with the routing in it. For each capture the record has the one delivery that matched, its status and timing, and what that destination actually received in the shape its binding gave it. Each delivery carries the endpoint’s signature header, so the destination can check it too.
get_capture_executions per capture, one row each:ship Ticketing Completed 200 4 ms 21:35:21.40 -> 21:35:23.77 charge Ledger Completed 200 3 ms 21:35:22.71 -> 21:35:23.81 escalate Ops chat Completed 200 5 ms 21:35:23.84 -> 21:35:25.90
Ticketing { "title": "ship order 1044", "order": "1044", "requestedBy": "agent" }
Ledger { "entry": "charge order 1044", "amount": "129.00", "order": "1044" }
Ops chat { "text": "escalate order 1044 (order 1044)" }The ledger never saw the shipment. The ticketing system never saw the charge. The record shows that not by absence but by the receipts, which name what was queued and where.list_watches answers Escalations: MatchCount 1 with the time it last matched, and get_capture_digest counts the hour by label.What this does not establish
- Not that the routing rule was the right rule. The record shows which rule matched, not whether it should have.
- Not that a destination told the truth about what it did with the delivery.
- Not why the agent chose that action.
- 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
- An audit trail for an agent: the first walkthrough, every payload to every destination.
- Transformations: predicates, JSONata shapes, and standing bindings.
- Auto-forward and targets: arming destinations and what a delivery record holds.
- Watches: predicates and labels.