UiPath Flow — TypeScript Builder SDK
UiPath Flow orchestrations can be authored in TypeScript using the @uipath/flow-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
The workspace installs @uipath/flow-sdk in node_modules/; examples/ contains authored examples, and references/ contains the details routed from this guide.
To author a Flow, create a root-level <Name>.flow.ts and import the package directly.
The source lives at the root; the compiled artifact does not. Scaffold the project
before authoring, then emit into it — compile -o is the authority over where the
emitted file is written. <Solution> and <Name> 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 <Name> for both.
uip solution init <Solution>
( cd <Solution> && uip maestro flow init <Name> )
uip maestro flow compile <Name>.flow.ts -o <Solution>/<Name>/<Name>.flow
Exactly one emitted <Name>.flow may exist, at that path. Never leave a second copy
at the workspace root, and never leave behind the trigger-only stub flow init writes —
validators and evidence collectors cannot choose safely between duplicates, and an
abandoned stub outranks the real work. 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) and do not mix the two.
Integrations with non-UiPath systems are handled through connectors.
Connectors require a root-level bindings.json.
uip maestro registry pull writes a descriptor per referenced connector to connectors/<key>.ts, and caches the library itself outside the project.
Prepared connector modules live at connectors-local/<key>.ts; their descriptor data is kept separately below connectors-local/descriptors/<key>/.
Schema-dynamic connector gate
If the request mentions loadByDefault, dependent dropdowns, preselected
reference values, customFieldsRequestDetails, or other connection-specific
fields, the static library descriptor is not sufficient. Before authoring the
connector call, resolve the real parent values, run
npx flow-sdk registry prepare <connector-key> <action> with every required
-f NAME=VALUE, and import the generated connectors-local/<key>.ts descriptor.
It finds the connection itself and writes bindings.json.
Do not substitute manual resources run list lookups plus a static
connectors/<key>.ts import: the lookups choose values but do not create the
design-time schema-replay 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
import { flow, script, input, out, types } from '@uipath/flow-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/flow-sdk owns
their semantics. A workspace with { "flowSdk": { "emitOnly": true } } in
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.
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 in an external staging directory, decompile writes
<Name>.pipeline.mjs, which runs the loop in two invocations and gets the
baseline ordering right:
uip maestro flow decompile <Name>.flow -o <Name>.flow.ts
node <Name>.pipeline.mjs # captures the pristine baseline
# edit <Name>.flow.ts narrowly
node <Name>.pipeline.mjs # compiles the edit and merges it back
uip maestro flow validate <Name>.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.
Builder frame
Start with flow(id), declare .input({ name: types.* }),
.output({ name: types.* }), and .var(name, types.*, default?); add graph
nodes; use .return(...) when a path should answer and .terminate(...) when
the whole run should stop; call .build().
Expressions: lit(value), input(name), v(name), out(step, path?),
err(step, field?), ran(step), js`...` , and tmpl`...` . A shared
continuation is often clearer than duplicating work in several arms; use
ran(step) when its value legitimately comes from only one arm.
Exact function signatures and option shapes:
references/api.md — the builders too (FlowBuilder,
StepList, ArmBuilder). The sibling authoring surfaces have their own skills:
uipath-maestro-case for @uipath/flow-sdk/case and uipath-maestro-bpmn
for @uipath/flow-sdk/bpmn. Neither is needed to build a Flow.
Those pages are compact — signature, summary, one line per field — because they are read under a token budget. The unabridged declarations they are generated from ship in the installed package and are the authority when a signature names a type whose members or rules you need:
grep -rln "declare function err" node_modules/@uipath/flow-sdk/dist --include="*.d.ts"
# -> node_modules/@uipath/flow-sdk/dist/core/expr.d.ts (full @param prose, all five field values)
Grep 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.
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.md | examples/GreenhouseWatering.flow.ts |
| Scheduled trigger | core.trigger.scheduled |
scheduled(...) |
Scheduled trigger | scheduled-trigger.md | examples/HerbariumDispatch.flow.ts |
| Connector event trigger | uipath.connector.trigger.<key>.<event> |
onEvent(...) |
Connector events | event-trigger.md | examples/DoorbellLog.flow.ts |
| Connector event wait | uipath.connector.event.<key>.<event> |
waitForEvent(...) |
Connector events | event-trigger.md | examples/PlanetariumConfirmation.flow.ts |
| Form trigger | core.trigger.form |
formTrigger(...) |
Form trigger | form-trigger.md | examples/BakeOffEntryForm.flow.ts |
| Conversation trigger | core.trigger.conversation |
conversationTrigger(...) |
Conversational | conversational.md | examples/LibraryDeskChat.flow.ts |
| Voice trigger | core.trigger.voice |
voiceTrigger(...) |
Voice | voice.md | examples/HarbourRadioLine.flow.ts |
| Standalone HTTP | core.action.http |
http({ managed: false, ... }) |
HTTP | http.md | examples/LighthouseSignal.flow.ts |
| Managed HTTP | core.action.http.v2 |
http({ managed: true, ... }) |
HTTP | http.md | examples/ObservatorySeeing.flow.ts |
| Script | core.action.script |
script(...) |
Script | script.md | examples/GreenhouseWatering.flow.ts |
| Transform | core.action.transform |
transform(...) |
Transform | transform.md | examples/TrailLogSummary.flow.ts |
| Filter | core.action.transform.filter |
transform({ variant: 'filter', ... }) |
Transform | transform.md | examples/TrailLogSummary.flow.ts |
| Map | core.action.transform.map |
transform({ variant: 'map', ... }) |
Transform | transform.md | examples/TrailLogSummary.flow.ts |
| Group by | core.action.transform.group-by |
transform({ variant: 'group-by', ... }) |
Transform | transform.md | examples/TrailLogSummary.flow.ts |
| Integration Service action | uipath.connector.<key>.<action> (Data Fabric / Data Service entity ops — create, delete, get-by-id, query-many: uipath.connector.uipath-uipath-dataservice.*) |
connector(...) |
Integration Service connectors | connector-params.md | examples/ClubDirectory.flow.ts |
| Data Fabric read | core.datafabric.read |
dataFabricRead(...) |
Data Fabric | data-fabric.md | examples/BeeHiveLedger.flow.ts |
| Data Fabric update | core.datafabric.update |
dataFabricUpdate(...) |
Data Fabric | data-fabric.md | examples/BeeHiveLedger.flow.ts |
| Subflow | core.subflow |
subflow(...) |
Subflow | subflow.md | examples/RecipeScaler.flow.ts |
| Human task | uipath.human-in-the-loop |
hitl(...) |
Human task | hitl.md | examples/GallerySubmission.flow.ts |
| Human quick form | uipath.human-in-the-loop.quick-form |
hitl({ variant: 'quick-form', ... }) |
Human task | hitl.md | examples/FieldTripQuickForm.flow.ts |
| Human action app | uipath.human-in-the-loop.coded-action-app |
hitl({ variant: 'action-app', ... }) |
Human task | hitl.md | examples/KilnReview.flow.ts |
| RPA workflow | uipath.core.rpa-workflow.<key> |
rpaWorkflow(...) |
RPA workflow | rpa-workflow.md | examples/WorkshopInventory.flow.ts |
| Queue item | core.action.queue.create* |
queueItem(...) |
Queue item | queue.md | examples/HerbariumDispatch.flow.ts |
| Summarize | uipath.pattern.deep-rag |
summarize(...) |
AI patterns | summarize.md | examples/OralHistoryDigest.flow.ts |
| Batch transform | uipath.pattern.batch-transform |
batchTransform(...) |
AI patterns | batch-transform.md | examples/FossilCatalogEnrich.flow.ts |
| Branch | core.logic.decision |
.branch(...) |
Branch | branch.md | examples/GreenhouseWatering.flow.ts |
| Switch | core.logic.switch |
.switch(...) |
Switch | switch.md | examples/BeltProgression.flow.ts |
| Parallel / Merge | core.logic.merge |
.parallel(...) |
Parallel branches | parallel-merge.md | examples/ConcertSoundcheck.flow.ts |
| Loop | core.logic.loop |
.loop(...) |
Loops | loops.md | examples/ClubDirectory.flow.ts |
| Do while | core.logic.dowhile |
.doWhile(...) |
Do while | loops.md | examples/MeteorShowerPages.flow.ts |
| Return / End | core.control.end |
.return(...) |
Return and end | return.md | examples/GreenhouseWatering.flow.ts |
| Terminate | core.logic.terminate |
.terminate(...) |
Terminate | terminate.md | examples/AquariumSafetyStop.flow.ts |
| Placeholder | core.logic.mock |
mock() |
Placeholder | placeholder.md | examples/FestivalMapScaffold.flow.ts |
| Error handler | error handle on an action node |
.onError(...) |
Error handling | error-handling.md | examples/ObservatorySeeing.flow.ts |
| Delay | core.logic.delay |
delay(...) |
Delay | delay.md | examples/LighthouseSignal.flow.ts |
| API workflow | uipath.core.api-workflow.<key> |
apiWorkflow(...) |
API workflow | api-workflow.md | examples/BirdCountLookup.flow.ts |
| Agentic process | uipath.core.agentic-process.<key> |
agenticProcess(...) |
Agentic process | agentic-process.md | examples/NeighborhoodWalkPlanner.flow.ts |
| Agent resource | uipath.core.agent.<key> |
agent(...) |
Agent resource | agent.md | examples/PlantNameAdvisor.flow.ts |
| Inline agent | uipath.agent.autonomous |
inlineAgent(...) |
Inline agent | inline-agent.md | examples/PostcardCaption.flow.ts |
| IxP extraction | uipath.ixp.<project>.<version>-<folder> |
ixpExtract(...) |
Document extraction | ixp.md | examples/ArchiveCardExtract.flow.ts |
| Document classify | uipath.document.classify |
documentClassify(...) |
Document classify and Dynamic Extract | document-pipeline.md | examples/SeedPacketReader.flow.ts |
| Dynamic extract | uipath.ixp.extract-document-builder |
dynamicExtract(...) |
Document classify and Dynamic Extract | document-pipeline.md | examples/SeedPacketReader.flow.ts |
| Published function | uipath.core.function.<key> |
publishedFunction(...) |
Published function | published-function.md | examples/TideTableConverter.flow.ts |
| Conversation message wait | uipath.conversational.wait-for-message |
waitForMessage(...) |
Conversational | conversational.md | examples/LibraryDeskChat.flow.ts |
| Conversational agent | uipath.agent.conversational |
conversationalAgent(...) |
Conversational | conversational.md | examples/LibraryDeskChat.flow.ts |
| Conversation send message | uipath.conversational.send-message |
sendMessage(...) |
Conversational | conversational.md | examples/LibraryDeskChat.flow.ts |
| Voice outgoing call | uipath.conversational.voice.create-outgoing-call |
createOutgoingCall(...) |
Voice | voice.md | examples/PotteryStudioCallback.flow.ts |
| Voice agent | uipath.agent.voice |
voiceAgent(...) |
Voice | voice.md | examples/PotteryStudioCallback.flow.ts |
| Voice end call | uipath.conversational.voice.end-call |
endCall(...) |
Voice | 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().
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
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.
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
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).
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
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('<id>', '<name>')), 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.
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)).
const mail = { connector: 'uipath-microsoft-outlook365',
event: 'email-received', where: { parentFolderId: inboxId } };
export default flow('mail').trigger(onEvent(mail))
.step('reply', script({ code: 'return $vars.start.output.subject;' })).build();
Resolve scope names and ids from the bound connection; preserve filter casing.
A generic event (record-created/record-updated) needs object: '<Entity>' — never put it in where.
Use the reference's completion contract before debugging: an injected start
payload can exercise downstream wiring, but it is not a subscription witness.
Reference: 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? }).
.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 bindings.json.
A branch-<name> side exit uses .stepToList; omit both bindings for manual/implicit mode.
Reference: 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?).
.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
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? }).
.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
Batch transform: 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? }).
.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.
Reference: 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? }).
.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: '<folder-guid>' } }))
Dynamic Extract still needs a model deployment identity — copy modelName and
folderKey from the tenant; never construct them.
Reference: 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').
.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
RPA workflow
Run a deployed robotic process and wait for its job result.
Signature: rpaWorkflow({ key, name, folderPath, inputs?, returns? }).
.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
Finding the key: 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? }).
.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
Finding the key: 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? }).
.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 <folderPath>.<name>.
Reference: references/published-function.md
Agentic process
Run a deployed Maestro agentic process synchronously.
Signature: agenticProcess({ key, name, folderPath, inputs?, returns?, form?, completion? }).
.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
Finding the key: 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? }).
.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
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?, ... }).
.step('triage', inlineAgent({ model: 'gpt-5.4', systemPrompt: 'Return JSON with category.',
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 — resource families: references/agent-resources.md
Evaluation assets
An inline agent does not create evaluators, eval sets, or data points. Manage
those project files with the Flow eval CLI; read
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? }).
.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
Data Fabric
Route on the VERB, not the name in the prompt. "Data Fabric" and "Data
Service" are one product (key uipath-uipath-dataservice shows as UiPath Data Fabric).
core.datafabric.* has two verbs and no output schema: dataFabricRead({ entity, filters? })
and dataFabricUpdate({ entity, record, set }) (record is one of { byId } /
{ fromRead: '<read step>' }). Create, delete, get-by-id, query-many with a row
limit, file fields, declared outputs and events are
connector('uipath-uipath-dataservice', …) even when the scenario says "Data
Fabric" — registry prepare … -f entityName=<Entity> first.
.step('lookup', dataFabricRead({ entity: 'Invoices',
filters: [{ field: 'InvoiceId', value: input('invoiceId') }] }))
.step('markPaid', dataFabricUpdate({ entity: 'Invoices',
record: { fromRead: 'lookup' }, set: { Status: 'Paid' } }))
Filters default to =; or: true joins with OR. Reference: references/data-fabric.md
Error handling
Route the immediately preceding action's failure through a handler path.
Signature: .step(name, action).onError(handler => ... ); handler may use err(step, field) and stepToRef(target). .stepToList(port, fn) runs a path from any port; .stepToRef(port, target) is a side exit that leaves the success path running.
.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
Terminate
Stop the entire Flow run, including sibling parallel arms.
Signature: .terminate(name, label?).
.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
Placeholder
Mark where a real capability will be inserted later.
Signature: mock().
.step('extractInvoice', mock())
.step('continueWithInput', script({
code: 'return $vars.assumedInvoiceId;' }))
Use a script for stand-in data; use a placeholder only to expose a capability gap.
Reference: 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? }).
.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 npx flow-sdk registry prepare <key> <action> (see
connector-params.md),
not rawNode.
Reference: references/placeholder.md
Branch
Split runtime control into true and false paths.
Signature: .branch(name, condition, thenFn, elseFn?).
.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
Switch
Split runtime control among cases of one discriminant.
Signature: .switch(name, on, [{ value, label?, body }], defaultFn?).
.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
Parallel branches
Fan out independent arms and join them at a Merge.
Signature: .parallel(name, [armFn, armFn, ...]).
.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
Subflow
Run a child Flow authored in the same file as one parent step.
Signature: subflow(childFlow, { childInput: expression, ... }).
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 <callerStepId>Start, so a bare $vars.raw is wrong.
Reference: 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? }).
.step('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'], outcomePorts: true }))
.stepToList('outcome-reject', (b) => b.return({ status: 'rejected' }))
.step('proceed', script({ code: 'return "approved";' }))
outcomePorts routes per outcome (outcome-<slug> exits; the FIRST continues the main path); without it, route on out('review', 'Action').
Reference: 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? }).
.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
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? }).
.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
Transform
Filter, map, group, or chain operations over an array without custom JavaScript.
Signature: transform({ collection, operations, variant?: 'filter' | 'map' | 'group-by' }).
.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
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? }).
.step('issue', connector('uipath-atlassian-jira', 'get-issue',
{ issueId: input('issueId'), project: 'IN', issuetype: 'Task' },
{ connection: 'jira', folder: 'shared' }))
Data
…(truncated)