brain/wiki/flows-execution/subflows.md
A Subflow is a flow invoked by another flow instead of by its own external trigger — a reusable function at flow granularity. The @activepieces/piece-subflows core piece supplies both halves: the Callable Flow trigger that makes a flow callable, and the actions a parent uses to reach it. A parent calls a subflow once (Call Flow, optionally waiting for a response through a waitpoint) or fans out many calls from one streaming step (Stream CSV to Subflows). The Respond action sends data back to a waiting parent. There is no dedicated transport: every call is a webhook POST to /v1/webhooks/:flowId.
No server entity of its own — subflows are ordinary flows plus two conventions on the webhook path.
data payload and an optional callbackUrl.{ status, data } to the parent's callbackUrl.common.ts — the Callable Flow dropdown (listFlowsWithSubflowTrigger), findFlowByExternalIdOrThrow, request/response types, callback key.x-parent-run-id and x-fail-parent-on-failure.externalId → POSTs { data, callbackUrl? } to the subflow's production webhook. With wait-for-response it creates a WEBHOOK waitpoint, passes its resume URL as callbackUrl, and pauses until Respond calls back; the RESUME branch rethrows when the subflow answered status: 'error'. Without it the step returns as soon as the webhook is acknowledged.Property.File (streaming: true, so it resolves to an ApStreamingFile and accepts a URL, an upload, or a previous step's file — a plain Property.File would materialize an ApFile Buffer before run() starts and OOM), a Callable Flow dropdown target, batchSize (default 100, capped at 10,000), delimiter (comma/tab) and optional extraData merged into every call.
file.body straight into a streaming csv-parse parser — the engine owns the fetch, so the piece carries no HTTP client.subflows/csv.ts (createCsvParser), pinned by test/csv.test.ts. Its bom: true, relax_column_count: true and group_columns_by_name: true are all load-bearing, not defensive — see the CSV gotchas in building-pieces. Because of group_columns_by_name, a row value is string | string[]: duplicate header names arrive as an array rather than silently dropping a column.data = { batchIndex, headers, rows, extraData }. Dispatch is fire-and-forget — no callbackUrl, x-fail-parent-on-failure: false. extraData is re-serialized into every batch, so a large {{step.output}} reference counts against the webhook bodyLimit independently of batchSize.fanOutBatches bounds in-flight dispatches (5) and awaits Promise.race when the window is full, so parsing back-pressures instead of buffering the file.{ headers, firstRow, rowsProcessed, batchesDispatched }.httpClient (retries: 2, so 3 attempts, 1s/2s backoff, 5xx and network errors only — every 4xx aborts on the first try). That is deliberate for a 413 (an oversized batch will never succeed) and for a 404 from a subflow deleted mid-run; the one it costs you is a 429, unreachable self-hosted (rate-limit.ts registers global: false and webhook routes never opt in) but reachable behind a Cloud edge WAF at MAX_IN_FLIGHT=5. After that the fan-out stops reading the stream and throws with the failed batchIndex and the cause. Already-dispatched subflows keep running — at-least-once, no fan-in, no rollback. A parse error on the read side takes the same exit: in-flight dispatches are drained before throwing, so rowsProcessed / batchesDispatched still tell the user how far it got.text/csv is a binary content type on the webhook path, on purpose. It was a 415 before — webhook routes get Fastify's built-ins (json, text/plain) plus webhook-module.ts's explicit list, and the only catch-all parser is scoped to the /ingest PostHog proxy. Adding it was therefore additive, not a behaviour change. It buys compatibility, not capacity: application/octet-stream and multipart/form-data already stream to the File service under the same AP_MAX_FILE_SIZE_MB ceiling (the converted { fileUrl } payload is far below MAX_WEBHOOK_PAYLOAD_SIZE_MB), so the win is senders whose header you cannot change — a partner's nightly export, an S3 event notification, a SaaS "POST my report" hook. Don't send a large CSV as text/plain: Fastify's string parser and the preParsing rawBody hook each buffer the whole body, and it dies at 25MB. Consequence to keep in mind: rawBody is not captured for binary types, so HMAC-over-raw-body sees undefined — fine here because the payload is { fileUrl } anyway. text/tab-separated-values has the same gap and is not yet covered, though the action offers a Tab delimiter.FLOW_TIMEOUT_SECONDS (default 600s, fixed on Cloud). A file whose fan-out cannot finish inside that window is out of scope for v1 — it fails loud, never a silent partial fan-out. See 000015.action.ts) is defaultValue: false but visible, and step retry re-runs run() from the top — for a fire-and-forget fan-out that means re-reading the file from row 0 and re-dispatching every batch that already landed. A failure at batch 900 would duplicate 900 batches. So the action declares retryOnFailure: { defaultValue: false, hide: true }; continueOnFailure stays visible. Call Flow leaves both visible because one invocation is safe to retry.batchSize is capped at 10,000, because "streaming bounds memory" only holds for sane batch sizes. Peak memory is roughly (MAX_IN_FLIGHT + 1) × batchSize × rowSize, so an unbounded batchSize defeats the whole design. The cap that bites first in practice is not memory but the webhook bodyLimit (max(AP_MAX_FILE_SIZE_MB + 4, AP_MAX_FLOW_RUN_LOG_SIZE_MB + 4, 25) MB, 54MB by default) — a wide-row file will 413 well before 10,000 rows.(inactive); streaming to a disabled flow throws before the first request.Community, Enterprise, Cloud — core piece, no plan flag.
Entry point: streamCsvToSubflows.run, which drives fanOutBatches over a streaming CSV parser.
packages/pieces/core/subflows/src/index.ts — piece definitionpackages/pieces/core/subflows/src/lib/actions/call-flow.ts — Call Flow (one call, optional wait-for-response)packages/pieces/core/subflows/src/lib/actions/stream-csv-to-flow.ts — Stream CSV to Subflows (streaming fan-out)packages/pieces/core/subflows/src/lib/actions/respond.ts — Respond (subflow → parent)packages/pieces/core/subflows/src/lib/triggers/callable-flow.ts — Callable Flow triggerpackages/pieces/core/subflows/src/lib/fan-out.ts — batching + bounded-concurrency dispatch loop, transport-agnosticpackages/pieces/core/subflows/src/lib/common.ts — flow dropdown, lookup helpers, request/response typespackages/server/api/src/app/webhooks/webhook-request-converter.ts — text/csv in BINARY_CONTENT_TYPE_PATTERNS, parent-run headers