Playable Scenarios: Walk Through a Workflow Like You're Publishing an Article
A simulator that only knew states was only half a simulator
The original SymFlowBuilder simulator could fire transitions, track active places, evaluate guards, and replay Symfony events. What it could *not* do was answer the most common question someone asks of a workflow diagram:
> "But what does the data look like at each step?"
A workflow is rarely just a graph. It is a graph plus a *thing* moving through it — an article on its way to publication, an order on its way to fulfillment, a moderation case waiting on a reviewer. Without that thing, the simulator was an abstract token-pusher. You could see that approved was reachable from submitted. You could not see what an *approved article* actually looked like.
This release fixes that. The simulator now carries a subject — the JSON object your workflow operates on — and lets each transition mutate it as it fires. You can also attach a fake HTTP request to any transition and see exactly what would have hit your backend. The result feels less like watching a state machine and more like clicking through an n8n run.
The article-publishing example, end to end
Open the editor, build a state machine with five places — draft, pending_review, approved, rejected, published — and four transitions: submit_for_review, approve, reject, publish. Click Simulate.
The simulator panel now has three tabs: Steps, Scenario, and Inspector. Open Scenario and click the Publishing an article template card.
The starting subject lands on the page:
{
"id": "art_1042",
"title": "My first post",
"body": "Hello, world.",
"author": "alice",
"reviewer": null,
"reviewNotes": null,
"publishedAt": null
}Below it, four transition cards. Each one has patches that mutate the subject and a mock HTTP request that *would* have fired:
submit_for_review
patches: set reviewer = "bob"
request: POST /api/articles/{{id}}/submit
{ "reviewer": "bob" }
→ 202 { "id": "{{id}}", "status": "pending_review" }Switch to Steps. Click submit_for_review. The article is now in pending_review and the reviewer is bob. Click approve. Then publish. The history list shows each step with a colored POST badge — visual proof that a request would have fired.
Click any history row. The panel jumps to Inspector and shows you, side by side, what the article looked like *before* and *after* that transition, with the changed fields highlighted. Below that: the resolved mock request — POST /api/articles/art_1042/publish (the {{id}} was substituted with the real subject value at the moment of the transition) and a 200 response with a real-looking publish URL.
That is the moment the feature clicks. You are not reading a YAML file or staring at boxes connected by arrows. You are watching an article get reviewed and published, with the API calls a real backend would make plotted out next to it.
What you can declare per transition
Each transition can carry an optional effect with three pieces, all of them optional:
1. A description
A one-line note about what the transition represents. Shown in the Inspector at the top of the step.
2. Patches that mutate the subject
A list of { op, path, value } operations applied to a deep-cloned subject when the transition fires. Three ops are supported:
set— write a value at the path. Path syntax isfield.nested.deeperoritems[0].name.push— append a value to an array at the path.remove— delete a key or splice an array element.
Patches are interpreted client-side, in pure JavaScript, on a *cloned* subject. Nothing escapes the simulator. The original subject in the scenario is your "starting state" — re-running the simulator always rewinds to it.
3. A mock HTTP request
A pretend request the transition would have fired, with method, URL, optional body, and an optional canned response with status + body. URLs and JSON values support {{ subject.path }} interpolation, so:
POST /api/articles/{{id}}/publish…becomes POST /api/articles/art_1042/publish when fired. The substitution happens once, at the moment of the transition, using the subject *as it was* before the patches ran. That snapshot is frozen into the step's history — even if you patch id later, the historical request keeps the value it had at the time.
No actual network call is made. The mock request is a UI artifact. That is intentional: scenarios are for *teaching, reviewing, and demoing* a workflow, not for testing your backend.
Three tabs, one mental model
- Steps is the playback surface. Active places, available transitions, history. Click history to jump to Inspector.
- Scenario is the design surface. Subject editor, per-transition patches and mock requests, template gallery. Edits persist with the workflow when you save.
- Inspector is the *what just happened* surface. Before/after subject diff for the selected step, with changed paths highlighted, plus the resolved mock request and response.
The footer carries the playback controls — Step Back, Restart, Auto-play with a configurable interval, and a stop. The header has tooltipped icons for restart and close.
Persistence and sharing
A scenario is stored alongside the workflow in a new simulationConfig JSONB column. It travels with the workflow:
- Auto-save picks up scenario edits and writes them to the cloud (signed-in users) or localStorage (guests), debounced 2 seconds.
- The public share view at
/w/[shareId]loads the scenario when the share owner has saved one. Anyone visiting the link can hit Simulate and walk the article through the workflow without an account. - The iframe embed at
/embed/[shareId]is now interactive. Drop a<iframe>into a docs page, add?play=1, and the scenario auto-starts. Readers click through it the same way they click through an n8n demo on the n8n website.
<iframe
src="https://symflowbuilder.com/embed/abc123?play=1&minimap=0&branding=0"
width="100%"
height="560"
style="border:0;border-radius:14px"
loading="lazy"
title="Article publishing flow"
></iframe>That iframe is a runnable demo of your workflow. Embed it in your README, your docs site, your design review document — wherever explaining the flow has so far required either a screenshot or an awkward "let me share my screen" moment.
What this is *not*
A few deliberate non-goals, since people will ask:
- Not a backend mock. No request leaves the browser. Mock responses are static — no scripting, no JavaScript evaluation. If you need a real mock backend, MSW is what you want; this is a teaching surface.
- Not in the Symfony YAML export. Scenarios are simulator-only and live in their own column. Your exported YAML stays a clean Symfony workflow definition that drops into
config/packages/workflow.yamlwithout surprises. - Not a replacement for tests. It is excellent for *showing* a flow. It is not a substitute for the integration tests that pin your guards in production.
How it works under the hood
The data model is small enough to describe in one paragraph. SimulationConfig is { subject, effects, templateId }. Each effect is { description?, patches?, mockRequest? }. The simulator store carries the subject as live state, deep-clones it on initialization, and on each apply() runs the matching effect's patches against a clone — preserving the *before* snapshot on the step. The mock request is resolved with a tiny {{ path }} interpolator that walks the subject via JSON-pointer-style segments and substitutes string and JSON values. Pure functions, fully sandboxed, no eval.
The whole thing is built on the same `symflow` engine that powers Symfony-compatible runtime. The marking is still a Symfony marking. The events still fire in Symfony's order. The subject is a layer the simulator adds on top — the engine itself is unchanged.
Try it
- Open the editor and either build a workflow or load an existing one.
- Click Simulate.
- Switch to the Scenario tab and click Publishing an article.
- Switch back to Steps and walk
submit_for_review→approve→publish. - Click any history row.
If you have already shared a workflow publicly, open its /w/[shareId] page — your viewers can now play through whatever scenario you saved.
What is next
The natural follow-ups are user-supplied scenarios via share links (?scenario=template-id to override the saved one), per-place initial-state branches (so you can simulate "what if this article *started* in pending_review?"), and a record-and-replay button that captures a real run from your app and lets you scrub through it. Open issues if any of those would help.
Until then: design the workflow, save the scenario, share the link, watch your reviewer click through it without ever opening a YAML file.