Back to Activepieces

Flows

brain/wiki/flows-execution/flows.md

0.87.09.0 KB
Original Source

Flows

Flows are the core automation primitive: a versioned directed graph of trigger + action steps stored as a JSONB tree. The module covers the full lifecycle — draft editing, publishing, enable/disable, folders, sample data, human-input forms/chat, and the XYFlow visual builder.

Entities & services

  • Flow — persistent record: status (ENABLED/DISABLED), folderId, publishedVersionId, externalId, operationStatus (NONE/DELETING/ENABLING/DISABLING), ownerId, createdBy (FlowCreator: {type: MCP|AGENT, id}, null for humans → "AI" badge).
  • FlowVersion — immutable-once-LOCKED snapshot of the graph. DRAFT is editable; current schemaVersion is '22'. Holds trigger (full graph JSONB), connectionIds, agentIds, notes.
  • Folder — simple per-project grouping, case-insensitive unique.
  • Core service: flow.service.ts; single controller endpoint POST /v1/flows/:id.

How it works

  • All 26 modification types dispatch through one endpoint POST /v1/flows/:id with a FlowOperationRequest discriminated union (ADD/UPDATE/DELETE/MOVE_ACTION, branch ops, UPDATE_TRIGGER, LOCK_AND_PUBLISH, CHANGE_STATUS, CHANGE_FOLDER, IMPORT_FLOW, SAVE_SAMPLE_DATA, notes, etc.).
  • Draft vs published: editing always hits DRAFT. LOCK_AND_PUBLISH snapshots to a LOCKED version + sets publishedVersionId; USE_AS_DRAFT copies it back. Only published flows can be enabled.
  • Publish/enable side effects: lock version → register trigger source (webhook/polling/app-event) → invalidate execution cache → emit WebSocket event → fire-and-forget telemetry. Disable unregisters the trigger source.
  • Sample data is captured per step (input+output) as File entities per flow version.

