FlowBuilder
FlowBuilder is the animation layer of cloud-arch. After the topology is committed with topology.apply(), you create a FlowBuilder, define scenarios, and return flow from the script. The AnimationPlayer then drives playback.
const flow = new FlowBuilder();scenario
Declares a named scenario. All subsequent .from() / .to() / .showMessage() calls belong to this scenario until the next .scenario() call.
flow.scenario(
id: string,
title: string,
description?: string,
options?: ScenarioOptions
): FlowBuilder| Parameter | Type | Description |
|---|---|---|
id | string | Machine-readable identifier, unique within the script |
title | string | Human-readable name shown in the AnimationPlayer dropdown |
description | string | Optional subtitle shown below the title |
options.layer | string | When set, this scenario is only available when the named layer is active |
flow.scenario('happy-path', 'Happy Path', 'All services healthy');
flow.scenario('failover', 'Database Failover', 'Primary crashes, replica promotes', { layer: 'v2' });If you call .from() before any .scenario(), the steps are appended to an implicit default scenario.
from
Sets the current source node for subsequent .to() calls.
flow.from(nodeId: string): FlowBuilderfrom does not send a packet by itself — it only sets the cursor. The packet is sent when .to() is called.
flow
.from('client') // cursor = client
.to('api') // sends packet: client → api
.from('api') // cursor = api
.to('db'); // sends packet: api → dbto
Sends an animated packet from the current source node to nodeId, then sets the cursor to nodeId for the next step.
flow.to(nodeId: string): FlowBuilderto() resolves the edge between the current source and nodeId in both directions:
- Looks for a forward edge:
source → nodeId - If not found, looks for a reverse edge:
nodeId → sourceand animates it in reverse
This is how responses work without any additional edge definitions.
// Edge: connect('api', 'db')
flow
.from('api').to('db') // forward animation on edge api→db (query)
.from('db').to('api'); // REVERSE animation on same edge api→db (result)Bidirectional flow uses the same edge. Never add connect('db', 'api') to make responses work — to() already handles reverse traversal automatically. Adding a duplicate reverse edge creates two separate lines on the canvas and breaks the animation.
showMessage
Displays a blue informational message banner at the bottom of the canvas during the current step.
flow.showMessage(text: string): FlowBuilderMessages appear immediately when the step they are attached to plays. They disappear when the next step begins.
flow
.from('client').to('api')
.showMessage('[GET] /api/users/42 — Authorization: Bearer ...')
.to('db')
.showMessage('[QUERY] SELECT * FROM users WHERE id = 42');Use [BRACKET] prefixes for message labels instead of emojis. This keeps the interface professional. Common prefixes: [REQUEST], [RESPONSE], [QUERY], [RESULT], [AUTH], [ERROR], [TIMEOUT], [HIT], [MISS], [OK], [RETRY].
showError
Displays a red error message banner at the bottom of the canvas and adds a red glow to the current node.
flow.showError(text: string): FlowBuilderUse showError for failures, timeouts, circuit open states, and any condition that represents a problem.
flow
.from('api').to('db')
.showError('[TIMEOUT] No response from database after 30s')
.from('api').to('client')
.showError('[503] Service temporarily unavailable');flashError
Applies a red glow to an arbitrary node without blocking the animation flow. The glow fades after approximately 2.5 seconds.
flow.flashError(nodeId: string): FlowBuilderUse flashError when you want to highlight a node as unhealthy while simultaneously sending a packet somewhere else — for example, showing a circuit breaker counter incrementing while a fallback response is already in flight.
flow
.from('cb')
.flashError('cb-counter') // highlight counter (non-blocking)
.showMessage('[COUNT] Failure count: 2/3')
.to('client')
.showError('[503] Upstream unavailable');parallel
Sends multiple packets simultaneously, one per sub-path.
flow.parallel(paths: [string, string][]): FlowBuilderEach entry in paths is a [source, target] pair. All packets are sent at the same time. The step completes when all packets have arrived.
flow
.from('leader')
.parallel([
['leader', 'follower-1'],
['leader', 'follower-2'],
['leader', 'follower-3'],
])
.showMessage('[REPLICATE] Write replicated to all followers');Critical rule: responses use reverse animation
This is the most common source of bugs in cloud-arch scripts.
Wrong approach — adding reverse edges:
// WRONG: creates two separate edge lines on the canvas
topology.connect('client', 'api', { protocol: 'http' });
topology.connect('api', 'client', { protocol: 'http' }); // <-- do not do thisCorrect approach — .from().to() in reverse order:
// CORRECT: one edge, animation goes both ways
topology.connect('client', 'api', { protocol: 'http' });
// Request (forward)
flow.from('client').to('api').showMessage('[GET] /data');
// Response (reverse on the same edge)
flow.from('api').to('client').showMessage('[200 OK] Here is your data');The same rule applies to all intermediate hops. A full request/response cycle through client → proxy → backend:
// Topology: two edges, no duplicates
topology.connect('client', 'proxy', { protocol: 'http' });
topology.connect('proxy', 'backend', { protocol: 'http' });
// Animation: four hops, two edges, both directions
flow
.from('client').to('proxy') // forward on edge client→proxy
.from('proxy').to('backend') // forward on edge proxy→backend
.from('backend').to('proxy') // reverse on edge proxy→backend
.from('proxy').to('client'); // reverse on edge client→proxyComplete example
const topology = new TopologyBuilder(true);
const g = topology.createGroup('app', { label: 'Application' });
g
.addProcess({ id: 'client', label: 'Client', state: 'running' })
.addProcess({ id: 'api', label: 'API Server', state: 'running' })
.addProcess({ id: 'db', label: 'PostgreSQL', shape: 'cylinder', state: 'running' })
.autoResize();
topology.connect('client', 'api', { protocol: 'http', label: 'REST' });
topology.connect('api', 'db', { protocol: 'postgresql', label: 'SQL' });
await topology.apply();
const flow = new FlowBuilder();
// --- Scenario 1: Happy path ---
flow.scenario('success', 'Happy Path', 'Query returns data');
flow
.from('client').to('api')
.showMessage('[GET] /data')
.to('db')
.showMessage('[QUERY] SELECT ...')
.from('db').to('api')
.showMessage('[ROWS] 10 records')
.from('api').to('client')
.showMessage('[200 OK] Data delivered');
// --- Scenario 2: Database timeout ---
flow.scenario('timeout', 'DB Timeout', 'Postgres does not respond');
flow
.from('client').to('api')
.showMessage('[GET] /data')
.to('db')
.showError('[TIMEOUT] No response after 30s')
.from('api').to('client')
.showError('[503] Database unreachable');
return flow;