April 30, 2026featuresimulatorscenariosmock-http

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:

json
{
    "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:

text
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 is field.nested.deeper or items[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:

text
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.
html
<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.yaml without 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

  1. Open the editor and either build a workflow or load an existing one.
  2. Click Simulate.
  3. Switch to the Scenario tab and click Publishing an article.
  4. Switch back to Steps and walk submit_for_reviewapprovepublish.
  5. 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.