--- name: uipath-maestro-flow description: "TRIGGER for `.flow` / `.flow.ts` files and UiPath Flow / Maestro Flow / Maestro Automate requests: build, edit, run, debug, fix, or evaluate a flow. Author with the TypeScript builder SDK (`@uipath/maestro-builder-sdk`): nodes, triggers, schedules, connectors, IXP document extraction (add or list IXP models), inline agents, HITL, chat/voice flows, bindings, brownfield edits, and the check/compile/validate loop. Operate: upload, publish, deploy, debug a real run, trigger a process, job status/traces, pause/resume/cancel/retry an instance. Diagnose: faulted runs, incidents, runtime variables, why validate passed but the run failed. Evaluate: eval sets, evaluators, simulations, eval runs. Case plans (`caseplan.json`) → uipath-maestro-case; BPMN (`.bpmn`, `.bpmn.ts`) → uipath-maestro-bpmn. DO NOT TRIGGER for raw IXP labelling or prompt tuning outside a Flow → uipath-ixp; standalone agents → uipath-agents; C#/XAML → uipath-rpa." allowed-tools: Bash, Read, Write, Edit, Glob, Grep, AskUserQuestion --- # UiPath Flow — TypeScript Builder SDK UiPath Flow orchestrations can be authored in TypeScript using the `@uipath/maestro-builder-sdk` package. The SDK provides a builder API to construct a Flow graph, allowing developers to define inputs, outputs, steps, and control flow in a type-safe manner. The graph is "compiled" down to a Flow JSON, which is the artifact used for executing the Flow on the UiPath platform. An existing Flow JSON can also be decompiled back into TypeScript for editing. ## Project layout `@uipath/maestro-builder-sdk` is installed globally (`npm install -g`); `examples/` contains authored examples, and `references/` contains the details routed from this guide. A Flow is authored as `.flow-sdk/.flow.ts` inside its project folder `//`, and it imports the package directly. **Authoring files live in `.flow-sdk/`; the compiled artifact does not.** `.flow-sdk/` is the SDK's own work directory: the source, `bindings.json`, `connectors/` and `connectors-local/` all go there by default, relative to the directory you run `uip` from. Each Flow project keeps its own, so a solution can hold several flows; Studio Web never reads it, and `uip solution pack`/`upload` and `flow debug` leave it out. **Run the SDK verbs (`flow check`, `compile`, `decompile`, `merge`, `registry pull`/`prepare`, `node .flow-sdk/*.pipeline.mjs`) from the project folder `//`**, as `( cd / && … )` when your shell does not keep its directory between commands; every `.flow-sdk/` path in this guide and its references is relative to that folder. Everything else runs from the workspace root. Scaffold the project first, seed the source from it, then emit back into it — `compile -o` is the authority over where the emitted file is written. `` and `` are the request's own names, used verbatim: a request that gives one name for both ("inside a solution of the same name") uses it for both, and a request that names only the Flow uses `` for both. **Look for an existing solution before `uip solution init`:** run `find . -maxdepth 2 -name '*.uipx'`. If one exists and a user can answer, ask which to use (one option per solution, then "Create a new solution", then "Something else") and scaffold nothing until they do; never create a second solution silently. Headless, use the solution the request names, else the only one present, else a new one named as above, and record the choice in the final response. ```bash uip solution init ( cd && uip maestro flow init --sdk-source ) # edit //.flow-sdk/.flow.ts ( cd / && uip maestro flow compile .flow-sdk/.flow.ts -o .flow ) ``` Do not hand-write the skeleton. `--sdk-source` decompiles the trigger-only artifact `flow init` writes into the project's `.flow-sdk/.flow.ts`, creating the folder; the source carries the flow id and name the product already assigned — a hand-written `flow('')` invents an id instead. So the stub is the seed rather than litter: the first `compile -o` overwrites it in place. `init` refuses an existing source file unless `--force`; when the source is already there, drop `--sdk-source`. **Maestro Automate is `--automate` on the same `flow init`:** when the request names **Maestro Automate** as the product, run `( cd && uip maestro flow init --automate --sdk-source )`; the bare verb ("automate invoice intake") asks for a plain Flow. Nothing after `init` changes; the flag writes `runtimeOptions.profile` into `operate.json` plus a `.maestro_automate` marker (how Orchestrator and Studio Web tell the two apart), and `compile -o` rewrites only the `.flow`, so both survive. An existing project needs no `init`: skip the first two commands and seed from the `.flow` that is already there with `( cd / && uip maestro flow decompile .flow -o .flow-sdk/.flow.ts --no-pipeline )` (`--no-pipeline` skips the brownfield helper, [`references/brownfield.md`](references/brownfield.md)). Exactly one emitted `.flow` may exist, at `//.flow`, and never a second copy at the workspace root — validators and evidence collectors cannot choose safely between duplicates. Emitting to the root is correct only for the packaged-SDK local gates, which never scaffold a project; pick the loop first ([`references/CLI-LOOP.md`](references/CLI-LOOP.md)) and do not mix the two. **Install the SDK first, once per machine:** `npm install -g @uipath/maestro-builder-sdk`; skip it when already installed, and see [`references/CLI-LOOP.md`](references/CLI-LOOP.md#installing-the-package) for the checks and failure handling. Integrations with non-UiPath systems are handled through connectors. **Choose the node before writing it.** For an external service or data (weather, Slack, a REST API), run `uip maestro registry search ''` over the local connector library, unless the request names the transport itself ("over HTTP, not a connector" means `http()`): a hit is a connector, `"total": 0` is a miss and means `http()`, and a usage error means the library is not cached, so run `uip maestro registry pull` first. For document extraction or another tenant capability (agent, process), which that library does not hold, run the family's `uip maestro flow registry search` ([`references/ixp.md`](references/ixp.md), [`references/agent.md`](references/agent.md)). A `script()` returning fixed values is never a stand-in for that step, and `mock()` only marks a capability the search proved absent. Connectors require [`.flow-sdk/bindings.json`](references/bindings.md). `uip maestro registry pull` writes a descriptor per referenced connector to `.flow-sdk/connectors/.ts`, and caches the library itself outside the project. Prepared connector modules live at `.flow-sdk/connectors-local/.ts`; their descriptor data is kept separately below `.flow-sdk/connectors-local/descriptors//`. Because the source sits in `.flow-sdk/` too, it imports them as `./connectors/.ts` and `./connectors-local/.ts`. ### The connector loop: author → check → prepare → check → compile Authoring never waits on `prepare`: once the search above has chosen the node, no further discovery command precedes the source. Write the connector step from the task's own words — the fields you intend, `lookup()` tokens for ids, `{ object: '' }` for a generic operation — then run `uip maestro flow check .flow-sdk/.flow.ts --source`. Check names every prepare you owe, with the exact command: `OBJECT_UNPREPARED` for an unmaterialized object, `CUSTOM_FIELDS_UNPREPARED` for an input outside the tenant-agnostic snapshot, `LOOKUP_UNRESOLVED` for a lookup token with no recorded value, `CONNECTOR_INPUT` for a field the operation does not declare. Run that one `uip maestro registry prepare ` — `--object`, `--resolve` and `-f` compose in a single invocation, it finds the connection itself, writes `.flow-sdk/bindings.json`, and repoints your import at the generated `./connectors-local/.ts` descriptor — then re-run `check` and compile. Where two flows import the same connector it names them instead of guessing, and asks for `--source`. The gate this replaces still holds for schema-dynamic operations (`loadByDefault`, dependent dropdowns, `customFieldsRequestDetails`): the static library descriptor is not sufficient there, and the prepare that check names — with every required `-f NAME=VALUE` — is what creates the design-time schema-replay cache. Do not substitute manual `resources run list` lookups plus a static `./connectors/.ts` import: the lookups choose values but do not create that cache. After compiling, inspect the emitted connector configuration. `flow validate` can accept a missing cache, so completion requires non-null `customFieldsRequestDetails` whose parent values match the runtime inputs. ### Hello world Flow ```ts import { flow, script, input, out, types } from '@uipath/maestro-builder-sdk'; export default flow('hello').name('Hello') .input({ name: types.string }).output({ greeting: types.string }) .step('greet', script({ code: 'return `Hello ${$vars.start.output.name}`;' })) .return({ greeting: out('greet') }).build(); ``` A script is a first-class Flow node; it runs inline JavaScript and returns a value. The `start` step is the default name for a "manual trigger", which carries the flow's inputs. A Flow can have outputs, which are returned to the caller when the flow completes successfully. ## Lifecycle The `uip maestro flow` commands keep source checks, emission, and compiled-artifact checks explicit while the installed `@uipath/maestro-builder-sdk` owns their semantics. A workspace with `{ "flowSdk": { "emitOnly": true } }` in a `package.json`, or `FLOW_SDK_EMIT_ONLY=1`, makes `uip maestro flow compile` emit-only and makes both `flow check` modes refuse. Product validate owns final structural verification in that mode; use product debug only when the node family and the requested evidence support it. **Run the correct loop for your packaging mode: [`references/CLI-LOOP.md`](references/CLI-LOOP.md)**. ## Editing an existing flow In brownfield work, preserve the supplied source, step names, and unaffected wiring. Insert a step by moving the old edge through it, not by creating a second path. If only emitted `.flow` JSON exists, decompile it, compile the pristine baseline, edit narrowly, and merge the delta back into the original. These are before/after judgments; no final-artifact checker can prove them. For a narrow edit, `decompile` writes `.flow-sdk/.pipeline.mjs`, which runs the loop in two invocations and gets the baseline ordering right. It keeps its baseline, edited and merged `.flow` files in `.flow-sdk/` too: ```bash uip maestro flow decompile .flow -o .flow-sdk/.flow.ts node .flow-sdk/.pipeline.mjs # captures the pristine baseline # edit .flow-sdk/.flow.ts narrowly node .flow-sdk/.pipeline.mjs # compiles the edit and merges it back uip maestro flow validate .flow-sdk/.merged.flow --output json ``` Validate the merged artifact, never the intermediate edited compile. If the source must stay inside the Flow project (for example, to preserve relative sidecars), keep baseline, edited, and candidate `.flow` files in an external `.flow-work/` directory. Validate the candidate, replace the canonical artifact, and leave exactly one `.flow` under the project. The reference below contains the copyable safe-project sequence. **True-brownfield procedure:** **[`references/brownfield.md`](references/brownfield.md)**. ## Builder frame The quick start above shows the shape — `flow(id)`, declarations, nodes, `.return(...)`, `.build()`. Three things it does not show: - **`.var(name, types.*, default?)`** declares a flow VARIABLE: a value more than one step writes or reads. `.input` and `.output` are the flow's contract with its caller; a var is the state in between. A step writes one with `{ updates: { name: } }`. Inputs, outputs and vars use one of the six types the Flow CLI and Workbench offer: `types.string`, `types.number`, `types.boolean`, `types.object`, `types.array`, `types.file`. The other `types` members (`integer`, `float`, `double`, `date`, `datetime`, `jsonSchema`) come from the Case and BPMN builders; the Flow builder and `validate` accept them without an error, but Workbench does not offer them, so write a date as `types.string` and an integer as `types.number`. - **`.return(...)` ends a PATH. `.terminate(...)` ends the RUN.** They look interchangeable on a straight chain and are not: inside a `.parallel` arm a terminate aborts the sibling arms mid-flight, where a return leaves them going. - **Expressions are how a step names something that is not a literal.** There is one per kind of thing you can refer to: | | refers to | | --- | --- | | `input(name)` | a flow input | | `v(name)` | a flow variable | | `out(step, path?)` | a step's result, whole or one field | | `err(step, field?)` | a FAILED step's error envelope — only inside its handler | | `ran(step)` | whether a step ran at all, as a boolean | | `lit(value)` | a constant, where a raw value would be ambiguous | | ``js`…` `` / ``tmpl`…` `` | an expression, or a string, you write yourself | `ran(step)` earns an early mention: when arms converge, one shared continuation usually reads better than the same work duplicated per arm, and `ran` is how that continuation asks whether the value it wants was produced. ## API index **Every signature, option shape and field is indexed in the installed package**, not in this guide. Two files, keyed by the kind of name you have: | you have | look in | a row gives you | | --- | --- | --- | | a field or method — `outcomePorts`, `stepToList` | `dist/api-members.md` | the shape that declares it, and the lines that do | | an exported symbol — `HitlInputs`, `hitl`, `FlowBuilder` | `dist/api-index.md` | its kind, area, and the lines that declare it | Both ship in the installed `@uipath/maestro-builder-sdk` package, and a row's path is relative to that package's root: ``` | `outcomePorts` | property | `HitlInputs` | `dist/core/actions.d.ts:583-597` | ``` Each file's own header names the repo and generator it came from, and says not to edit it there — the rows are regenerated from the declarations on every build. **Match one name; do not read either file end to end.** Then read the span — it is the whole declaration including its doc comment, so one read answers the question, with the types and the `@remarks` and `@example` bodies in full. No searching and no shell, so it works the same on Windows. A field or method name is the usual case, because these references are one line per field — so `api-members.md` is usually the one you want. Both cover all three entry points, `/case` and `/bpmn` included. They live in the package rather than here **because the spans are only true of one build**: a line moves whenever a declaration above it changes, and this guide ships on its own cadence. An index beside the `.d.ts` files it points into cannot disagree with them. Read the `.d.ts`, never `dist/*.js`: the compiled JavaScript carries no types and no comments, so searching it is how a lookup turns into twenty tool calls. A name in neither index is probably a RUNTIME output key — a human task's `Action`, an error envelope's fields — which no declaration carries; those are in the node references. The sibling surfaces' runtime-only decisions live in their own skills: `uipath-maestro-case` and `uipath-maestro-bpmn`; neither is needed to build a Flow. ## Supported node types The table is the authoritative router. `Section` identifies the governed H2; `Reference` carries the details; `Example` names the one file to read. Paths under `examples/` resolve inside this skill folder. | Node or surface | Emitted node type | Builder | Section | Reference | Example | |---|---|---|---|---|---| | Manual trigger | `core.trigger.manual` | omit `.trigger(...)` | [Manual trigger](#manual-trigger) | [manual-trigger.md](references/manual-trigger.md) | `examples/GreenhouseWatering.flow.ts` | | Scheduled trigger | `core.trigger.scheduled` | `scheduled(...)` | [Scheduled trigger](#scheduled-trigger) | [scheduled-trigger.md](references/scheduled-trigger.md) | `examples/HerbariumDispatch.flow.ts` | | Connector event trigger | `uipath.connector.trigger..` | `onEvent(...)` | [Connector events](#connector-events) | [event-trigger.md](references/event-trigger.md) | `examples/DoorbellLog.flow.ts` | | Connector event wait | `uipath.connector.event..` | `waitForEvent(...)` | [Connector events](#connector-events) | [event-trigger.md](references/event-trigger.md) | `examples/PlanetariumConfirmation.flow.ts` | | Form trigger | `core.trigger.form` | `formTrigger(...)` | [Form trigger](#form-trigger) | [form-trigger.md](references/form-trigger.md) | `examples/BakeOffEntryForm.flow.ts` | | Conversation trigger | `core.trigger.conversation` | `conversationTrigger(...)` | [Conversational](#conversational) | [conversational.md](references/conversational.md) | `examples/LibraryDeskChat.flow.ts` | | Voice trigger | `core.trigger.voice` | `voiceTrigger(...)` | [Voice](#voice) | [voice.md](references/voice.md) | `examples/HarbourRadioLine.flow.ts` | | Standalone HTTP | `core.action.http` | `http({ managed: false, ... })` | [HTTP](#http) | [http.md](references/http.md) | `examples/LighthouseSignal.flow.ts` | | Managed HTTP | `core.action.http.v2` | `http({ managed: true, ... })` | [HTTP](#http) | [http.md](references/http.md) | `examples/ObservatorySeeing.flow.ts` | | Script | `core.action.script` | `script(...)` | [Script](#script) | [script.md](references/script.md) | `examples/GreenhouseWatering.flow.ts` | | Transform | `core.action.transform` | `transform(...)` | [Transform](#transform) | [transform.md](references/transform.md) | `examples/TrailLogSummary.flow.ts` | | Filter | `core.action.transform.filter` | `transform({ variant: 'filter', ... })` | [Transform](#transform) | [transform.md](references/transform.md) | `examples/TrailLogSummary.flow.ts` | | Map | `core.action.transform.map` | `transform({ variant: 'map', ... })` | [Transform](#transform) | [transform.md](references/transform.md) | `examples/TrailLogSummary.flow.ts` | | Group by | `core.action.transform.group-by` | `transform({ variant: 'group-by', ... })` | [Transform](#transform) | [transform.md](references/transform.md) | `examples/TrailLogSummary.flow.ts` | | Integration Service action | `uipath.connector..` (Data Fabric / Data Service — the ops the native family lacks: file record fields, events: `uipath.connector.uipath-uipath-dataservice.*`) | `connector(...)` | [Integration Service connectors](#integration-service-connectors) | [connector-params.md](references/connector-params.md) | `examples/ClubDirectory.flow.ts` | | Data Fabric read | `core.datafabric.read` (`resultMode: 'multiple'` selects its 1.4 definition; `limit` caps at 1000) | `dataFabricRead(...)` | [Data Fabric](#data-fabric) | [data-fabric.md](references/data-fabric.md) | `examples/BeeHiveLedger.flow.ts` | | Data Fabric create | `core.datafabric.create` | `dataFabricCreate(...)` | [Data Fabric](#data-fabric) | [data-fabric.md](references/data-fabric.md) | `examples/BeeHiveLedger.flow.ts` | | Data Fabric update | `core.datafabric.update` | `dataFabricUpdate(...)` | [Data Fabric](#data-fabric) | [data-fabric.md](references/data-fabric.md) | `examples/BeeHiveLedger.flow.ts` | | Data Fabric delete | `core.datafabric.delete` (declares NO outputs) | `dataFabricDelete(...)` | [Data Fabric](#data-fabric) | [data-fabric.md](references/data-fabric.md) | `examples/BeeHiveLedger.flow.ts` | | Subflow | `core.subflow` | `subflow(...)` | [Subflow](#subflow) | [subflow.md](references/subflow.md) | `examples/RecipeScaler.flow.ts` | | Human task | `uipath.human-in-the-loop` | `hitl(...)` | [Human task](#human-task) | [hitl.md](references/hitl.md) | `examples/GallerySubmission.flow.ts` | | Human quick form | `uipath.human-in-the-loop.quick-form` (one exit per outcome: `.stepSwitch` routes them, a plain `.step()` continues every outcome) | `hitl({ variant: 'quick-form', ... })` | [Human task](#human-task) | [hitl.md](references/hitl.md) | `examples/FieldTripQuickForm.flow.ts` | | Human action app | `uipath.human-in-the-loop.coded-action-app` | `hitl({ variant: 'action-app', ... })` | [Human task](#human-task) | [hitl.md](references/hitl.md) | `examples/KilnReview.flow.ts` | | RPA workflow | `uipath.core.rpa-workflow.` | `rpaWorkflow(...)` | [RPA workflow](#rpa-workflow) | [rpa-workflow.md](references/rpa-workflow.md) | `examples/WorkshopInventory.flow.ts` | | Queue item | `core.action.queue.create*` | `queueItem(...)` | [Queue item](#queue-item) | [queue.md](references/queue.md) | `examples/HerbariumDispatch.flow.ts` | | Summarize | `uipath.pattern.deep-rag` | `summarize(...)` | [AI patterns](#ai-patterns) | [summarize.md](references/summarize.md) | `examples/OralHistoryDigest.flow.ts` | | Batch transform | `uipath.pattern.batch-transform` | `batchTransform(...)` | [AI patterns](#ai-patterns) | [batch-transform.md](references/batch-transform.md) | `examples/FossilCatalogEnrich.flow.ts` | | Branch | `core.logic.decision` | `.branch(...)` | [Branch](#branch) | [branch.md](references/branch.md) | `examples/GreenhouseWatering.flow.ts` | | Switch | `core.logic.switch` | `.switch(...)` | [Switch](#switch) | [switch.md](references/switch.md) | `examples/BeltProgression.flow.ts` | | Parallel / Merge | `core.logic.merge` | `.parallel(...)` | [Parallel branches](#parallel-branches) | [parallel-merge.md](references/parallel-merge.md) | `examples/ConcertSoundcheck.flow.ts` | | Loop | `core.logic.loop` | `.loop(...)` | [Loops](#loops) | [loops.md](references/loops.md) | `examples/ClubDirectory.flow.ts` | | Do while | `core.logic.dowhile` | `.doWhile(...)` | [Do while](#do-while) | [loops.md](references/loops.md) | `examples/MeteorShowerPages.flow.ts` | | Return / End | `core.control.end` | `.return(...)` | [Return and end](#return-and-end) | [return.md](references/return.md) | `examples/GreenhouseWatering.flow.ts` | | Terminate | `core.logic.terminate` | `.terminate(...)` | [Terminate](#terminate) | [terminate.md](references/terminate.md) | `examples/AquariumSafetyStop.flow.ts` | | Placeholder | `core.logic.mock` | `mock()` | [Placeholder](#placeholder) | [placeholder.md](references/placeholder.md) | `examples/FestivalMapScaffold.flow.ts` | | Error handler | `error` handle on an action node | `.onError(...)` | [Error handling](#error-handling) | [error-handling.md](references/error-handling.md) | `examples/ObservatorySeeing.flow.ts` | | Delay | `core.logic.delay` | `delay(...)` | [Delay](#delay) | [delay.md](references/delay.md) | `examples/LighthouseSignal.flow.ts` | | API workflow | `uipath.core.api-workflow.` | `apiWorkflow(...)` | [API workflow](#api-workflow) | [api-workflow.md](references/api-workflow.md) | `examples/BirdCountLookup.flow.ts` | | Agentic process | `uipath.core.agentic-process.` | `agenticProcess(...)` | [Agentic process](#agentic-process) | [agentic-process.md](references/agentic-process.md) | `examples/NeighborhoodWalkPlanner.flow.ts` | | Agent resource | `uipath.core.agent.` | `agent(...)` | [Agent resource](#agent-resource) | [agent.md](references/agent.md) | `examples/PlantNameAdvisor.flow.ts` | | Inline agent | `uipath.agent.autonomous` | `inlineAgent(...)` | [Inline agent](#inline-agent) | [inline-agent.md](references/inline-agent.md) | `examples/PostcardCaption.flow.ts` | | IxP extraction | `uipath.ixp..-` | `ixpExtract(...)` | [Document extraction](#document-extraction) | [ixp.md](references/ixp.md) | `examples/ArchiveCardExtract.flow.ts` | | Document classify | `uipath.document.classify` | `documentClassify(...)` | [Document classify and Dynamic Extract](#document-classify-and-dynamic-extract) | [document-pipeline.md](references/document-pipeline.md) | `examples/SeedPacketReader.flow.ts` | | Dynamic extract | `uipath.ixp.extract-document-builder` | `dynamicExtract(...)` | [Document classify and Dynamic Extract](#document-classify-and-dynamic-extract) | [document-pipeline.md](references/document-pipeline.md) | `examples/SeedPacketReader.flow.ts` | | Published function | `uipath.core.function.` | `publishedFunction(...)` | [Published function](#published-function) | [published-function.md](references/published-function.md) | `examples/TideTableConverter.flow.ts` | | Conversation message wait | `uipath.conversational.wait-for-message` | `waitForMessage(...)` | [Conversational](#conversational) | [conversational.md](references/conversational.md) | `examples/LibraryDeskChat.flow.ts` | | Conversational agent | `uipath.agent.conversational` | `conversationalAgent(...)` | [Conversational](#conversational) | [conversational.md](references/conversational.md) | `examples/LibraryDeskChat.flow.ts` | | Conversation send message | `uipath.conversational.send-message` | `sendMessage(...)` | [Conversational](#conversational) | [conversational.md](references/conversational.md) | `examples/LibraryDeskChat.flow.ts` | | Voice outgoing call | `uipath.conversational.voice.create-outgoing-call` | `createOutgoingCall(...)` | [Voice](#voice) | [voice.md](references/voice.md) | `examples/PotteryStudioCallback.flow.ts` | | Voice agent | `uipath.agent.voice` | `voiceAgent(...)` | [Voice](#voice) | [voice.md](references/voice.md) | `examples/PotteryStudioCallback.flow.ts` | | Voice end call | `uipath.conversational.voice.end-call` | `endCall(...)` | [Voice](#voice) | [voice.md](references/voice.md) | `examples/PotteryStudioCallback.flow.ts` | ## Manual trigger The default start node accepts on-demand caller input; omit `.trigger(...)`. Signature: `flow(id).input({...}).step(...).build()`. ```ts export default flow('lookup').input({ id: types.string }).output({ value: types.string }) .step('read', script({ code: 'return $vars.start.output.id;' })) .return({ value: out('read') }).build(); ``` Choose it when a caller, test, or another process should start each run. **Reference: [`references/manual-trigger.md`](references/manual-trigger.md)** ## Scheduled trigger A platform timer starts the flow on a recurring interval. Signature: `.trigger(scheduled({ every: string }))`. `every` takes an ISO-8601 repeating interval, or a Quartz cron expression (e.g. `'0 0 2 * * ?'`), which selects the trigger's 1.2 definition automatically. ```ts export default flow('nightly') .trigger(scheduled({ every: 'R/P1D' })) .step('rollup', script({ code: 'return { ok: true };' })) .build(); ``` Prefer self-contained variables because there may be no caller supplying inputs. **Reference: [`references/scheduled-trigger.md`](references/scheduled-trigger.md)** ## Form trigger A person starts the flow by submitting a form (`core.trigger.form`); the submitted values ARE the flow's inputs. Signature: `.trigger(formTrigger())` — no arguments; the form's fields are derived from `.input()` (one per input, required unless it has a default). ```ts export default flow('expense') .input({ amount: types.number, reason: types.string }) .trigger(formTrigger()) .step('log', script({ code: 'return $vars.start.output.amount;' })) .build(); ``` Locally `--input` supplies the values; no rung renders a form. **Reference: [`references/form-trigger.md`](references/form-trigger.md)** ## Entry points (multiple triggers) A flow may have more than one root. `.trigger()` / `.input()` stay the DEFAULT root; `.entryPoint(id, trigger, { inputs?, version? }, prefixFn?)` adds another — its own trigger node, its own scoped inputs (read them with `entryInput('', '')`), and an optional prefix that runs before the root joins the first shared step. A prefix that ends terminally (or hands off with `.stepToRef(...)`) joins nothing. ```ts flow('order-intake') .input({ order: types.object }) // default (manual) root .entryPoint('nightly', scheduled({ every: 'R/P1D' }), { inputs: { batchDate: types.string }, }, (b) => b.step('loadBatch', script({ code: 'return { note: $vars.nightly.output.batchDate };', returns: 'object' }))) .step('normalize', script({ code: 'return 1;' })) // shared body ``` ## Connector events Start on, or pause for, an Integration Service event subscription. Signatures: `.trigger(onEvent(subscription))`; `.step(name, waitForEvent(subscription))`. ```ts const mail = { connector: 'uipath-microsoft-outlook365', event: 'email-received', where: { parentFolderId: inboxId }, connection: 'outlook', folder: 'shared' }; // bindings.json labels, both required export default flow('mail').trigger(onEvent(mail)) .step('reply', script({ code: 'return $vars.start.output.subject;' })).build(); ``` An id-valued `where` parameter is a `lookup()` token: `registry prepare ` resolves it, writes bindings, and stores the vocabulary `check` validates `where`/`filters` against (a wrong-case filter field is an error — the platform drops it silently). A generic event (`record-created`/`record-updated`) needs `object: ''` — never put it in `where`. Use the reference's completion contract before debugging: an injected start payload exercises downstream wiring but is not a subscription witness. **Reference: [`references/event-trigger.md`](references/event-trigger.md)** ## HTTP Standalone HTTP keeps non-2xx responses on its success output. Managed HTTP routes them through its error port. Both expose JSON response bodies as parsed values. Signature: `http({ method?, url, managed, connection?, folder?, targetConnector?, headers?, query?, body?, contentType?, timeout?, retryCount?, returns?, branches? })`. ```ts .step('getPolicy', http({ method: 'GET', url: policyUrl, managed: true, returns: { limit: 'number' }, branches: [{ name: 'throttled', condition: js`$vars.getPolicy.output.statusCode === 429` }] })) .stepToList('branch-throttled', (b) => b.return({})) .step('limit', script({ code: 'return $vars.getPolicy.output.body.limit;' })) ``` Match `managed` to the scenario's node; connector auth needs both `connection` and `folder` from `.flow-sdk/bindings.json`. A `branch-` side exit uses `.stepToList`; omit both bindings for manual/implicit mode. **Reference: [`references/http.md`](references/http.md)** ## Script Run inline JavaScript for computation that is not a first-class Flow node. Signature: `script({ code: string })`; read the result with `out(step, path?)`. ```ts .step('normalize', script({ code: ` const amount = Number($vars.amount); return { amount, valid: Number.isFinite(amount) }; ` })) ``` Use a first-class action when the scenario names one; use script for computation. **Reference: [`references/script.md`](references/script.md)** ## AI patterns Summarize reads a document; Batch transform enriches a CSV into a new file. Signatures: `summarize({ attachment, prompt, returnCitations? })`; `batchTransform({ attachment, prompt, outputColumns, enableWebSearchGrounding? })`. ```ts .step('digest', summarize({ attachment: out('start', 'document'), prompt: 'Summarize the decisions and owners.', returnCitations: true })) ``` Request citations or web grounding only when the scenario needs them. **Summarize: [`references/summarize.md`](references/summarize.md)** **Batch transform: [`references/batch-transform.md`](references/batch-transform.md)** ## Document extraction Run a published Intelligent eXtraction Platform project on an attachment. Signature: `ixpExtract({ project, modelName, name, folderName, fileRef, pageRange?, versionTag?, folderPath? })`. ```ts .step('extract', ixpExtract({ project: ixpNodeType, modelName: 'invoice-model', name: 'Invoice Extractor', folderName: 'Shared', fileRef: out('start', 'invoiceFile') })) ``` Copy identity fields from a freshly pulled tenant registry; never construct them. Asking WHICH models exist ("what document extractors can I add?") is read-only Q&A, not a build: one `registry search 'uipath.ixp'` is the whole answer, and `registry get` is never fanned out across its hits. **Reference: [`references/ixp.md`](references/ixp.md)** ## Document classify and Dynamic Extract Classify a document (`uipath.document.classify`), or extract fields against an INLINE schema (`uipath.ixp.extract-document-builder`) instead of a published IxP project's trained fields. Signatures: `documentClassify({ fileRef, pageRange?, splitPages?, modelConfig? })`; `dynamicExtract({ fileRef, schema, model: { modelName, folderKey, ... }, pageRange? })`. ```ts .step('classify', documentClassify({ fileRef: input('file'), splitPages: true })) .step('extract', dynamicExtract({ fileRef: input('file'), schema: { type: 'object', properties: { total: { type: 'string' } } }, model: { modelName: 'invoiceixp-cef0d447-ixp', folderKey: '' } })) ``` Dynamic Extract still needs a model deployment identity — copy `modelName` and `folderKey` from the tenant; never construct them. **Reference: [`references/document-pipeline.md`](references/document-pipeline.md)** ## Delay Pause this path for a duration — or until an absolute date-time — then continue. Signature: `delay({ duration: string })` or `delay({ until: string })` (exactly one; `until` is an ISO-8601 date-time, e.g. `'2026-09-01T09:00:00Z'`). ```ts .step('cooldown', delay({ duration: 'PT30S' })) .step('embargo', delay({ until: '2026-09-01T09:00:00Z' })) .step('resumedAt', script({ code: 'return new Date().toISOString();' })) ``` Use a real-time rung when elapsed time itself is the requirement. **Reference: [`references/delay.md`](references/delay.md)** ## RPA workflow Run a deployed robotic process and wait for its job result. Signature: `rpaWorkflow({ key, name, folderPath, inputs?, returns? })`. ```ts .step('title', rpaWorkflow({ key: releaseKey, name: 'RPA Workflow', folderPath: 'Shared', inputs: { problemId: 123 }, returns: { title: 'string' } })) ``` Confirm identity and argument names against the same deployed tenant resource. **Reference: [`references/rpa-workflow.md`](references/rpa-workflow.md)** **Finding the key: [`references/or-processes.md`](references/or-processes.md)** ## API workflow Run a deployed coded API workflow and wait for its job result. Signature: `apiWorkflow({ key, name, folderPath, inputs?, returns? })`. ```ts .step('age', apiWorkflow({ key: workflowKey, name: 'NameToAge', folderPath: 'Shared', inputs: { name: input('name') }, returns: { age: 'integer' } })) ``` Confirm identity and exact argument casing on the tenant; `.onError(...)` is supported. **Reference: [`references/api-workflow.md`](references/api-workflow.md)** **Finding the key: [`references/or-processes.md`](references/or-processes.md)** ## Published function Run a deployed Orchestrator **Function** — a small unit of code published as its own resource — as one step. Signature: `publishedFunction({ key, name, folderPath, inputs?, returns? })`. ```ts .step('echo', publishedFunction({ key: functionKey, name: 'acme-echo', folderPath: 'Shared/acme-echo', inputs: { message: input('message') }, returns: { echoed: 'string' } })) ``` A function is usually deployed into a folder of its OWN name — read `folderPath` from the tenant rather than assuming `'Shared'`, since the binding's resourceKey is `.`. **Reference: [`references/published-function.md`](references/published-function.md)** ## Agentic process Run a deployed Maestro agentic process synchronously. Signature: `agenticProcess({ key, name, folderPath, inputs?, returns?, form?, completion? })`. ```ts .step('intake', agenticProcess({ key: processKey, name: 'ProcurementProcess', folderPath: 'Shared', inputs: { productId: 1 }, returns: { status: 'boolean' } })) ``` Confirm identity and argument names live; declared outputs may still be null; `.onError(...)` is supported. `form: 'bpmn' | 'flow' | 'case'` picks the published form; `completion: 'fire-and-forget'` waits for nothing — see the reference. **Reference: [`references/agentic-process.md`](references/agentic-process.md)** **Finding the key: [`references/or-processes.md`](references/or-processes.md)** ## Agent resource Start a published coded/low-code agent, or a sibling agent registered in this solution, and wait for its answer. Signature: `agent({ key, name, folderPath?, location?, projectId?, inputs, returns?, flavour? })`. ```ts .step('count', agent({ key: releaseKey, name: 'CountLetters', folderPath: 'Shared', inputs: { word: input('word') }, returns: { count: 'integer' }, flavour: 'coded' })) ``` This references rather than creates an agent; scaffold and register a task-created sibling before calling it. Verify resource identity and answer quality live. `.onError(...)` is supported. **Reference: [`references/agent.md`](references/agent.md)** ## Inline agent Define an autonomous agent inside this Flow project, with optional resources. Signature: `inlineAgent({ model, systemPrompt, userPrompt, inputs?, returns?, source?, context?, tools?, escalation?, guardrails?, mode?, ... })`. ```ts .step('triage', inlineAgent({ model: 'gpt-5.4', systemPrompt: 'Return a result conforming to the output schema. category: billing | technical | account.', userPrompt: 'Classify {{input.body}}', inputs: { body: input('body') }, returns: { category: 'string' }, guardrails: [{ id: 'no-pii', $guardrailType: 'custom', name: 'Block PII', selector: { scopes: ['Agent'] }, enabledForEvals: true, action: { $actionType: 'block', reason: 'PII detected' }, rules: [{ $ruleType: 'always', applyTo: 'inputAndOutput' }] }] })) ``` `tools` also takes `mcp`, `a2a`, `clientside`, `httpRequest` and `function` kinds; `memory: { name, id }` attaches an episodic memory; `escalation` takes `variant: 'quick-form'` for an inline form. `mode: 'advanced'` selects the Advanced harness. **Reference: [`references/inline-agent.md`](references/inline-agent.md)** — resource families: [`references/agent-resources.md`](references/agent-resources.md) ## Authoring a flow Choose each node from a registry search, never a brand name; ask when the request leaves a finite decision open; bind every declared output on every path (`check` misses a missing one, and the run returns `undefined`); handle an error only when the request says what should happen. The journeys, the scope gate for a request that names no steps, when to plan first, and the completion report are in **[`references/author.md`](references/author.md)**. ## Operating a deployed flow Upload, deploy, debug, trigger, inspect a job, and drive an instance's lifecycle. All of it needs `uip login`, and `uip solution resources refresh` comes before every upload, publish or debug. `flow debug` is a REAL run, not a validation step. Read **[`references/operate.md`](references/operate.md)**. ## Diagnosing a failed run Triage in order — the debug response you already have, then incidents, runtime variables, the deployed artifact, and traces last. Never re-run `flow debug` to look again. The builder removes several classic `.flow` defects and leaves others, including an expression that is really a literal. Read **[`references/diagnose.md`](references/diagnose.md)**. ## Evaluating a flow An inline agent does not create evaluators, eval sets, data points or simulations — the Flow eval CLI manages them as project files. Simulate every side-effecting component before running a set, and never `solution upload` as part of an eval workflow without asking. Read **[`references/evaluate.md`](references/evaluate.md)**. ## Queue item Create an Orchestrator queue item, optionally waiting for its consumer. Signature: `queueItem({ queue, folderPath, key, item, priority?, reference?, deferDate?, dueDate?, wait?, returns? })`. ```ts .step('enqueue', queueItem({ queue: 'Invoices', folderPath: 'Shared', key: queueKey, item: { InvoiceId: input('invoiceId') }, reference: input('invoiceId'), wait: false })) ``` Check tenant uniqueness/schema settings; wait only when a consumer exists and its result is needed. **Reference: [`references/queue.md`](references/queue.md)** ## Data Fabric **One product, two surfaces.** "Data Fabric" and "Data Service" are one product (key `uipath-uipath-dataservice` shows as *UiPath Data Fabric*). **CRUD is NATIVE** — `dataFabricRead` / `dataFabricCreate` / `dataFabricUpdate` / `dataFabricDelete`, no connection binding, no `registry prepare`. ```ts .step('open', dataFabricRead({ entity: 'Invoices', resultMode: 'multiple', filters: [{ field: 'Status', value: 'Open' }], limit: 200, sort: { field: 'CreateTime' } })) ``` `resultMode: 'multiple'` publishes matches under `output.results` and selects the read node's 1.4 definition; plain `dataFabricRead()` stays on 1.0 and reads one record. **Wrong here fails SILENTLY** — a bad column is dropped, not rejected, so `validate` passing is not evidence. Resolve columns with `uip df entities get` first. Delete publishes nothing; system columns (`Id`, `CreateTime`, `CreatedBy`, `UpdateTime`, `UpdatedBy`) are never writable; `fromRead` needs a single-record read; a folder-scoped entity needs `folderKey` AND `resourceKey`. **Still connector-only**: file record fields and Record Created/Updated events — `connector('uipath-uipath-dataservice', …)` + `registry prepare -f entityName=`. **Reference: [`references/data-fabric.md`](references/data-fabric.md)** ## Error handling Route the immediately preceding action's failure through a handler path. Signature: `.step(name, action).onError(handler => ... )`; the handler reads the failure with `h.err(field)` (or `err(step, field)`) and may `stepToRef(target)`. `.stepToList(port, fn)` runs a path from any port; `.stepToRef(port, target)` is a side exit that leaves the success path running. Never read the failed step's `out(...)` inside its own handler — that output was never written. ```ts .step('fetch', http({ url, managed: true })) .onError((h) => h.step('recover', script({ code: 'return "cached";' })) .stepToRef('useValue')) .step('useValue', script({ code: 'return "done";' })) ``` Choose deliberately between handling, rejoining, returning, terminating, and failing loud; test success and failure. **Reference: [`references/error-handling.md`](references/error-handling.md)** ## Terminate Stop the entire Flow run, including sibling parallel arms. Signature: `.terminate(name, label?)`. ```ts .branch('fatal', input('fatal'), (yes) => yes.terminate('abort', 'Abort run'), (no) => no.step('continue', script({ code: 'return "ok";' }))) ``` Use it only for stop-all intent; prove cancellation with an abort-specific witness. **Reference: [`references/terminate.md`](references/terminate.md)** ## Placeholder Mark where a real capability will be inserted later. Signature: `mock()`. ```ts .step('extractInvoice', mock()) .step('continueWithInput', script({ code: 'return $vars.assumedInvoiceId;' })) ``` Use a script for local fixed data, never in place of a capability the request needs; use a placeholder only to expose a capability gap the search proved. **Reference: [`references/placeholder.md`](references/placeholder.md)** ## Unknown node type Place a node this SDK has no factory for, carrying its definition verbatim. Signature: `rawNode({ nodeType, version, manifest, inputs?, outputs? })`. ```ts .step('exotic', rawNode({ nodeType: 'uipath.exotic.thing', version: '2.1', manifest: exoticManifest, // exactly what `registry get` returned inputs: { where: input('scope') } })) ``` `manifest` must be a real definition, copied from the registry — not one you wrote. Prefer a typed factory when one exists: it carries the family's checks, defaults and output contract. `decompile` emits this for a node type it cannot name, so an unknown node keeps its type and version through a round trip. **Never for a connector.** `check` and `compile` refuse a `uipath.connector.*` node type here: a raw node keeps its inputs verbatim, so the emitted node has no `inputs.detail` and no connection binding — `validate` only warns and the run never reaches Integration Service. When `compile` refuses a connector input as unknown, the answer is `uip maestro registry prepare ` (see [connector-params.md](references/connector-params.md#schema-dynamic-operations-the-parent-field-loop)), not `rawNode`. **Reference: [`references/placeholder.md`](references/placeholder.md#unknown-node-types)** ## Branch Split runtime control into true and false paths. Signature: `.branch(name, condition, thenFn, elseFn?)`. ```ts .branch('large', js`${input('amount')} > 1000`, (yes) => yes.step('review', script({ code: 'return "review";' })), (no) => no.step('approve', script({ code: 'return "approved";' }))) ``` Use branch for a two-way decision; decide whether arms return or ref back into shared work. **Reference: [`references/branch.md`](references/branch.md)** ## Switch Split runtime control among cases of one discriminant. Signature: `.switch(name, on, [{ value, label?, body }], defaultFn?)`. ```ts .switch('priority', input('priority'), [ { value: 'high', body: (b) => b.step('page', script({ code: 'return 1;' })) }, { value: 'low', body: (b) => b.step('queue', script({ code: 'return 2;' })) }, ]) ``` Prefer switch when one value selects three or more paths; use branch for two. **Reference: [`references/switch.md`](references/switch.md)** ## Parallel branches Fan out independent arms and join them at a Merge. Signature: `.parallel(name, [armFn, armFn, ...])`. ```ts .parallel('ready', [ (a) => a.step('weather', http({ url: weatherUrl, managed: false })), (b) => b.step('news', http({ url: newsUrl, managed: false })), ]) ``` Use it only for independent arms; do not assume the local executor runs them concurrently. **Reference: [`references/parallel-merge.md`](references/parallel-merge.md)** ## Subflow Run a child Flow authored in the same file as one parent step. Signature: `subflow(childFlow, { childInput: expression, ... })`. ```ts const child = flow('normalize').input({ raw: types.string }) .output({ clean: types.string }) .step('trim', script({ code: js`return ${input('raw')}.trim();`.js, returns: { clean: 'string' } })) .return({ clean: out('trim', 'clean') }).build(); export default flow('parent').input({ text: types.string }).output({ clean: types.string }) .step('normalized', subflow(child, { raw: input('text') })).return({ clean: out('normalized', 'clean') }).build(); ``` Use a child for a meaningful contract or reuse boundary, not arbitrary splitting or speed; children can be reused at any nesting depth. Read a child's inputs with `input(...)`: its start node is named `Start`, so a bare `$vars.raw` is wrong. **Reference: [`references/subflow.md`](references/subflow.md)** ## Human task Pause for a person: an inline form, quick form, deployed Action App, or a document-validation station. Signature: `hitl({ variant?, app?, document?, title?, priority?, labels?, recipient?, fields?, outcomes, outcomePorts?, exposeError? })`. ```ts .var('status', types.string) .stepSwitch('review', hitl({ title: 'Review invoice', recipient: { assignee: { type: 'user', value: 'reviewer@acme.test' } }, fields: [{ id: 'amount', type: 'number', direction: 'inOut', value: input('amount') }], outcomes: ['Approve', 'Reject'] }), [ { value: 'Approve', body: (b) => b.step('proceed', script({ code: 'return "approved";' }), { updates: { status: lit('approved') } }) }, { value: 'Reject', body: (b) => b.step('notify', script({ code: 'return "rejected";' }), { updates: { status: lit('rejected') } }) }]) .return({ status: v('status') }) ``` Every outcome is its own exit (`outcome-`); the SDK never emits `completed` on a task that has outcomes, and every outcome must be wired. `.stepSwitch` gives each one an arm — no tacit exit, arms converge like `.switch`'s, a missing arm warns — on the default node, on quick-form, or on a pinned task. After a default-node task with more than one outcome, `.step` + `.stepToList` is the older shape where the FIRST outcome continues the main path. After quick-form, `outcomePorts: false` or `{ version: '1.0' }`, a plain `.step()` continues EVERY outcome to the next step, each on its own edge, where you route on `out('review', 'Action')`. **Reference: [`references/hitl.md`](references/hitl.md)** ## Conversational Work a live CHAT: wait for the person's message, answer it, post a reply. Every step is keyed by a `conversationId` — the conversation trigger publishes it. Signatures: `.trigger(conversationTrigger())`; `waitForMessage({ conversationId, numExchanges? })`; `conversationalAgent({ model, systemPrompt, settings })`; `sendMessage({ conversationId, exchangeId, content, endExchange? })`; `conversationContext({ conversationId, exchangeLimit? })`. ```ts .trigger(conversationTrigger()) .step('listen', waitForMessage({ conversationId: out('start', 'conversationId') })) .step('reply', conversationalAgent({ model: 'gpt-5.4', systemPrompt: 'Answer briefly.', settings: { context: out('listen', 'conversationContext') } })) ``` `waitForMessage` SUSPENDS the flow (a catch event), it does not poll. Use `sendMessage` when the flow decides what to say, an agent when the model does. **Reference: [`references/conversational.md`](references/conversational.md)** ## Voice Talk to someone on a phone call. The call is identified by a `callContext` OBJECT — pass the whole thing, never a field inside it. Signatures: `.trigger(voiceTrigger())`; `createOutgoingCall({ from, to })`; `endCall({ callContext })`; `voiceAgent({ systemPrompt, inputs?, callContext, voice?, maxIterations? })`. ```ts .step('dial', createOutgoingCall({ from: '+15550001111', to: input('phone') })) .step('talk', voiceAgent({ systemPrompt: 'Confirm {{input.customerName}}\'s delivery window.', inputs: { customerName: input('customerName') }, callContext: out('dial', 'callContext'), voice: { model: 'gemini-3.1-flash-live-preview', persona: 'Kore' } })) .step('bye', endCall({ callContext: out('dial', 'callContext') })) ``` The incoming-call trigger publishes `out('start', 'callContext')`. A persona belongs to its voice model; `maxIterations` is capped at 8. **Reference: [`references/voice.md`](references/voice.md)** ## Transform Filter, map, group, or chain operations over an array without custom JavaScript. Signature: `transform({ collection, operations, variant?: 'filter' | 'map' | 'group-by' })`. ```ts .step('active', transform({ variant: 'filter', collection: input('rows'), operations: [{ type: 'filter', filters: [ { field: 'status', condition: 'equals', value: 'active' }, ] }] })) ``` Prefer a named variant for one operation and generic Transform for a chain; verify chain order against real fields. **Reference: [`references/transform.md`](references/transform.md)** ## Integration Service connectors Call a curated or generic connector operation using a generated descriptor or key/action pair. Signatures: `connector(descriptor, inputs, opts?)`; `connector(key, action, inputs?, { connection, folder, object?, version? })` (`connection` and `folder` are required: `BINDING_MISSING`). ```ts .step('issue', connector('uipath-atlassian-jira', 'get-issue', { issueId: input('issueId'), project: 'IN', issuetype: 'Task' }, { connection: 'jira', folder: 'shared' })) ``` Data Fabric is also connector key `uipath-uipath-dataservice`: use it for file record fields, Record Created/Updated events, or a scenario that names the connector. Record CRUD is native ([Data Fabric](#data-fabric)). Discover tenant-specific fields and ids; preserve every scenario-named input. **Reference: [`references/connector-params.md`](references/connector-params.md)** **Bindings: [`references/bindings.md`](references/bindings.md)** ## Loops Run a body once for each value in a collection. Signature: `.loop(name, collection, bodyFn, options?)`. ```ts .loop('eachOrder', input('orders'), (body) => body .step('handle', script({ code: 'return { id: $vars.eachOrder.currentItem.id };' }))) ``` Per-iteration flow-variable writes go through `{ updates }` on a body step. Every `.loop()` emits `core.logic.loop` 2.4: the body reads `$vars..currentItem` and `$vars..currentIteration` (not the legacy 1.0.0 `currentIndex`, which `check` refuses). Options: `parallel: true`, `completionCondition` (checked after each iteration, stops early), and `body.break()` exits the whole loop from inside an arm. See the reference for the option details and examples. **Reference: [`references/loops.md`](references/loops.md)** ## Do while Run a body, then repeat **while a condition is true** — checked AFTER each pass, so the body always runs at least once (`core.logic.dowhile`). The container publishes no data output: write results to a `.var()` from inside the body with `{ updates }`. `limit` caps iterations (1–10,000; blank means the platform default of 10,000), and `body.break()` works exactly as in `.loop()`. Signature: `.doWhile(name, condition, bodyFn, { limit?, breakEnabled? })`. ```ts .var('page', types.number, 1) .doWhile('paginate', js`$vars.fetch.output.body.hasNextPage === true`, (body) => body .step('fetch', http({ url: tmpl`https://api.example.test/items?page=${v('page')}`, method: 'GET', managed: false, returns: { hasNextPage: 'boolean' } }), { updates: { page: js`$vars.page + 1` } }), { limit: 50 }) ``` ## Return and end End the current path and bind declared Flow outputs. Signature: `.return({ outputName: expression, ... })`. ```ts .branch('valid', out('check'), (yes) => yes.return({ status: 'accepted' }), (no) => no.return({ status: 'rejected' })) ``` Choose between arm-local returns and a shared continuation based on the graph the scenario needs. **Reference: [`references/return.md`](references/return.md)** ## Final evidence The final pass must use the loop appropriate to the packaging mode and run after the last edit. Product-resource truth is live evidence: confirm plausible ids, argument names, scenario-named optional inputs, and warnings against the tenant. Static diagnostics own all mechanically checkable structure; fix their cause rather than copying rules back into this router. Match proof to the request's acceptance bar. If one wiring question remains, a validate-only bar is complete when product validation is green and its required structural self-check passes; do not add debug only for confidence. For each behavior claim the bar names, plan at most one bounded product debug that answers it. If one wiring question remains, run one bounded experiment that distinguishes it, apply the answer, and stop; do not grow a family of scratch solutions or repeat equivalent variants.