Debug taps — trace live data through any node
Debug taps
A tap is a passive observer you attach to one node of a published flow. It mirrors the node's input (the packet arriving) and output (the packet it produced) into a durable, queryable hit log — so you can see the actual data flowing through a node while the flow runs, and change what you're watching on the fly without editing or republishing the flow.
Taps are the debugging counterpart to flow alerts: alerts capture failures; taps capture data through a chosen node.
Why it works the way it does
The FlowDSL runtime is single-path — selectNext picks exactly one outgoing
edge per node — so there is no execution fan-out. A tap therefore does not run
as a parallel node; it rides the existing FlowEventSink event stream (the same
stream that powers Trace/Live/Alerts). On each node.started / node.done
event matching an enabled tap, the TapSink records a hit. Zero runtime change,
works on any published flow, toggle without a republish.
- Package:
go-flowdsl/flowexec/taps(models,Store,MemStore,TapSink). - Mongo store:
flowexec/storage-mongo/store/tapstore—flow_taps(configs, persistent + shared across the api / admin-api containers via the same DB) andflow_tap_hits(a 24 h TTL, so debug data self-cleans). - Wiring: the
flowexecmodule mounts theTapSinkoutermost on the sink chain (broadcaster → taps → alerts → raw) and reloads its match cache on startup + after every tap CRUD.
Match key gotcha: a run's
FlowEvent.FlowIDis the workflow document ID (e.g.asyncshop-checkout-standard), not the flowexec flow-record ID (flow.<uuid>) the admin API uses. Each tap stores aWorkflowID(resolved from the published version at create time); the sink matches events by that but records each hit under the flow-record ID so the admin trace query works.
See it in action
Recorded on the real Redelay admin — publish a flow, tap a node, run it, watch the packet transform live. (See E2E & demo videos for how this was made.)
Try it in 60 seconds — the debug-playground demo
The flowtemplates module ships examples/debug-playground — a tiny,
infra-free flow (no Kafka/Mongo needed) built for exactly this. It takes an order
packet {order_id, amount_minor, currency}, adds VAT (gross_minor = amount × 1.23) with a core/math node, and renders a one-line summary with
core/template-render.
- Studio → Create flow → Debug playground → Publish.
- Flows → Debug: pick the flow, tap the gross node (capture both), Add tap.
- Launch a run (
POST /flows/{id}/runswith{"input":{"order_id":"A-100", "amount_minor":1000,"currency":"PLN"}}, or Run in Studio). - The Trace panel shows the
grossnode's input (amount_minor: 1000) and output (gross_minor: 1230) — the packet transforming, live. Add a second tap on summary to watch the rendered string appear.
Because it uses only in-process core nodes, the demo runs anywhere and is the
fixture the taps end-to-end tests exercise (flowexec/taps_e2e_test.go).
Using it — admin
Admin → Flows → Debug (/flows/debug):
- Pick a flow, then a node to tap.
- Choose capture (input / output / both) and action (log; log + raise an alert; or log + notify me, an in-app notification each time the node fires). Optionally bound volume on a busy flow with Sample 1-in-N and/or an only when field=value filter, then Add tap.
- Run the flow (storefront action,
POST /flows/{id}/runs, or Studio Run). - The Trace panel lists every captured hit — node, side, run, timestamp — each expandable to the full JSON payload. Filter by node; enable 3 s auto-refresh for a live view. Toggle a tap off or delete it at any time.
Using it — API
| Method | Path | Purpose |
|---|---|---|
GET | /api/v1/flows/{id}/taps | list taps on a flow |
POST | /api/v1/flows/{id}/taps | create { node_id, action, capture, sample?, match_field?, match_value? } |
PATCH | /api/v1/flows/{id}/taps/{tapID} | { enabled } — toggle on the fly |
DELETE | /api/v1/flows/{id}/taps/{tapID} | remove |
GET | /api/v1/flows/{id}/taps/hits?node_id=&tap_id= | the captured trace |
A hit's payload carries { input } (on node.started) or { output } (on
node.done) — the full accumulating packet at that node, so you see everything on
the bus (e.g. a checkout node's hit includes rates, customer, shipping_method
…), not just the node's headline output.
Roadmap — remaining steps
The feature was approved as option A — passive observer taps (not engine
fan-out). Shipped so far: the taps package + Mongo store + module wiring + admin
endpoints (go-flowdsl v0.1.9 / flowexec/module v0.2.11), the admin Debug
page, and the Studio node badge — a tapped node shows a purple ⦿ TAP badge
and the node context menu toggles the tap (Add / Remove debug tap), observe-only.
Also shipped (go-flowdsl v0.1.10 / flowexec/module v0.2.12): the clickable
canvas badge (click ⦿ TAP to remove — no right-click), redaction (sensitive
fields — passwords/tokens/card numbers/… — masked in captured hits by default;
email kept as it's usually what you're debugging; never mutates the live
event), and retention (DELETE /flows/{id}/taps/hits[?node_id=] + a Clear
button; hits also auto-expire after 24 h via a Mongo TTL).
Also shipped: cross-run diff — when a specific node is selected in the trace,
the Debug page groups that node's captured hits by run and shows a flattened,
field-level diff (added / removed / changed) of two runs' input or output packet
at that node — the "why did this run differ here?" view. Built entirely over the
existing /taps/hits data (no backend change).
Also shipped (go-flowdsl v0.1.12 / flowexec/module v0.2.14): sampling +
field filters to bound hit volume on high-traffic flows. A tap carries an
optional sample (record only 1 in every N matching events; the per-tap counter
resets on each cache reload) and match_field / match_value (a dotted-path
== filter on the node's packet — e.g. shipping_method == courier captures
only the courier case). Both apply in TapSink.capture before the hit is
recorded (filter first, then the sample counter); pure tap metadata, no runtime
change. Set them on the admin Add tap bar (a Sample 1-in-N input and an
only when field=value pair); active-tap chips show the 1-in-N and
field=value badges.
Also shipped (go-flowdsl v0.1.13 / flowexec/module v0.2.15 / go-modules
v0.5.3): the notify tap action — two decoupled reusable pieces, both in
the framework:
- Dispatch event. A
notify-action tap publishes a channel-agnosticflow.tap.notifyevent on the bus each time its node fires (flowexecmodule'sWithNotifyHook). The topic + payload (taps.NotifyTopic/taps.NotifyEvent) are the shared contract in the dep-lighttapspackage. flowexec never knows how the notification is delivered — it only says a tapped node fired, so any flow or module can subscribe (in-app, email, SMS, webhook, Slack…). - Notify module. The redelay
notificationsmodule (go-modules) consumesflow.tap.notifyand delivers an in-app notification (persist + SSE fan-out) to the tap's target user — the admin who set the tap (Tap.NotifyUser, captured from claims at create time). Pick Log + Notify me in the Add-tap action.
That completes the debug-taps roadmap. Every reusable piece — the tap models +
sink, the redaction, the dispatch event, and the notify delivery — lives in the
Redelay framework (go-flowdsl + go-modules), not in any downstream app.
See also: Observability (Trace / Live / Lifecycle / Alerts).