@uipath/maestro-builder-sdk 6.21.2 → 6.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,141 +10,6 @@
10
10
  import type { Expr } from './expr.js';
11
11
  import type { LookupToken } from './lookups.js';
12
12
  import { type ConnectorDescriptor, type TriggerDescriptor } from './connectors.js';
13
- /** One conditional response branch on an HTTP node — a designer-visible side exit. */
14
- export interface HttpBranch {
15
- /**
16
- * The branch's name — it becomes the node's `branch-<name>` source port, so
17
- * keep it port-safe (letters, digits, `_`, `-`). Route it with
18
- * `.stepToList('branch-<name>', ...)` after the step; the main path continues
19
- * from the default port as always.
20
- */
21
- name: string;
22
- /**
23
- * When to take this exit, evaluated against the response — e.g.
24
- * js-tagged: `$vars.fetch.output.statusCode === 429`. Branches are checked in
25
- * order; the first truthy one wins, else the default port fires.
26
- */
27
- condition: Expr;
28
- }
29
- interface HttpInputsBase {
30
- /**
31
- * Conditional response branches — extra source ports (`branch-<name>`)
32
- * beside the default. See {@link HttpBranch}; both HTTP node families
33
- * declare them.
34
- */
35
- branches?: HttpBranch[];
36
- method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
37
- url: string | Expr;
38
- headers?: Record<string, string | Expr>;
39
- query?: Record<string, string | Expr>;
40
- body?: string | Expr;
41
- contentType?: string;
42
- /**
43
- * Choose the HTTP node explicitly. `false` emits the standalone
44
- * `core.action.http`; `true` emits the managed `core.action.http.v2`, routed
45
- * through Integration Service's HTTP connector.
46
- *
47
- * This has no default because these are different platform nodes, not two
48
- * modes of one node. A silent `false` made an omitted field compile and
49
- * validate as the wrong node while changing failure routing and connection
50
- * behavior.
51
- *
52
- * It is not a version bump, it changes two things you can see:
53
- *
54
- * - **a 4xx/5xx FAILS the step**, so `.onError(…)` fires on it. On the plain
55
- * node the same 404 comes back on the SUCCESS path as
56
- * `out('fetch','statusCode')` and no error handler ever runs — which is why
57
- * an error port only makes sense here (`check` says so: HTTP_ONERROR_V1).
58
- * The handler can answer the flow (`.return`), stop the run
59
- * (`.terminate`), or compensate and continue at a step the success path
60
- * also reaches (`h.stepToRef('<step>')` — see `references/error-handling.md`);
61
- * - both nodes expose JSON responses as parsed values in the live product;
62
- * use `returns` to declare fields before reading them. The node choice does
63
- * not decide whether application/json is parsed.
64
- *
65
- * With `managed: true`, omit `connection` and `folder` for the platform's
66
- * manual/implicit mode, or provide both symbolic names to reuse an Integration
67
- * Service HTTP connection. A local run remains unauthenticated.
68
- *
69
- * @enforcedBy HTTP_ONERROR_V1 `.onError()` on an http step needs `managed: true`; on
70
- * the standalone node a 4xx arrives on the SUCCESS path and no handler runs.
71
- */
72
- managed: boolean;
73
- /**
74
- * The response BODY's top-level fields and their types — e.g.
75
- * `{ items: 'array' }`. Required for either HTTP node if anything reads a
76
- * field off a parsed JSON body.
77
- *
78
- * Why you have to say: the platform's own definition declares the response as
79
- * a bare object, because only the API being called knows its shape. Without
80
- * this, `out('fetch','body','items')` reads from an object with no declared
81
- * fields, and nothing downstream can tell a real field from a typo. Same rule,
82
- * and the same word, as
83
- * `rpaWorkflow`'s `returns`.
84
- *
85
- * **Where a declared field is read:** under `body`, never beside it. The
86
- * response is an envelope — `{ body, statusCode, headers }` — and these are the
87
- * body's fields, so `returns: { items: 'array' }` is read as
88
- * `out('fetch', 'body.items')`, which is `$vars.fetch.output.body.items`.
89
- * `$vars.fetch.output.items` is undefined at run time.
90
- *
91
- * @enforcedBy HTTP_READ_UNKNOWN_FIELD A read must be `output.body.<declared field>`,
92
- * `output.statusCode` or `output.headers`.
93
- * @enforcedBy HTTP_READ_WITHOUT_RETURNS Reading a body field requires declaring it here.
94
- */
95
- returns?: Record<string, ReturnFieldType>;
96
- /**
97
- * How long to wait for the response before giving up, as an **ISO-8601
98
- * duration** — `'PT30S'` is "give up after 30 seconds", `'PT1M'` one minute,
99
- * `'PT15M'` fifteen.
100
- *
101
- * Both http node types carry this, and both definitions declare the same
102
- * default (`PT15M`), so leaving it unset is not "no timeout" — it is fifteen
103
- * minutes. Omitted from the emitted node unless you set it, which keeps a flow
104
- * that doesn't care byte-identical to the platform's own default shape (~99%
105
- * of the 808 deployed http nodes are on the defaults).
106
- *
107
- * `check` rejects a string the platform cannot parse as a duration —
108
- * `'30s'`, `'PT'` and `'30'` are all errors that name the expected form.
109
- *
110
- * @defaultValue `'PT15M'`
111
- * @enforcedBy HTTP_TIMEOUT_NOT_DURATION Must be an ISO-8601 duration.
112
- */
113
- timeout?: string;
114
- /**
115
- * How many times to RETRY a retryable failure (5xx, 429, a network error) —
116
- * `0`, the definitions' default, means "try once, don't retry". `2` means up
117
- * to three attempts in total.
118
- *
119
- * Must be a non-negative integer. Above **5** `check` warns rather than
120
- * errors: the deployed corpus's
121
- * largest author-set value is 3, and the platform's own ceiling is not
122
- * measured — so a bigger number may well work, and refusing it outright would
123
- * fence off something we have no evidence is wrong.
124
- *
125
- * @defaultValue `0`
126
- * @enforcedBy HTTP_RETRY_COUNT_INVALID Must be a non-negative integer.
127
- */
128
- retryCount?: number;
129
- }
130
- export type HttpInputs = HttpInputsBase & ({
131
- managed: false;
132
- connection?: never;
133
- folder?: never;
134
- } | {
135
- managed: true;
136
- /** Symbolic HTTP connection name declared in bindings.json. */
137
- connection?: string;
138
- /** Symbolic folder name declared in bindings.json. */
139
- folder?: string;
140
- /**
141
- * Connector key whose authentication the HTTP proxy should reuse.
142
- * Defaults to `uipath-uipath-http`. Set this for a connector-specific
143
- * fallback endpoint, for example `uipath-salesforce-slack`.
144
- * Requires both `connection` and `folder`.
145
- */
146
- targetConnector?: string;
147
- });
148
13
  export interface ConnectorOpts {
149
14
  /** Connector action version (defaults to the library's newest match). */
150
15
  version?: string;
@@ -1928,10 +1793,6 @@ export interface ContextIndexRef {
1928
1793
  * whole body rather than an inputs payload.
1929
1794
  */
1930
1795
  export interface FlowActionSpecs {
1931
- /** The spec {@link http} builds. */
1932
- http: {
1933
- inputs: HttpInputs;
1934
- };
1935
1796
  /** The spec {@link hitl} builds. */
1936
1797
  hitl: {
1937
1798
  inputs: HitlInputs;
@@ -2059,14 +1920,6 @@ export declare function builtinActionSpec<S extends object>(spec: S): S;
2059
1920
  * @returns The same spec, as a `FlowTrigger` instance.
2060
1921
  */
2061
1922
  export declare function builtinTriggerSpec<S extends object>(spec: S): S;
2062
- /**
2063
- * Declare an HTTP request action, explicitly choosing standalone or managed.
2064
- *
2065
- * @param inputs - The request: `url`, `method`, `body`, the response fields you
2066
- * read back via `returns`, and the `managed` node choice.
2067
- * @returns An action spec to pass to `.step(name, …)`.
2068
- */
2069
- export declare function http(inputs: HttpInputs): ActionSpec;
2070
1923
  /**
2071
1924
  * Declare a human task — a step that pauses for a person to read some values,
2072
1925
  * fill others in, and press one of the outcome buttons.
@@ -88,21 +88,6 @@ export function builtinActionSpec(spec) {
88
88
  export function builtinTriggerSpec(spec) {
89
89
  return new BuiltinTrigger(spec);
90
90
  }
91
- /**
92
- * Declare an HTTP request action, explicitly choosing standalone or managed.
93
- *
94
- * @param inputs - The request: `url`, `method`, `body`, the response fields you
95
- * read back via `returns`, and the `managed` node choice.
96
- * @returns An action spec to pass to `.step(name, …)`.
97
- */
98
- export function http(inputs) {
99
- if (typeof inputs.managed !== 'boolean') {
100
- throw new Error('http() requires managed: true or managed: false because they emit different platform nodes. '
101
- + 'Use true for Managed HTTP Request/core.action.http.v2 or when non-2xx statuses must reach .onError(); '
102
- + 'use false for the standalone core.action.http node.');
103
- }
104
- return builtinActionSpec({ kind: 'http', inputs });
105
- }
106
91
  /**
107
92
  * Declare a human task — a step that pauses for a person to read some values,
108
93
  * fill others in, and press one of the outcome buttons.
@@ -196,7 +196,7 @@ export type ErrorEnvelopeField = 'code' | 'message' | 'detail' | 'category' | 's
196
196
  *
197
197
  * So the author writes one shape, `.error.<field>`, and `serialize` rewrites it
198
198
  * to `.output.<field>` for the one family that publishes it there
199
- * (`ERROR_ENVELOPE_VAR` in `serialize.ts`). Do not hand-write either prefix: which
199
+ * (a node type's `errorEnvelope`). Do not hand-write either prefix: which
200
200
  * one is right is not a property of the expression, it is a property of the node
201
201
  * the step happens to be, and only the builder knows that.
202
202
  *
package/dist/core/expr.js CHANGED
@@ -209,7 +209,7 @@ export function ran(step) {
209
209
  *
210
210
  * So the author writes one shape, `.error.<field>`, and `serialize` rewrites it
211
211
  * to `.output.<field>` for the one family that publishes it there
212
- * (`ERROR_ENVELOPE_VAR` in `serialize.ts`). Do not hand-write either prefix: which
212
+ * (a node type's `errorEnvelope`). Do not hand-write either prefix: which
213
213
  * one is right is not a property of the expression, it is a property of the node
214
214
  * the step happens to be, and only the builder knows that.
215
215
  *
@@ -258,7 +258,7 @@ export function ran(step) {
258
258
  */
259
259
  export function err(step, field) {
260
260
  // ONE shape for every family. `serialize` rewrites the prefix for the families
261
- // that publish the envelope elsewhere — see `ERROR_ENVELOPE_VAR`.
261
+ // that publish the envelope elsewhere — see `FlowNodeType.errorEnvelope`.
262
262
  return new Expr(`$vars.${step}.error${field ? '.' + field : ''}`);
263
263
  }
264
264
  /**
@@ -45,6 +45,13 @@ export interface OutputRead {
45
45
  /** The step whose expression contains the read. */
46
46
  reader: string;
47
47
  }
48
+ /** A read of a node's output, split where a `reader` contract checks it. */
49
+ export interface OutputPathRead extends OutputRead {
50
+ /** The first segment after the step (`output`, `error`, …); undefined for a read of the step itself. */
51
+ field: string | undefined;
52
+ /** The path after `field`, as the reference spells it (with its leading `.`); `''` when there is none. */
53
+ rest: string;
54
+ }
48
55
  /**
49
56
  * How the artifact words a read of a `fields` output: the code prefix its read
50
57
  * diagnostics share (`<PREFIX>_READ_NOT_OUTPUT`, `_READ_WITHOUT_RETURNS`,
@@ -65,6 +72,11 @@ export interface FieldsRead {
65
72
  * states the fields a read may name — `undefined` when the author declared none,
66
73
  * which makes any field read its own mistake — and `read` keeps the wording of
67
74
  * the read rule it replaces, for the same reason. `open` admits any field.
75
+ *
76
+ * `reader` is for an output whose shape no field list states: an HTTP response is
77
+ * an envelope (`body`, `statusCode`, `headers`) whose `body` holds the author's
78
+ * `returns`. The type checks each read itself and reports through its family's
79
+ * catalog, with the codes it had before the port.
68
80
  */
69
81
  export type OutputContract = {
70
82
  readonly kind: 'none';
@@ -75,6 +87,9 @@ export type OutputContract = {
75
87
  readonly fields: Readonly<Record<string, TypeDesc>> | undefined;
76
88
  readonly open?: boolean;
77
89
  readonly read: FieldsRead;
90
+ } | {
91
+ readonly kind: 'reader';
92
+ read(read: OutputPathRead, report: (code: string, message: string, suggestion?: string) => void): void;
78
93
  } | {
79
94
  readonly kind: 'opaque';
80
95
  };
@@ -1,14 +1,15 @@
1
1
  /**
2
- * The exits an action declares that the AUTHOR named — a human task's outcomes,
3
- * an HTTP call's response branches — and the port each one leaves on.
2
+ * The exits an action declares that the AUTHOR named — a human task's outcomes —
3
+ * and the port each one leaves on. A registered node type declares its own
4
+ * (`FlowNodeType.exits`: an HTTP call's response branches); `stepExits` in
5
+ * flow-step-exits.ts asks it first and falls back here.
4
6
  *
5
7
  * This is what `.stepSwitch()` routes over, and the reason it exists as a table
6
- * rather than as two more `if (spec.kind === …)` arms: `serialize` has to turn
8
+ * rather than as more `if (spec.kind === …)` arms: `serialize` has to turn
7
9
  * an arm's `value` into a port, `check` has to say which values are legal and
8
10
  * which were left out, and `decompile` has to turn a port back into a value.
9
- * Those three read the same list here instead of each carrying their own copy
10
- * of "how a human task names its ports" and "how an HTTP branch names its
11
- * ports".
11
+ * Those three read the same list instead of each carrying their own copy of
12
+ * "how a human task names its ports".
12
13
  *
13
14
  * `undefined` means the family has no author-declared fan-out at all, which is
14
15
  * what makes `.stepSwitch()` on it an error rather than a no-op.
@@ -68,23 +69,6 @@ export function declaredExits(spec, pinnedVersion) {
68
69
  ...(e.endsProcess ? { endsProcess: true } : {}),
69
70
  }));
70
71
  }
71
- if (kind === 'http') {
72
- const branches = (spec?.inputs?.branches
73
- ?? []);
74
- if (branches.length === 0) {
75
- return {
76
- reason: 'it declares no response branches',
77
- suggestion: `branches: [{ name: 'rateLimited', condition: … }]`,
78
- };
79
- }
80
- // The fall-through is an exit like any other here — it is what `.switch()`
81
- // spells as its default arm, and leaving it out is the same omission as
82
- // leaving out a branch.
83
- return [
84
- ...branches.map((b) => ({ value: String(b?.name ?? ''), port: `branch-${b?.name ?? ''}` })),
85
- { value: 'default', port: 'default', isDefault: true },
86
- ];
87
- }
88
72
  return {
89
73
  reason: `a ${kind ?? 'custom'} step declares no author-named exits`,
90
74
  suggestion: 'Use .step(); route failures with .onError().',
@@ -21,8 +21,6 @@ import { ownEntry } from './own-entry.js';
21
21
  * themselves (the Data Fabric verbs, since Phase 3).
22
22
  */
23
23
  export const TYPED_FACTORIES = {
24
- 'core.action.http': { factory: 'http({ managed: false })' },
25
- 'core.action.http.v2': { factory: 'http({ managed: true })' },
26
24
  'core.control.end': { factory: '.return()' },
27
25
  'core.logic.decision': { factory: '.branch()' },
28
26
  'core.logic.dowhile': { factory: '.doWhile(name, condition, body, { limit, breakEnabled })' },
package/dist/decompile.js CHANGED
@@ -38,6 +38,7 @@ import { commentText } from './core/comment-text.js';
38
38
  import { tsLiteral, tsNumber, tsText } from './core/ts-literal.js';
39
39
  import { contextIndexSlug, parseContextIndexType } from './core/context-index.js';
40
40
  import { wireExprBody } from './core/wire-expr.js';
41
+ import { hasExits } from './core/step-ports.js';
41
42
  import { parseIxpProjectType } from './core/actions.js';
42
43
  import { readFlowGlobals } from './flow-variables.js';
43
44
  import { outputBackingGlobal, outputBackingName } from './flow-output-backing.js';
@@ -236,8 +237,6 @@ const T = {
236
237
  triggerVoice: 'core.trigger.voice',
237
238
  createOutgoingCall: 'uipath.conversational.voice.create-outgoing-call',
238
239
  endCall: 'uipath.conversational.voice.end-call',
239
- http: 'core.action.http',
240
- httpV2: 'core.action.http.v2',
241
240
  decision: 'core.logic.decision',
242
241
  switch: 'core.logic.switch',
243
242
  merge: 'core.logic.merge',
@@ -547,21 +546,29 @@ function specValue(raw) {
547
546
  return raw;
548
547
  }
549
548
  /**
550
- * Print a node whose type is registered (flow-node-registry.ts): the node type
551
- * deserializes it to its spec and prints the factory call. A node at a version
552
- * the type does not register is lifted by its default version's implementation —
553
- * as every built-in family lifts any version — and `verClause` omits a
554
- * `{ version }` the type does not register, with a warning. `undefined` when the type is not registered, or the node type declines
555
- * the node; the caller's branches take it from there.
549
+ * The registered implementation a node of this type and version lifts with: its
550
+ * own version's, else the type's default (as every built-in family lifts any
551
+ * version); undefined when the type is not registered.
556
552
  */
557
- function emitRegisteredSpec(node, imp, inputNames) {
553
+ function registeredImpl(node) {
558
554
  const entry = typeof node.type === 'string' ? FLOW_NODE_TYPES.byTypeName(node.type) : undefined;
559
555
  if (!entry)
560
556
  return undefined;
561
557
  const version = String(node.typeVersion ?? '');
562
- const impl = (Object.hasOwn(entry.type.versions, version)
558
+ return (Object.hasOwn(entry.type.versions, version)
563
559
  ? entry.type.versions[version]
564
560
  : entry.type.versions[entry.type.defaultVersion]);
561
+ }
562
+ /**
563
+ * A node of a registered type (flow-node-registry.ts), lifted to its spec by the
564
+ * type's own `deserialize`: undefined when the type is not registered or declines
565
+ * the node. A node at a version the type does not register is lifted by its
566
+ * default version's implementation, as every built-in family lifts any version.
567
+ */
568
+ function liftRegistered(node, impl = registeredImpl(node)) {
569
+ if (!impl)
570
+ return undefined;
571
+ const version = String(node.typeVersion ?? '');
565
572
  const ctx = {
566
573
  nodeId: node.id,
567
574
  expr: specValue,
@@ -574,8 +581,21 @@ function emitRegisteredSpec(node, imp, inputNames) {
574
581
  warn: warnOnce,
575
582
  };
576
583
  const spec = impl.deserialize(node, ctx);
577
- if (spec === undefined)
584
+ return spec === undefined ? undefined : { impl, spec };
585
+ }
586
+ /**
587
+ * Print a node whose type is registered (flow-node-registry.ts): the node type
588
+ * deserializes it to its spec and prints the factory call. A node at a version
589
+ * the type does not register is lifted by its default version's implementation —
590
+ * as every built-in family lifts any version — and `verClause` omits a
591
+ * `{ version }` the type does not register, with a warning. `undefined` when the type is not registered, or the node type declines
592
+ * the node; the caller's branches take it from there.
593
+ */
594
+ function emitRegisteredSpec(node, imp, inputNames) {
595
+ const lifted = liftRegistered(node);
596
+ if (!lifted)
578
597
  return undefined;
598
+ const { impl, spec } = lifted;
579
599
  // A spec value as source, recursing into arrays and plain objects so an Expr
580
600
  // nested in one (a filter row's value) prints as the helper an author writes —
581
601
  // in the `{ k: v }` / `[a, b]` spelling the hand-written printers use.
@@ -707,10 +727,6 @@ function emitStepSpec(node, imp, inputNames, o, graph) {
707
727
  ['exchangeLimit', num(i.exchangeLimit) === '20' ? undefined : num(i.exchangeLimit)],
708
728
  ])})`;
709
729
  }
710
- case T.http:
711
- return emitHttp(node, false, imp, inputNames);
712
- case T.httpV2:
713
- return emitHttp(node, true, imp, inputNames);
714
730
  default:
715
731
  // A connector action node: `uipath.connector.<key>.<action>` (but NOT the
716
732
  // event nodes `uipath.connector.event.*` / `.trigger.*`, which are triggers).
@@ -767,66 +783,6 @@ function emitStepSpec(node, imp, inputNames, o, graph) {
767
783
  return unsupportedSpec(node, imp, o);
768
784
  }
769
785
  }
770
- /**
771
- * Invert an HTTP node. The plain node (`core.action.http`) keeps its request at
772
- * the top level (`url` / `method` / `headers` / `queryParams` / `body` /
773
- * `contentType`); the managed node (`core.action.http.v2`) nests the request
774
- * inside `detail.bodyParameters` and routes through IS. Request settings
775
- * (`timeout` / `retryCount`) sit at the top level on both. Emitted only-when-off
776
- * their definition defaults, exactly as serialize writes them, so the round-trip
777
- * stays minimal. The response schema (`returns`) is a Phase-2 refinement.
778
- */
779
- function emitHttp(node, managed, imp, inputNames) {
780
- const inputs = node.inputs ?? {};
781
- const detail = managed
782
- ? (inputs.detail ?? {})
783
- : {};
784
- const req = managed
785
- ? detail.bodyParameters ?? {}
786
- : inputs;
787
- const method = typeof req.method === 'string' ? req.method : 'GET';
788
- const headers = managed ? req.headers : inputs.headers;
789
- const query = managed ? req.query : inputs.queryParams;
790
- const bodySrc = req.body !== undefined && req.body !== '' ? emitVal(req.body, imp, inputNames) : undefined;
791
- const headersSrc = emitValMap(headers, imp, inputNames) || undefined;
792
- const querySrc = emitValMap(query, imp, inputNames) || undefined;
793
- const contentType = !managed && typeof inputs.contentType === 'string' && inputs.contentType !== 'application/json'
794
- ? tsLiteral(inputs.contentType) : undefined;
795
- const timeout = typeof inputs.timeout === 'string' && inputs.timeout !== 'PT15M' ? tsLiteral(inputs.timeout) : undefined;
796
- const retry = inputs.retryCount !== undefined && Number(inputs.retryCount) !== 0 ? tsNumber(inputs.retryCount) : undefined;
797
- const branchRows = Array.isArray(inputs.branches) ? inputs.branches : [];
798
- const branchesSrc = branchRows.length
799
- ? `[${branchRows.map((b) => {
800
- const cond = unwrapSource(b.conditionExpression) ?? '';
801
- const expr = emitBareExpr(wireExprBody(cond), imp, inputNames);
802
- return `{ name: ${tsText(b.name ?? b.id)}, condition: ${expr} }`;
803
- }).join(', ')}]`
804
- : undefined;
805
- const connectionValue = detail.connectionId ?? detail.connectionResourceId ?? req.connection;
806
- const connection = managed && typeof connectionValue === 'string' && connectionValue !== 'ImplicitConnection'
807
- ? tsLiteral(connectionValue) : undefined;
808
- const folder = managed && typeof detail.connectionFolderKey === 'string' && detail.connectionFolderKey.length > 0
809
- ? tsLiteral(detail.connectionFolderKey) : undefined;
810
- const targetConnector = managed && typeof req.targetConnector === 'string'
811
- && req.targetConnector !== 'uipath-uipath-http'
812
- ? tsLiteral(req.targetConnector) : undefined;
813
- const spec = opts([
814
- ['url', emitVal(req.url ?? '', imp, inputNames)],
815
- ['method', method !== 'GET' ? tsLiteral(method) : undefined],
816
- ['headers', headersSrc],
817
- ['query', querySrc],
818
- ['body', bodySrc],
819
- ['contentType', contentType],
820
- ['managed', boolLit(managed)],
821
- ['connection', connection],
822
- ['folder', folder],
823
- ['targetConnector', targetConnector],
824
- ['timeout', timeout],
825
- ['retryCount', retry],
826
- ['branches', branchesSrc],
827
- ]);
828
- return `${imp.use('http')}(${spec})`;
829
- }
830
786
  /**
831
787
  * Split a connector `nodeType` (`uipath.connector.<key>.<action>`) into key +
832
788
  * action. Mirrors `core/connectors.ts`'s `splitConnectorNodeType`.
@@ -2715,9 +2671,9 @@ function outcomeIdsOfNode(node) {
2715
2671
  * The exits a node declares, read back off the EMITTED artifact rather than off
2716
2672
  * an authored spec — `inputs.schema.outcomes` for a base or quick-form human
2717
2673
  * task (the product draws one `outcome-<id>` handle per outcome on both, at
2718
- * every version), `inputs.branches` plus `default` for an HTTP call. The
2719
- * authoring-side twin is `declaredExits` in `core/step-ports.ts`; they answer
2720
- * about different inputs, which is why this is not a call into it.
2674
+ * every version), and a registered type's own `exits` over the spec its lift
2675
+ * recovers (an HTTP call's `inputs.branches` plus `default`). The authoring-side
2676
+ * twin is `stepExits` in `flow-step-exits.ts`.
2721
2677
  */
2722
2678
  function declaredExitsOfNode(node) {
2723
2679
  const inputs = (node.inputs ?? {});
@@ -2729,16 +2685,16 @@ function declaredExitsOfNode(node) {
2729
2685
  return undefined;
2730
2686
  return outcomes.map((o) => ({ value: String(o?.name ?? ''), port: `outcome-${o?.id ?? ''}` }));
2731
2687
  }
2732
- if (node.type === T.http || node.type === T.httpV2) {
2733
- const branches = (inputs.branches ?? []);
2734
- if (branches.length === 0)
2735
- return undefined;
2736
- return [
2737
- ...branches.map((b) => ({ value: String(b?.name ?? ''), port: `branch-${b?.name ?? ''}` })),
2738
- { value: 'default', port: 'default' },
2739
- ];
2740
- }
2741
- return undefined;
2688
+ // A registered type that declares exits answers from its own spec, lifted off
2689
+ // this node; one that declares none is not lifted at all (this runs per edge).
2690
+ const impl = registeredImpl(node);
2691
+ if (!impl?.exits)
2692
+ return undefined;
2693
+ const lifted = liftRegistered(node, impl);
2694
+ const exits = lifted && impl.exits(lifted.spec);
2695
+ if (!exits || !hasExits(exits))
2696
+ return undefined;
2697
+ return exits.map((e) => ({ value: e.value, port: e.port }));
2742
2698
  }
2743
2699
  /**
2744
2700
  * Split a switch case expression into `<discriminant> === <literal>`, the only
@@ -11,10 +11,11 @@ import { hasBundledDefinition } from './flow-definitions.js';
11
11
  import { dataFabricFlowFamily } from './nodes/data-fabric/flow/family.js';
12
12
  import { delayFlowFamily } from './nodes/delay/flow/family.js';
13
13
  import { documentFlowFamily } from './nodes/document/flow/family.js';
14
+ import { httpFlowFamily } from './nodes/http/flow/family.js';
14
15
  import { mockFlowFamily } from './nodes/mock/flow/family.js';
15
16
  import { queueFlowFamily } from './nodes/queue/flow/family.js';
16
17
  import { scriptFlowFamily } from './nodes/script/flow/family.js';
17
18
  import { transformFlowFamily } from './nodes/transform/flow/family.js';
18
19
  /** The families, in no significant order. Adding one is the whole of registering it. */
19
- export const FLOW_NODE_FAMILIES = [dataFabricFlowFamily, delayFlowFamily, documentFlowFamily, mockFlowFamily, queueFlowFamily, scriptFlowFamily, transformFlowFamily];
20
+ export const FLOW_NODE_FAMILIES = [dataFabricFlowFamily, delayFlowFamily, documentFlowFamily, httpFlowFamily, mockFlowFamily, queueFlowFamily, scriptFlowFamily, transformFlowFamily];
20
21
  export const FLOW_NODE_TYPES = new NodeTypeRegistry('flow', FLOW_NODE_FAMILIES, { hasBundledDefinition });
@@ -14,6 +14,7 @@ import type { VersionedNodeType } from './core/nodes/versioned.js';
14
14
  import type { NodeFamily, Placement } from './core/nodes/family.js';
15
15
  import type { DefinitionReference, NodeContribution } from './core/node-classes.js';
16
16
  import type { CheckOpts } from './check.js';
17
+ import type { DeclaredExit, NoExits } from './core/step-ports.js';
17
18
  /**
18
19
  * What a Flow node type's `serialize` returns: the node's type-owned fields.
19
20
  *
@@ -81,6 +82,28 @@ export interface FlowSerializeContext {
81
82
  * pair) or must pick an id that is not taken; add entries through `bind`.
82
83
  */
83
84
  readonly bindings: ReadonlyMap<string, Readonly<Record<string, unknown>>>;
85
+ /**
86
+ * A symbolic `connection` or `folder` label resolved through the flow's
87
+ * bindings.json, as a connector step's is: a self-referential label throws the
88
+ * refusal check reports, and a value that cannot be a tenant id is warned about
89
+ * (`CONNECTION_STUB`). `where` names the site in both, e.g. `managed http "fetch"`.
90
+ */
91
+ resolveBinding(name: string, attribute: 'connection' | 'folder', where: string): string;
92
+ /**
93
+ * Hoist the Integration Service connection pair a definition's `model`
94
+ * references (`<bindings.NAME>` for its connection id and folder key), deduped
95
+ * against the flow's existing entries by name, resource key and attribute. An
96
+ * unresolved connection still gets its entries (no `default`), so the
97
+ * definition's references always close.
98
+ */
99
+ bindConnection(o: {
100
+ model: unknown;
101
+ connectorKey: string;
102
+ connectionId: string;
103
+ folderKey?: string;
104
+ /** The entries' `resourceKey`; defaults to `connectionId`. */
105
+ resourceKey?: string;
106
+ }): void;
84
107
  }
85
108
  /** What `deserialize` may ask of the decompiler, for one serialized node. */
86
109
  export interface FlowDeserializeContext {
@@ -146,6 +169,20 @@ export interface FlowCheckContext {
146
169
  } | undefined;
147
170
  /** The options `check` was called with (the connector-library views among them). */
148
171
  readonly options: CheckOpts;
172
+ /**
173
+ * Check a step's symbolic `connection` / `folder` labels against bindings.json,
174
+ * as a connector step's are, reporting `BINDING_SELF_NAME` / `BINDING_UNDECLARED`.
175
+ * Those codes are the pipeline's, shared by every family that binds a
176
+ * connection, so they belong to no family catalog. `use` names the connector
177
+ * the connection must belong to, for the cross-step connection-owner rule.
178
+ */
179
+ bindingLabels(where: string, labels: {
180
+ connection?: unknown;
181
+ folder?: unknown;
182
+ }, use?: {
183
+ id: string;
184
+ connector: string | undefined;
185
+ }): void;
149
186
  }
150
187
  /** What a family-level `check` may ask of the checker. */
151
188
  export interface FlowFamilyCheckContext {
@@ -199,6 +236,28 @@ export interface FlowNodeType<Spec> extends NodeType<Spec, FlowNodeContribution,
199
236
  readonly nodeModel?: 'definition' | 'omit';
200
237
  /** What `rawNode` reports for a raw node of this type. */
201
238
  readonly rawNode?: FlowRawNodeAdvice;
239
+ /**
240
+ * The runtime variable a failed node's envelope is published under, when it is
241
+ * not `error`. `err()` writes one shape for every family
242
+ * (`$vars.<step>.error.<field>`), and serialize rewrites a handler's reads to
243
+ * this variable.
244
+ *
245
+ * Measured on the product, one debug run per family (2026-09-10): six of the
246
+ * seven families probed publish it as `<step>.error`, which is what the
247
+ * definitions declare (`outputDefinition.error.var`). The exception is
248
+ * `core.action.http.v2`, whose `<step>.error` is the boolean `true` and whose
249
+ * envelope is in `<step>.output` (instance 430d43ad-4583-40c0-8f74-b49d2c4904e4).
250
+ */
251
+ readonly errorEnvelope?: string;
252
+ /**
253
+ * The exits the AUTHOR named on this step — an HTTP call's response branches,
254
+ * and the port each leaves on — or why it has none to route. What
255
+ * `.stepSwitch()` routes over, what an arm's `value` and a `.stepToList` port
256
+ * are checked against, and what decompile reads a fully wired node back as.
257
+ * Omitted: the type has no author-declared fan-out. The `error` port is never
258
+ * one of these (`.onError()` routes it).
259
+ */
260
+ exits?(spec: Spec): DeclaredExit[] | NoExits;
202
261
  }
203
262
  /** A Flow node type with several versions. */
204
263
  export type FlowVersionedNodeType<Spec> = VersionedNodeType<Spec, FlowNodeContribution, FlowContexts, FlowNodeType<Spec>>;
@@ -21,6 +21,7 @@ export * from './core/actions.js';
21
21
  export * from './nodes/data-fabric/flow/index.js';
22
22
  export * from './nodes/delay/flow/index.js';
23
23
  export * from './nodes/document/flow/index.js';
24
+ export * from './nodes/http/flow/index.js';
24
25
  export * from './nodes/mock/flow/index.js';
25
26
  export * from './nodes/queue/flow/index.js';
26
27
  export * from './nodes/script/flow/index.js';
package/dist/flow-sdk.js CHANGED
@@ -25,6 +25,7 @@ export * from './core/actions.js';
25
25
  export * from './nodes/data-fabric/flow/index.js';
26
26
  export * from './nodes/delay/flow/index.js';
27
27
  export * from './nodes/document/flow/index.js';
28
+ export * from './nodes/http/flow/index.js';
28
29
  export * from './nodes/mock/flow/index.js';
29
30
  export * from './nodes/queue/flow/index.js';
30
31
  export * from './nodes/script/flow/index.js';
@@ -0,0 +1,12 @@
1
+ import { type DeclaredExit, type NoExits } from './core/step-ports.js';
2
+ /**
3
+ * The author-declared exits of a step's spec: a registered type's own `exits`,
4
+ * at the version the step resolves to (its pin, else the type's choice; an
5
+ * unregistered pin falls back to that choice, as check's other rules do), and
6
+ * `declaredExits` for a kind not yet registered. `serialize` and `check` ask
7
+ * here, so they agree on what a port means.
8
+ */
9
+ export declare function stepExits(spec: {
10
+ kind?: unknown;
11
+ inputs?: unknown;
12
+ } | undefined, pinnedVersion?: string): DeclaredExit[] | NoExits;