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.

1
"Create an endpoint called Agent actions, routed, turn on signing, and add three destinations with auto-forward on: Ticketing at http://localhost:8771/hook, Ledger at http://localhost:8772/hook, Ops chat at http://localhost:8773/hook." 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.

1
"Bind three rules, one per destination, and make them standing. Ticketing gets captures where args.action is ship, shaped as a title, the order, and who asked. Ledger gets charges, shaped as an entry, an amount, and the order. Ops chat gets escalations, as one line of text." Three 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.

1
"Watch this endpoint for any capture where args.action is escalate, and label it: An agent escalated." register_watch answers with the predicate, the label, and Enabled: true.
2
Start one listener per destination, in three terminals (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, 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 needs no listener.
3
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.

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.

1
"Post three actions to Agent actions, routed: ship order 1044; charge order 1044 for 129.00; escalate order 1044." Three 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 chat
One 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.

1
"Show me every delivery for those three captures." 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
2
"What did each destination receive?" The shaped bodies, one per destination:
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.
3
"How many escalations today?" 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