Workflow playback
Show how a run went on a workflow chart, and replay it with values travelling the lines, a scrubber and the failure in focus.
6 min read
WorkflowCanvas shows a run two ways. An overlay says where each node ended, with no time. A playback replays the run: your host owns the moment t, the canvas draws the run at that moment, and dots carry values along the lines. Both bring a failed run's way to its first failure forward.
import { WorkflowCanvas } from '@tale/ui/flow/workflow-canvas';
import {
buildPlaybackTimeline,
flowStateAt,
usePlaybackClock,
type FlowRunOverlay,
} from '@tale/ui/flow/playback';
import { FlowPlaybackBar } from '@tale/ui/flow/playback-bar';Show where a run ended
Pass a FlowRunOverlay as overlay: each node's state, and optionally a detail such as "1.4 s", a reason such as the error line, a condition's decision, the items of a node that runs once per item, and the pass of one that repeats. Set finished when the run is over, so a node it never reached reads as not run.
Leave edges out to let the canvas work out the lines. A line counts as travelled when its source succeeded and its target started, and as not taken when its condition decided the other way or its target was skipped.
| State | The box |
|---|---|
| Running | Blue frame and a top bar that sweeps across, still under reduced motion |
| Waiting | Amber frame and top bar |
| Succeeded | Plain |
| Failed | Red frame and left edge; the strip shows the error line |
| Skipped, stopped, not run | Dashed border |
Each box shows its state's glyph and says its state in its name, such as "Classify (Succeeded)". Its strip says the reason or the state, then the detail: "Succeeded · 1.4 s". A condition shows a Yes or No chip; the branch it took gets a check, the other a dash and a thin line. A frame counts "12 of 50 items" or "Pass 3 of 5". No text is ever faded.
When the run failed, the canvas brings forward the way the run took to the first failure and steps back from the rest. The line into the failed node turns red. Without an error line, the failed node's strip says "The run stopped here". Set focusFailure={false} to show the run without the focus. A highlight you pass wins over it.
Show simulated and held-back nodes
A test or a preview does not always do a node's real work. Give the node's run info a substitute to say what stood in for it: a kind, and your words as label. The canvas shows a chip with the kind's glyph and your words at the start of the node's strip, at the strip's height, so no box changes size. The words are also said in the node's description and in its List view lines.
kind | When | The box |
|---|---|---|
simulated | A test gave the node's output. | A flask on the chip |
pinned | The node's output was kept from an earlier run. | A pin on the chip |
failure | A test made the node fail. | A lightning bolt on the chip; the failed frame stays |
held | A preview held back a write. | A pause sign on the chip |
left-out | A test leaves the node out. | A dashed circle on the chip and a dashed border, as a node that does not run |
Choose A preview in the example to see a held-back write. Write the label in the reader's language, such as "Simulated output" or "Not sent — preview"; the package adds no words of its own to the chip.
A replayed run carries the chip too: give the node's own span (one with neither item nor pass) the same substitute, and the node shows it from the moment its span starts.
Replay a run
Press Play, drag the scrubber, or step between events. The answer waits three hours for an approval; the replay shows that wait as a hatched band and moves on.
- Record the run in real time: each node's spans with
startedAtandendedAt, each moment a value left a node (travels, with thetargetit goes to), and each wait. A span'sdetail, such as its duration, shows once the replay reaches the span's end; a span still open on a live run shows itsdetailat once. - Build the timeline once with
buildPlaybackTimeline(run). - Drive it with
usePlaybackClock({ timeline })and passplayback={{ timeline, t }}to the canvas. - Put
FlowPlaybackBarunder the canvas, fed by the same clock.
buildPlaybackTimeline compresses real time piece by piece. The stretch between two moments plays for its real length, but at least minStepMs (240 ms) and at most maxGapMs (1.2 s), and each value gets travelMs (320 ms) to reach its target before the target starts. A long wait becomes a wait mark with your words and its end, drawn as a band on the scrubber; a wait still open has no end yet and its band runs to the end of the timeline. A failure becomes a failure mark. toReal(t) and fromReal(ms) map between playback and real time, so the bar can show how long the run really took.
flowStateAt(graph, timeline, t) is the frame the canvas draws. Use it to say what is happening for the scrubber's spoken value (activity), or to drive the List view.
Watch the values travel
While a value is on its way, an 8 px dot rides its line. Its motion is a Web Animation created paused: every change of t sets its currentTime, so playing, scrubbing and stepping are the same operation. It fades in over the first 8 % of the line and dissolves into the box it reaches. Strips settle softly when their words change while the run plays forward; scrubbing back swaps them at once.
While the run plays, the canvas follows the node it reaches into view, until the reader moves the view.
Under reduced motion there are no dots: a line switches to travelled when its value arrives. Play steps from event to event every 600 ms instead of sweeping.
Control the replay
usePlaybackClock opens on the end of the run, which shows the whole story. Playing from the end starts over. On a live timeline it follows the growing end until the reader moves t, and follow() returns to it.
FlowPlaybackBar holds play or pause, previous and next event, a scrubber with a tick for each event, the time and the speed. The speed is a segmented control, folded into a menu on a phone. A live run shows Live, and Follow live while the reader is behind its end. The play button's name says what it does next.
| Key | Does |
|---|---|
| Space | Plays or pauses, anywhere on the bar except a button |
| ← / → | Moves the scrubber by 1 % |
| Page Down / Page Up | Moves it by 10 % |
| Home / End | Jumps to the start or the end |
[ / ] | Steps to the previous or next event |
The scrubber is named "Run timeline" and says where it is, such as "00:42 of 03:10 — Classify running", from formatTime and your activity words.
Props
FlowPlaybackBar:
| Prop | Type | Notes |
|---|---|---|
timeline | FlowPlaybackTimeline | From buildPlaybackTimeline. |
t / onTChange | number | The moment shown, in playback ms. |
playing / onPlayingChange | boolean | |
speed / onSpeedChange | 0.5 | 1 | 2 | 4 | The speed control shows only with onSpeedChange. |
formatTime | (t) => string | Defaults to playback time as mm:ss; pass formatFlowClock(toReal(t) - startedAt) for real time. |
activity | string | What happens at t, for the scrubber's spoken value. |
onFollowLive | () => void | Shows Follow live on a live run. |
On WorkflowCanvas, see Workflow canvas for overlay, playback and focusFailure.