Skip to content

Latest commit

 

History

History
69 lines (50 loc) · 24.2 KB

workflow.md

File metadata and controls

69 lines (50 loc) · 24.2 KB
subsystem workflow
owned_paths
plugins/workflow/

workflow (@flybot/workflow)

A group-shaped chat of local Bots, with a flowchart the owner can drag once Edit is on (#1318, #1378). The plugin stores the graphs under $DSH_HOME/bot/flybot-workflow and answers flybot/workflow/*. It lives entirely under plugins/workflow/.

This is not the workflow-definition engine. That engine (#1272) stores workflows/<id>.json and guards artifacts. This plugin does not read or write those files and does not call that engine.

Settings → Workflows (#1364) is this plugin's list. The page lives in the core client and calls workflow/state, workflow/create and workflow/graph. Factory rows are templateCatalog() — id, name, description, triggers, plus the drawable nodes, edges, layers and chain — and the owner's rows are the saved workflows. The settings page does not replace the sidebar chat. A sidebar click opens this plugin's stage and leaves the Workflows section in place.

A host call on that page that throws, rejects, or never answers ends on the page's own .wf-err alert instead of staying on Reading workflows… or looking saved: a failed read offers Try again, and a failed save or model change clears the Changes are saved automatically hint. A successful call clears the alert again.

Owns

Everything in plugins/workflow/: the graph rules, the run store, the matcher, the browser chat and the flowchart canvas.

Does not own

  • flybot.api.handle, flybot.tools.register and flybot.tasks.onWrite → plugin-runtime.
  • The board's own state machine (task-create, task-update, the task drive) → core-host. The plugin hears a saved card and may name a local Bot; the host applies that owner.
  • Install commands → install. @flybot/workflow is a default install. @flybot/timeline stays opt-in.

Key files

File Role
plugins/workflow/index.js Plugin entry (flybot-workflow). Endpoints, run_workflow, define_workflow, the board listener, and the reply middleware that ingests a step result and denies delivery to persistent Bot chats.
plugins/workflow/src/graph.js Nodes, edges, positions, and named inputs and outputs. A node starts when its forward predecessors are settled and a taken edge reaches it. Same-rank nodes run together. A forward cycle is refused. A loop edge is the only cycle for success paths. A when edge chooses a branch; the other is skipped. A required input with no upstream provider is refused before a run starts. An onFail edge provides failure recovery: when a node fails with recoveries remaining, the workflow resets the body from the upstream target through the failing node and re-runs it in order (tracked by failAttempts per edge, with max counting recoveries, 1–8, default 3). After the recoveries are used, the node fails permanently.
plugins/workflow/src/service.js The store, lanes (active runs, FIFO queue, terminal history), maxConcurrent, rename, delete, saveGraph, define, runNamed, ingest, stepResult. rename updates a workflow's name and optional description; delete removes the workflow, releases ephemeral Bots, and cleans logs/runs/routes. Factory template ids (e.g. delivery) are normal saved rows and may be deleted. A spawn hook makes one run Bot per node. Disposing the plugin ends every live run still marked running (nodes in flight are failed, the run is archived) and then releases every run Bot and drops those ids from the saved record only once the release has finished, handing the release back as a promise the host awaits, so a shutdown cannot persist a run as running with no ephemeral map and cannot leave hidden Bots that no state names, for every live run in a lane rather than only the one archived last (#1387, #1414, #1472). A released run Bot also gives its runner name back to the allocator instead of keeping it for the lifetime of the process. A dispatch that does not land is not marked handed: it is retried twice with a short backoff, and a step still unanswered after its workflow's stepTimeoutMinutes (45 minutes by default) fails the run and releases its Bots (#1530, #1580).
plugins/workflow/src/ephemeral.js Builds each node's run Bot from its prompt, role, model and tools, and releases the ids the host returns. node.tools names the connectors that node may call: spawn passes the cleaned list to fb.bot.create, the host records it as stepTools, and connector-access.js allows only the connectors it names (github covers mcp__github__*, composio covers mcp__composio__*) — a node that names none reaches none. File and shell rights do not come from that list: every node Bot gets the danger-full-access preset a normal sidebar Bot has, and EPHEMERAL_DENIED_TOOLS still refuses the team tools at the guard and approval layers. The list is also stored in fields.tools for the UI.
plugins/workflow/src/match.js Which open card matches a Workflow or a local Bot.
plugins/workflow/src/template.js Factory delivery/weekly graphs plus EARLY_END_GUIDANCE for Claim/Verify/Triage early-end markers; 发布交付流水线 (a parallel branch) and the weekly report, each node with the inputs and outputs that edge carries.
plugins/workflow/src/narrate.js Progress lines for a step. A live run quotes the Bot reply when present; otherwise delivery-template nodes may use canned copy. A join line is only for a rank of more than one node.
plugins/workflow/client.js Sidebar section (with mini-flowcharts), workflow stage (view/control: one card per live run with its own title, node line, segment progress, status chip, and Pause / Resume / Stop; click a card or its Flow button to focus that run in the drawer flowchart; optional aggregate chip when several run or the queue is non-empty — no message composer), assign dialog (scrollable with sticky footer; it selects the first card once the board loads, keeps Assign disabled until a card is selected, and shows a failed Tasks load as an error with a Retry instead of the empty-board hint, #1457, #1458), flowchart drawer (collapsed by default, per-node inspector, fitted to the 400px panel so the graph needs no inner horizontal scroll), and Settings → Workflow graphs (canvas-first with inspector panel). The product UI starts only live runs (assign, ingest). The stage shows the queue and refreshes state while runs are active; a running node uses a breathing border animation. Visual flowcharts replace text chains across all surfaces. Sidebar rows (including factory ids such as delivery) support Delete with confirm. The stage and dialogs portal onto document.body; a missing container stays in the section tree. A stage control that fails — a graph save, Reset, Pause, Resume or Stop that throws, rejects, answers {ok:false} or does not answer within 10 s — ends on the section's .wf-err alert instead of being swallowed, and a success clears the alert. The flowchart's node role sub-label takes its colour from --bt-ink-2 rather than a hardcoded grey, so it stays readable on every theme's node card (#1495).

Entry points

  • Endpoint workflow/state and workflow/templates are reads.
  • workflow/create, workflow/rename, workflow/delete, workflow/assign, workflow/concurrency, workflow/graph, workflow/advance, workflow/reset, workflow/ingest write. workflow/concurrency sets that workflow's maxConcurrent (an integer from 1 to 8); the same control is in Settings → Workflow graphs. workflow/step-timeout sets that workflow's stepTimeoutMinutes (an integer from 1 to 120, or null to go back to the default), and workflow/create accepts the same field. workflow/rename updates a workflow's name and optional description; read-only hosts are refused. workflow/delete removes a workflow from state, releases its ephemeral Bots if a run exists, and cleans logs/runs/routes.
  • Tool run_workflow starts a named run with a brief. Tool define_workflow writes the same graph the canvas edits.
  • flybot.tasks.onWrite calls ingest. A new Bot match returns { ownerBotId }. A Workflow match starts the run inside the plugin and leaves the card's owner to the host. A host that offers no tasks.onWrite warns once and reports tasksWired: false in workflow/state, and the Workflows panel says that a board card cannot start a run there (#1462).

Invariants

  1. Friends' Bots are not nodes. A remote Bot is refused by create, define and the matcher.
  2. The canvas and the run share one graph: nodes, edges and positions. A drag commits workflow/graph, but only while Edit is on — a factory preset is copied first (#1378). Status highlighting reads that same graph. Replacing edges does not recompute who is already running. The Settings canvas paints that graph on the same 960px design plane, scaled to the width the settings page gives it, so the canvas is not a second scroll container inside the page's own scroll; its pointer deltas are divided by the scale, and its wires keep a stronger stroke than a plain divider so an edge can be followed at that size (#1483).
  3. A live run advances only when its ephemeral Bot replies, following the graph's edges in order. That reply advances the run and may appear on the workflow stage log; it is not delivered into a persistent sidebar Bot conversation (for example Chief). The workflow-step send middleware denies those replies after stepResult runs. Nodes start only when their forward predecessors finish and edges reach them. Early termination (#1537, #1538, #1546, #1570, #1571): When a node completes or fails, the run ends immediately with status done, sets run.earlyEnd to the condition name, and does not advance to successors, take a success-path loop, or burn onFail recovery attempts. The two markers that report the task itself end it only on a structured signal (#1546): needs-human and already-claimed must be a declared OUTPUT whose value equals the marker, or a whole reply line that is exactly EARLY-END <reason> in any case. A step that quotes an issue title, negates the label or reports it absent keeps running. tip-unchanged (#1570) and open-pr (#1571) accept that same structured signal and also honor their prose fallbacks (tip unchanged / unchanged tip, open PR already / already has an open PR / has an open PR / found an open PR), so a Bot that forgets the structured marker cannot soft-lock Claim↔Verify on the same tip. See checkEarlyEnd in Key files. Factory prompt guidance lives in EARLY_END_GUIDANCE (plugins/workflow/src/template.js) for Settings graphs that wire Triage / Claim / Verify: Verify must escalate once on an unchanged tip, not re-run the full suite. There is no scripted or seeded run path: workflow/state exposes only live === true runs — the running ones, plus the last run when it is still live and ended failed — a failed sibling stays on the stage beside whatever is running now, with run.failure ({ code, at, nodeName?, text? }) naming what ended it, so the room does not have to open Runs to learn that a run failed (#1493, #1494). The failed sibling is left out when a running run carries the same task id: that is a retry, and the live run keeps the row (#1507). History entries carry the run id and the same failure, so a Runs row shows when the run ended and why, and a row for a run already on the stage is not listed twice. A run does not create a schedule. scheduleLabel is display text. When the host can create Bots, starting a live run (assign, run_workflow, ingest) spawns one ephemeral Bot per node from that node's prompt, role, model and tools, sends it the step from the Main Bot, and removes every instance when the run finishes, fails for good, or is reset. A failOnce retry keeps them. They are hidden and left out of the sidebar. Loading the plugin does not spawn Bots for the default workflows. Plugin unload (dispose) and the load-time reap both end a live run still marked running: in-flight nodes are failed, the run is archived as failed, and any ephemeral Bot ids are released. A run already written as running with no ephemeral map (an older unload) is healed the same way on the next load (#1414). Loading is also the one moment a run the host left behind is noticed when its ephemeral map is still present: reap releases those instances and records the run as failed. Nothing resumes it. A run that already ended keeps its own status (#1388). workflow/reset stopping one live run ends it the same way before its Bots are released: nodes still running become failed, so the strip cannot keep painting a run that is over (#1424). A graph edit that drops a node that is still running ends that run the way Stop does: the node is failed, the run is archived failed, its node Bots are released, and the queue drains (#1549). Every wait is bounded (#1530). A dispatch that does not land is not marked handed: the chat names the code and the attempt, and it is retried after a short backoff at most twice, never twice inside one hand-off, so a 429 is not hammered. When it still does not land, the step and the run fail with the reason written into the chat. Each running live step also holds a deadline — its workflow's stepTimeoutMinutes, DEFAULT_STEP_TIMEOUT_MINUTES (45 minutes) unless the record sets one, an integer from 1 to 120 given at create or through workflow/step-timeout (armed even when the host cannot spawn ephemeral Bots; #1530, #1580); when it expires the node and the run fail as failed, the run's ephemeral Bots are released, the chat says the step timed out, and warn names it once. The deadline is cleared as soon as the step moves, and by Pause, Stop, Reset, delete, reap and dispose; a paused run holds none, and Resume arms it again from that moment. A timer never outlives the run that started it. A reply that matches no running step — its run ended, or the node already moved — is reported through warn once per Bot and run instead of being dropped silently. A reply that says could not answer your message or model call failed fails that node and does not start its successor. A 429 or rate-limit mention in any other reply does not. Stop and Reset (workflow/reset) follow the same order as dispose: they release the run's Bots before the record forgets their ids, and an id whose release failed stays named on the still-live run, so the next Stop, Reset, dispose or load-time reap retries it (#1476). A run left that way is marked paused and releasePending and shows as Paused: it hands off no further node, resume is refused with an error, and Stop again is the retry. A whole-workflow Reset that could not release every Bot freezes every run it did not reset, because each of them has lost the ids whose release did go. The markers go away when the run ends: Stop again, dispose, reap, or its own completion.
  4. Each workflow keeps a FIFO queue and up to maxConcurrent live runs (default 2, an integer from 1 to 8 on the record, via workflow/concurrency, or in Settings → Workflow graphs). A task that does not fit is enqueued. When a run finishes, every workflow with a free slot is drained, oldest enqueue first. A new assign on a workflow that already has a queue waits behind it. A task does not replace a running run or release that run's Bots. Migrating an old store keeps only runs that were still running with ephemeral Bot ids; other legacy scripted rows are dropped from active state (finished tasks may remain in lane history). History records a finished or failed live run. Lowering the cap does not stop a run that has already started. Assigning { botId } claims the card for that local Bot and does not enqueue. A remote Bot is refused. activeCount is runningOf(lane) — live runs with live === true and status === 'running'. The stage aggregate chip and the Runs tab (运行记录) must agree: every such run appears as its own running row and progress card with its own status (never a hardcoded Running), the chip count does not exceed that list, and a failed sibling appears with its time and reason.
  5. A node declares up to eight inputs and eight outputs. A required input is an output of the same name on an ancestor, or task, title, or brief on a root. A start with no provider is refused, and Bots spawned for that attempt are released. On a Bot-backed run a successor stays queued until its required inputs are present. If every forward predecessor is done, failed or skipped and an input is still missing, that node fails and the run stops. The dispatch text carries Input <name>: <value>. The Bot reports OUTPUT <name>: <value>; a single declared output may be the reply body. Nodes start in edge order.
  6. Each node is a real ephemeral Bot for that run, then it is removed. A permanent sidebar Bot (botId) is optional and is not required for the step to run. File and shell rights are the same as a normal sidebar Bot's: every node Bot gets the danger-full-access preset, and EPHEMERAL_DENIED_TOOLS still refuses the team-admin tools at the guard and approval layers (#1466, 29 names). node.tools selects connectors only: spawn passes the node's cleaned list to fb.bot.create, the host records it as stepTools, and connector-access.js allows mcp__github__* for github and mcp__composio__* for composio — a connector the node did not name is refused, and a node that names none reaches none. The list is also stored in fields.tools for the UI. A hidden run Bot cannot show an approval card, so the approval seam answers the same way the guard does. The host stores this on the run Bot as stepTools and permission.
  7. A loop edge (loop: true, max from 1 to 8, default 3, optional until: { output, eq }) is the only cycle for success paths. The body runs again until until matches or max runs have finished, and the forward edges wait until then. Restarting the body drops the branch choice made inside it and queues a sibling that the previous pass skipped, so the next pass can take the other side. A when: { output, eq } edge is taken only when that output equals eq. when: { else: true } is taken only when no other when matched. A node whose forward edges are all closed is skipped and does not block a join. A run cannot spin: every loop has a max. An onFail edge (onFail: true, max recoveries from 1 to 8, default 3) provides failure recovery: when a node fails, if an onFail edge points from that node back to an upstream target and recoveries remain (tracked in failAttempts per edge), the workflow resets every node from the recovery target through the failing node (the target to running, the rest to queued) and re-runs them in order. After max recoveries are used, the node fails permanently and the run stops. An onFail edge does not interfere with failOnce retry (which restarts the same node once) or success-path loop edges. Settings → Workflow graphs can add a link, a condition, or a loop; a failure recovery edge is added through define_workflow (onFail: true) and has no Settings control yet.
  8. Auto-assign considers an open card only. Every saved board write reaches the hook, including a card a Bot records for itself through track_tasks, so a workflow:<id> on the card a Bot wrote starts that run (#1462). A saved Bot claim beats a workflow trigger. Otherwise first hit wins: a workflow: token, then workflowId / workflow, then an assignee that names a workflow, then a bot: token, then an assignee that names a local Bot, then the longest trigger in the title, note, criteria or tags — an ASCII trigger must match a whole word (notice does not match noticeable, pr does not match Improve), while a non-ASCII trigger matches inside a longer text because CJK has no word boundary. A miss returns no route. A second ingest of the same task and target is a no-op, and a live run for that task id is not restarted.
  9. The browser draws text only. Names and messages are React text children. The unit renderer asserts a hostile name is not markup.
  10. enabled: false registers no endpoint, tool or listener and writes nothing.
  11. A sidebar click opens the stage or a dialog and leaves the Workflows section in place. The stage and the dialogs portal onto document.body. When that container is missing they stay in the section tree; the click does not throw.
  12. A node's prompt and the graph are one record. Settings → Workflow graphs (canvas-first with per-node inspector), the flowchart drawer (collapsed by default, inspector on select), and define_workflow all write workflow/graph. The drawer paints that record fitted to its fixed 400px panel: fitPlane scales node centres (never the node boxes) into the panel width, wires and parallel frames use the fitted positions, and a pointer delta is divided by the fit scale before it is stored, so an edit still commits the same world coordinates. The Settings canvas paints the same record scaled to its own width (invariant 2), not as a second scroll container. A run reads that record; it does not define it. After a live run finishes, the stage shows the last finished live run (lane.last with live === true) until a new live run starts.
  13. The stage chat follows a new line only while the reader is at the bottom. A line the stage filters out (a workflow node's own reply) changes no pixel, so it moves the chat nowhere; a reader who scrolled up keeps their place until they return to the bottom.

Tests

plugins/workflow/test/: graph advance and join, graph edit, assign and persistence, the FIFO queue and concurrency cap, Bot-reply advance, a dispatch that fails without being marked handed, a bounded hand-off retry and a step deadline that fails the run and clears its timer (#1530), a per-workflow stepTimeoutMinutes that replaces the ten-minute constant and is refused outside 1..120 (#1580), disposing releases the Bots before the record drops their ids and a released runner name is reusable again, assign-to-bot versus assign-to-workflow, inputs and outputs along edges, a loop that breaks and an if/else branch, failure recovery (onFail edges with max attempts, exhaustion behavior, and compatibility with failOnce and loop edges), templates, define / runNamed, the matcher, ephemeral spawn and cleanup, a stopped run with its settled node map (#1424), the reply middleware, the client canvas, the settings page (including a condition and a loop), rename and delete (including factory-id rows such as delivery), the stage chat's scroll rule (a new visible line follows, a filtered node reply moves nothing), the assign dialog's preselected card and its failed and empty board states (#1457, #1458), and a sidebar click that leaves the Workflows section in place. Run npm test in plugins/workflow. The host seam that applies ownerBotId is core/test/task-route.test.js, which also pins that a card written through track_tasks reaches the listener (#1462). The host omission of run Bots, and the grant of stepTools plus the hidden-approval answer, is core/test/flybot.test.js. Which connectors a run Bot may call is core/test/connector-access.test.js, and the chain from a node's tools through spawn to that guard — the built-in delivery nodes included — is core/test/workflow-node-connectors.test.js.

Connector ceiling (#1544)

A connector a Bot switched off is not reopened by a Workflow, whichever surface started the run. A run records the Bots whose switches cap it: the Bot whose run_workflow call started it (createdBy), and the Bot that saved the graph (authoredBy on the workflow record, set by define_workflow and by any save that names a Bot). Every node's run Bot carries cappedBy — the caller, else the graph's author — and core/src/host/connector-access.js intersects the node's tools with those switches, so a node that names a connector its caller or author turned off is refused with turned off for <name>; the step's tools list cannot turn it back on, and the tool is masked out of the run Bot's list too. A run nobody's tool call started (the board's ingest, the canvas, the workflow/* API) has no caller and is capped by the graph's author alone; a save with no Bot behind it records no author, which is the owner's own edit and leaves no ceiling; a graph saved before this has none either. visibleTo on run_workflow/define_workflow is unchanged: the ceiling is the fix, not limiting who may define a Workflow.