Skip to main content

Flow animations

An Act app already knows its own flows. Every action, the state that checks it, the events it commits, the projections that fold them and the reactions that pick them up are declared in the builders. A flow animation draws all of that as one map and plays it: a token leaves a person or an automation, passes through the state where the preconditions are checked, lands as an event, feeds the projections, wakes a reaction, and goes on to the next action or out to another system.

See one: WolfDesk ticket flows ยท Calculator flows ยท Risk game flows, from the Risk board game built on Act ยท all animations

What you getโ€‹

  • The whole model on one screen. Swimlanes for actors, actions, state, events, projections, reactions and the outside world. Time runs left to right. Select any box for its source file and neighbors: an action shows its preconditions and payload, an event its payload, and a state its schema as its invariants (checked before every commit), with each refinement listed by its message.
  • The boundaries the builders declare. Each slice() is a dashed box over the actions and events of the partial states it declares, plus the reactions it declares. An app without slices gets one box per state instead. Partial states of the same name still merge into one state bar. While a scenario plays, the boxes on the current step light up, and a token rings where it crosses from one box into another, for example a timer reaction calling an action owned by another slice.
  • The checks, one by one. When a token reaches the state, the action's preconditions (โ–ฒ) appear in a list beside it, in the order the state runs them: first the rules on the state (.given), then those that also read the payload (.requires). The action's box shows how many there are, as โ–ฒ. Each gets a โœ“ as it passes. A refused action stops on the rule that failed with โœ•, and the rules after it are never checked. A refusal thrown from .emit(...) shows as the last row, after every declared rule passed.
  • Scenarios that cover every flow. Each tab plays one story. Together they walk every connection on the map, including the ones people forget to show: refused actions, retries with backoff, a blocked target, and timers that wake and do nothing.
  • The state and the stream, live. The right rail shows the event stream as it grows (global id, stream version, and the person or automation that caused each event) and the state as each event patches it.
  • Video. One script exports a 1080p MP4 per scenario for talks, READMEs and release notes.
  • One static file. No build, no server. It runs on GitHub Pages, any static host, or inside an iframe.

Make one with Claude Codeโ€‹

The repo ships an animate-act-flows skill. Ask for it in plain words:

animate the flows of packages/wolfdesk

The skill does four things:

  1. Extracts a draft. It runs the same parser as the act CLI over your builders and gets states, actions, preconditions, events, reactions and projections.
  2. Reads the source. The parser is best-effort, so the skill fills in what it can't see: actions invoked from helper functions, webhook() reactions, .defer timers, calls to outside services, and who the actors are (people, or automations such as the actor a reaction runs as).
  3. Writes the scenarios. Short, believable stories, each showing one mechanism, until every connection is covered.
  4. Saves the spec. The result is one JSON file, docs/flows/<app>.flow.json.

Preview itโ€‹

pnpm -F docs flows # validate every spec and render docs/static/flows/
open docs/static/flows/wolfdesk.html

The build refuses a spec whose scenario walks a connection the map doesn't draw, and warns about connections no scenario walks.

Controls: space plays or pauses, the arrow keys step, and the tabs switch scenarios. The legend and the "how to read" notes float and close. Panels on the right collapse.

Publish itโ€‹

Commit the spec and merge it. docs/flows/*.flow.json is part of the docs build (pnpm -F docs build:all), so the next docs deploy publishes the page at /flows/<app>.html and adds it to the gallery. To publish without waiting for another docs change, run the Deploy Docusaurus Docs workflow by hand from the Actions tab.

To host it somewhere else, render a standalone file:

node docs/scripts/flows.mjs render docs/flows/wolfdesk.flow.json wolfdesk.html --standalone

Export a videoโ€‹

pnpm -F docs flows:video wolfdesk --scenario 2 --out escalation.mp4
OptionDefaultMeaning
--scenario nall, back to backWhich tab to record (1-based)
--size WxH1920x1080Video size
--theme dark|lightdarkColor theme
--speed x1Playback speed
--out file<app>[-n].mp4Output path. A .gif path writes an animated GIF for READMEs, which can't embed video
--fps n12GIF frame rate
--gif-width pxhalf the video widthGIF width

The repo README's animation is a GIF made this way, in each theme:

pnpm -F docs flows:video wolfdesk --scenario 1 --size 1440x810 --speed 2 --gif-width 960 --theme dark --out ../assets/flow-dark.gif

It drives Chrome through playwright-core, captures every frame the page paints and encodes H.264 with ffmpeg. You need Chrome and ffmpeg installed.

The hosted page also has a Record button. In desktop Chrome it captures the tab while the current scenario plays and saves the file. Phones don't show it.

On a phoneโ€‹

The map never shrinks below a readable size. A map wider than the screen scrolls sideways, and playback scrolls to each step as it plays. The scenario tabs scroll in one row, the caption stays at the bottom, and the event stream and state panels follow the map. The landing page's embedded overview is the exception: on a wide screen it fits the whole map.

The page reads a few words from the URL fragment, joined with -:

FragmentEffect
#s3Open on the third scenario
#dark, #lightForce a theme
#embedHide the title and extra buttons, loop all scenarios. For iframes
#recordHide the controls and play once. Used by the video export
<iframe src="https://rotorsoft.github.io/act-root/flows/wolfdesk.html#embed" width="100%" height="640" loading="lazy"></iframe>

The specโ€‹

The player is generic. All app knowledge lives in the spec: lanes, nodes with a column, edges, and scenarios made of steps. A step either walks an edge (from, to) or stands at a node (at). Steps can carry the event data, the state patch, a wait for timers, a reject reason, retries with backoff, and blocked. An app with several states gets one bar per state, each with a span over the actions that load it; the state panel shows the scenario's own stream. An app with slices lists them under slices (owns, registers, file) and sets slice on the actions, events and reactions each one owns. A slice owns the partial states declared in its own file and only references the others it registers. Each slice gets its own run of columns, and the build refuses a spec where they interleave. The skill's SKILL.md documents every field, and docs/flows/wolfdesk.flow.json uses all of them.