API ReferenceFlowBuilder

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
ParameterTypeDescription
idstringMachine-readable identifier, unique within the script
titlestringHuman-readable name shown in the AnimationPlayer dropdown
descriptionstringOptional subtitle shown below the title
options.layerstringWhen 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): FlowBuilder

from 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 → db

to

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): FlowBuilder

to() resolves the edge between the current source and nodeId in both directions:

  1. Looks for a forward edge: source → nodeId
  2. If not found, looks for a reverse edge: nodeId → source and 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): FlowBuilder

Messages 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): FlowBuilder

Use 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): FlowBuilder

Use 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][]): FlowBuilder

Each 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 this

Correct 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→proxy

Complete example

flow-builder-example.ts
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;