Gotchas

  • Step output nesting (schema v21+): every step output is wrapped as { output, error? }; expressions must use the ['output'] accessor. The v20→v21 migration rewrites existing expressions via expression-rewriter.
  • Continue on Failure: CODE/PIECE steps with continueOnFailure.value: true carry onSuccess/onFailure sub-trees under settings.errorHandlingOptions.continueOnFailureBranches.
  • addActionUtils.clone() is the single chokepoint for renaming copied steps and rewriting their {{ }} references — paste, duplicate step, and duplicate branch all route through it. It walks the whole settings deliberately, not just settings.input: router conditions live at settings.branches[].conditions[][].firstValue/.secondValue and loop expressions at settings.items, and an 'input' in settings guard silently skipped both (GIT-1075). Two things to respect when touching it: settings.sourceCode is excluded on purpose (it holds a user program, and a literal {{ … }} in a code step was being rewritten), and the remap must stay a single pass over the name map — applying one rename at a time rewrites its own output whenever a copied step's new name equals another copied step's old name, which happens on cross-flow paste into a flow that lacks the clipboard's names.
  • Paste order follows selection order, not flow order. _getActionsForCopy's .sort((a, b) => allSteps.indexOf(a) - ...) compares deep clones, so indexOf is always -1 and the sort is a no-op. Harmless for a single step; it decides the chaining order for a multi-step paste.
  • createdBy (automated source) is distinct from ownerId (current owning user).
  • List filtering: folderId uses the string sentinel "NULL" for uncategorized; folderIds (array) loads all foldered flows in one request.
  • Builder is Zustand-sliced (flow/run/canvas/step-form/piece-selector state). Canvas supports vertical (default) and horizontal orientations, and PNG export via a hand-rolled clone-and-rasterize pipeline.
  • A stray DRAFT row makes a published flow look blank. "Latest version" is resolved by getFlowVersionOrThrow({ versionId: undefined }), which is just ORDER BY created DESC with no state filter — so any DRAFT created after the LOCKED version is what the builder opens. This is why a half-created draft is a user-visible outage, not DB litter: recovering needs the row deleted or a USE_AS_DRAFT. Editing a published flow used to commit the empty DRAFT and import the locked content into it as two separate writes, so an import failure (field case: RangeError: Maximum call stack size exceeded on deeply nested flows — the recursive sanitizeObjectForPostgresql before the save is the prime suspect) left the flow rendering empty forever. GIT-1590 / Pylon #5225; the RangeError itself is still open as GIT-1593.
  • Draft creation is now atomic, in two different ways — know which one you're in. createNewDraftIfVersionIsPublished runs createEmptyVersion + the IMPORT_FLOW loop in one transaction(), threading the entityManager down through applyOperation to the updateLastModified side effect. The user's operation deliberately stays outside that transaction (it would hold Postgres open across prepareRequest piece-metadata fetches and non-rollbackable file/webhook side effects) and is instead undone by a compensating delete of the freshly created draft. flowService.create wraps the flow row + first empty version the same way, so a failure can't leave a zero-version, unopenable flow.
  • flowVersionSideEffects.preApplyOperation writes on the default connection, so it escapes any caller transaction. handleSampleDataDeletion and handleUpdateTriggerWebhookSimulation take no entityManager; a write they make from inside a transaction() survives its rollback. They early-return for IMPORT_FLOW/UPDATE_SAMPLE_DATA_INFO, so the transactional path above is safe today — but adding an operation type to it silently reintroduces partial commits. updateLastModified sits outside that swallow-all catch on purpose: a swallowed statement failure inside a transaction poisons it and resurfaces as a confusing "transaction is aborted" error on the next statement.
  • transaction() (core/db/transaction.ts) is a bare dataSource.transaction() — it acquires a new connection, not a savepoint. Nesting it deadlocks, so check every caller before wrapping a service method that others may already call inside a transaction.
  • Flows stuck in DELETING keep eating the active-flow limit. Deletion is a durable BullMQ system job (delete-flow-<flowId>), not synchronous: delete() sets operationStatus=DELETING and enqueues, and the row plus status=ENABLED only go away when the job finishes. That job runs sampleDataService.deleteForFlow, whose DELETE FROM file … metadata->>'flowId'=? had no index — on the large prod file table it seq-scans, blows statement_timeout, exhausts its 2 attempts and lands permanently in the failed set. The flow is then hidden from the UI list (which filters !=DELETING) but still counted by the active-flows quota (getUsage counts status=ENABLED), so Publish silently shows the "Purchase Extra Active Flows" dialog instead of publishing — this is what breaks the webhook-should-return-response e2e monitor. Stuck flows are functionally dead (preDelete disables the trigger before the failing delete), so forcing their rows away is safe. Fixes on fix/flow-delete-sample-data-timeout: a partial expression index idx_file_sample_data_flow_id on file (type, (metadata->>'flowId')), plus operationStatus != DELETING in the active-flow counts so the quota stops depending on delete-job success.

Editions

CE has full authoring/publishing/folders/forms. EE/Cloud add owner transfer, piece filtering, template sharing, and active-flow quota enforcement on publish/enable.

Key files

Entry point: flowService, exported from flows/flow/flow.service.ts and called per-request as flowService(request.log) from the flow controller.

  • packages/server/api/src/app/flows/ — server module: flow service + REST controller, folders, step-run sample data, human-input form/chat endpoints
  • packages/server/api/src/app/flows/flow-version/migrations/ — schema migrations, including v21 step-output nesting and the expression-rewriter
  • packages/core/execution/src/lib/flows/ — shared types: Flow, FlowVersion, the FlowOperationRequest union, actions, triggers
  • packages/web/src/features/flows/ — client API, hooks, components, export/import utils
  • packages/web/src/app/builder/ — visual builder: Zustand state slices, step settings, step data panel, test-step, data selector
  • packages/web/src/app/builder/flow-canvas/ — XYFlow canvas, orientation layout, canvas controls, PNG export
  • packages/web/src/components/custom/smart-output-viewer/ — friendly and raw output rendering for test-step and run details
  • packages/web/src/lib/path-utils.ts — dot/bracket path resolution with the wrapper-key fallback
  • packages/web/src/app/routes/automations/index.tsx — flows list page

Paths verified 2026-07-17. An earlier version pointed the shared flow types at packages/core/shared/src/lib/automation/flows/; they moved to packages/core/execution/src/lib/flows/. expression-rewriter.ts also left that tree and now lives in the server's flow-version/migrations/.