Skip to content

Commit f4b13fb

Browse files
committed
feat(spec): FLOW_DISABLED / FLOW_NO_START_NODE complete the trigger status ruling (#9415)
The #9378 ruling classified four trigger-route outcomes. PR #9413 delivered two of them (404, and 400 FLOW_FAILED) and stopped on the other two exactly as dispatched: a DISABLED flow and one with NO START NODE are both exits that never dispatched anything, and the closed `AutomationResult.code` union had no honest member for either. The two workarounds were measured and rejected — a regex on the engine's message prose, and a route-side enable-state probe that duplicates execution policy and opens a TOCTOU window answering "disabled mid-flight" as "malformed definition". This is the spec-seat fragment the #9384 ruling routes here: a deliberate union widening with measured need, not a call-site mint. - spec: `AutomationResult.code` gains 'FLOW_DISABLED' / 'FLOW_NO_START_NODE', documented as trigger-time refusals classified BEFORE dispatch and grouped apart from the resume-refusal members. Both registered in the ADR-0112 error-code ledger under @objectstack/runtime — the door that names the wire vocabulary, following FLOW_FAILED. - engine: the disabled-flow and no-start-node exits stamp their codes and continue to carry NO `status`. That absence is #9413's provable separation of never-dispatched from ran-and-failed, and the test asserting it is strengthened rather than relaxed. - route: two arms in `respondToFlowTrigger` read the producer codes — FLOW_DISABLED => 409, FLOW_NO_START_NODE => 422. No sniffing of `summary`, `durationMs` or the message text. - tests: the two "stay unchanged at 200" pins, written to flip when this landed, now pin 409/422; both directions are covered (a classified exit is answered by its code even carrying a failed run's incidental fields; an UNCLASSIFIED exit still stays 200). Client-side pins mirror the FLOW_FAILED ones. - docs: the five pages the #9413 drift advisory named, re-verified against the now-complete four-row contract. Also corrects two stale TSDoc bullets on the same union that named 'PERMISSION_DENIED' / 'INVALID_SIGNAL' by lowercase spellings the union has never contained and no producer emits (engine stamps the SCREAMING_SNAKE members; ADR-0112 D6). Fixes #9378
1 parent a433122 commit f4b13fb

16 files changed

Lines changed: 569 additions & 73 deletions

File tree

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/service-automation': minor
4+
'@objectstack/runtime': minor
5+
'@objectstack/client': minor
6+
---
7+
8+
**BREAKING** — the automation `trigger` routes now answer **409** for a disabled
9+
flow and **422** for a flow whose definition has no start node, instead of HTTP
10+
200 wrapping an inner `{success: false}`.
11+
12+
This finishes the migration the previous release started. That changeset flipped
13+
two of the four outcomes and said of the other two:
14+
15+
> **Also unchanged, pending a ruling:** a DISABLED flow and one with no start
16+
> node still answer 200 with the inner failure. Both are exits that never
17+
> dispatched anything, and telling them apart needs a producer-side
18+
> classification the closed `AutomationResult.code` union cannot yet express.
19+
20+
That is the paragraph this change resolves. The union was widened deliberately —
21+
two new members, with measured need — rather than the transport guessing from
22+
message text or re-implementing the engine's enable-state policy.
23+
24+
`POST /api/v1/automation/:name/trigger` and the legacy
25+
`POST /api/v1/automation/trigger/:name` now answer, in full:
26+
27+
| Status | `error.code` | The run |
28+
|:---|:---|:---|
29+
| `404` || never dispatched: no such flow |
30+
| `409` | `FLOW_DISABLED` | never dispatched: the flow is switched off |
31+
| `422` | `FLOW_NO_START_NODE` | never dispatched: the definition has no `start` node |
32+
| `400` | `FLOW_FAILED` | RAN, and was rejected |
33+
| `200` || succeeded, or PAUSED at a screen node — a pause is not a failure |
34+
35+
The three refusals report no run because none exists: no node executed and
36+
nothing was written. Only `400` describes a run, and only it carries
37+
`error.details.summary` / `error.details.errorMessage`.
38+
39+
Why two statuses and not one: a disabled flow is reversible operational state —
40+
enable it and the identical request succeeds, which is what `409` means. A flow
41+
with no start node cannot be executed as stored, and no retry helps, which is
42+
what `422` means. Collapsing them would tell an operator to flip a switch that
43+
will not help.
44+
45+
**`@objectstack/spec`:** `AutomationResult.code` gains `'FLOW_DISABLED'` and
46+
`'FLOW_NO_START_NODE'`. The union stays closed; these are trigger-time refusals
47+
classified *before* dispatch, documented as a group distinct from the existing
48+
resume-refusal members. Both are registered in the ADR-0112 error-code ledger.
49+
50+
**`@objectstack/service-automation`:** `execute()` stamps the matching `code` on
51+
its disabled-flow and no-start-node exits. They continue to carry **no**
52+
`status` — that absence is what lets a transport tell a never-dispatched exit
53+
from a run that dispatched and failed (`status: 'failed'`) without inspecting
54+
`summary`, `durationMs` or the message.
55+
56+
**`@objectstack/client`:** `client.automation.trigger()`, `.execute()` and
57+
`client.project(id).automation.execute()` already rejected on a failed run;
58+
they now reject with these two additional classifications, so a caller can tell
59+
"enable the flow and retry" from "the flow definition is broken":
60+
61+
```ts
62+
try {
63+
await client.automation.execute(flow, { params });
64+
} catch (err: any) {
65+
err.httpStatus; // 409 | 422 | 400 | 404
66+
err.code; // 'FLOW_DISABLED' | 'FLOW_NO_START_NODE' | 'FLOW_FAILED'
67+
}
68+
```
69+
70+
Callers that branch only on `FLOW_FAILED` keep working for the case they
71+
handle, but will no longer see these two refusals under it — they arrive with
72+
their own codes, which is the point.
73+
74+
Not affected, and deliberately so: `POST /api/v1/actions/...` with a
75+
`type: 'flow'` action, and metadata-declared `type: 'flow'` endpoints. Both
76+
dispatch the same flow through a different door with its own response
77+
conventions, and whether they should inherit this table is tracked separately.
78+
79+
<!-- adr-0087: not-required (no-migration-prescription) retires no metadata surface: no Zod schema, no authorable key, and no stored sys_metadata row changes shape, so `objectstack migrate meta` has nothing to rewrite and no ledger entry could be written for it. What changes is an HTTP status plus two new members of a runtime result type, and the channel that reaches those consumers is this changeset plus the compiler. -->

content/docs/api/client-sdk.mdx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,17 @@ await client.i18n.getFieldLabels('account', 'zh-CN');
385385
// Automation — Trigger workflows and automations
386386
await client.automation.trigger('send_welcome_email', { userId });
387387

388+
// A flow that does not run REJECTS — it does not resolve with an inner
389+
// `{ success: false }`. Branch on `err.code`, not on the resolved value:
390+
try {
391+
await client.automation.execute('order_approval', { params: { orderId } });
392+
} catch (err) {
393+
err.httpStatus; // 409 | 422 | 400 | 404
394+
err.code; // 'FLOW_DISABLED' | 'FLOW_NO_START_NODE' | 'FLOW_FAILED'
395+
err.details?.errorMessage; // the flow author's own text, on a FLOW_FAILED
396+
err.details?.summary; // which node failed, on a FLOW_FAILED
397+
}
398+
388399
// Screen flows pause for user input instead of completing. `execute()` returns
389400
// `{ status: 'paused', runId, screen }`; render the screen, then resume the run
390401
// with the collected values. A wizard pauses again for each further step.

content/docs/api/plugin-endpoints.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ Approve/reject were never workflow routes (ADR-0019): approval is a flow node, a
4646

4747
| Method | Endpoint | Description |
4848
|:-------|:---------|:------------|
49-
| POST | `/automation/:name/trigger` | Trigger an automation flow by name (legacy alias: `/automation/trigger/:name`) |
49+
| POST | `/automation/:name/trigger` | Trigger an automation flow by name (legacy alias: `/automation/trigger/:name`). Failures answer real status codes, not a `200` wrapping an inner failure: **404** unknown flow, **409** `FLOW_DISABLED`, **422** `FLOW_NO_START_NODE`, **400** `FLOW_FAILED` for a run that ran and was rejected — see [Run a flow via API](/docs/automation/flows#run-a-flow-via-api) |
5050

5151
The automation dispatcher also exposes flow CRUD (`GET`/`POST /automation`, `GET`/`PUT`/`DELETE /automation/:name`) and run observability/resume routes — see [Durable pause & resume](/docs/automation/flows#durable-pause--resume-adr-0019).
5252

content/docs/automation/flows.mdx

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1296,6 +1296,43 @@ curl -b cookies.txt -X POST \
12961296
| `POST /api/v1/automation/:name/runs/:runId/resume` | Resume a paused run — body `{ inputs, output, branchLabel }` |
12971297
| `GET /api/v1/automation/:name/runs/:runId/screen` | The pending screen of a screen-flow run |
12981298

1299+
**Failures answer real HTTP status codes.** A flow that does not run is not a
1300+
`200`. The route classifies four outcomes, and it reads the engine's own verdict
1301+
to do it — never the message text:
1302+
1303+
| Status | `error.code` | What happened | What to do |
1304+
|:---|:---|:---|:---|
1305+
| `404` || No flow by that name in this deployment | Check the name; nothing was dispatched |
1306+
| `409` | `FLOW_DISABLED` | The flow exists but is switched off | Enable it — the identical request then succeeds |
1307+
| `422` | `FLOW_NO_START_NODE` | The stored definition has no `start` node | Fix the flow; retrying cannot help |
1308+
| `400` | `FLOW_FAILED` | The flow **ran** and was rejected | Read `error.details.summary` for the failing node |
1309+
1310+
The first three never dispatched anything: no node executed, no record was
1311+
written, and there is no run to look up. Only `400` describes a run.
1312+
1313+
```
1314+
POST /api/v1/automation/order_approval/trigger
1315+
1316+
409 { "success": false,
1317+
"error": { "code": "FLOW_DISABLED",
1318+
"message": "Flow 'order_approval' is disabled",
1319+
"httpStatus": 409 } }
1320+
```
1321+
1322+
A `400` additionally carries the flow author's own `errorMessage` (when the flow
1323+
declares one) at `error.details.errorMessage`, and the run's per-node accounting
1324+
at `error.details.summary`.
1325+
1326+
<Callout type="warn">
1327+
**Upgrading from v16 or earlier?** All four of these used to answer `200` with
1328+
the failure wrapped inside `data`
1329+
(`{"success":true,"data":{"success":false,"error":"..."}}`). A caller that
1330+
branched on the HTTP status alone read every failed run as a successful one.
1331+
There is no inner envelope on a failure any more — the error is at the top
1332+
level. SDK callers must move from inspecting the resolved value to a `catch`;
1333+
see [the SDK reference](/docs/api/client-sdk).
1334+
</Callout>
1335+
12991336
**Passing inputs.** Declare variables with `isInput: true`, then send them in
13001337
`params` under the same names — only declared inputs are bound. `recordId` and
13011338
`objectName` are lifted into `params` for you (plus an `<objectName>Id` alias),
@@ -1310,13 +1347,15 @@ row-level security. See [API Authentication](/docs/api#authentication) for
13101347
credentials.
13111348

13121349
<Callout type="warn">
1313-
**`success: true` doesn't always mean "it ran".** If the start condition isn't
1314-
met, the response is a *successful* skip — the run result rides in `data`, and
1315-
the skip marker sits on its `output`:
1350+
**A `200` doesn't always mean "it ran".** The failure statuses above cover
1351+
flows that were refused or that died; a *skip* is neither. If the start
1352+
condition isn't met, the response is a **successful** skip — the run result
1353+
rides in `data`, and the skip marker sits on its `output`:
13161354
`{ "success": true, "data": { "success": true, "output": { "skipped": true, "reason": "condition_not_met" } } }`.
13171355
A self-triggering flow caught by the loop guard reports
1318-
`reason: "reentrancy_loop_guard"` the same way. Check `data.output.skipped`
1319-
before assuming the work happened.
1356+
`reason: "reentrancy_loop_guard"` the same way. So checking the status is not
1357+
enough on its own: check `data.output.skipped` before assuming the work
1358+
happened.
13201359
</Callout>
13211360

13221361
## Console Flow Viewer & Test Runner

content/docs/protocol/kernel/http-protocol.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1216,7 +1216,7 @@ declaration to shadow a built-in route:
12161216
| Endpoint declares | Answer |
12171217
|:---|:---|
12181218
| `type: 'object_operation'` | delegated to the same `callData` binding that serves `/api/v1/data/{object}` — byte-identical `data` |
1219-
| `type: 'flow'` | delegated to the same automation pipeline as `POST /api/v1/automation/{name}/trigger` |
1219+
| `type: 'flow'` | delegated to the same automation pipeline as `POST /api/v1/automation/{name}/trigger` — the same execution context builder and the same `execute` call, so the run itself is identical. **The response is not**: the trigger route classifies a refused or failed run into real status codes (404 / 409 `FLOW_DISABLED` / 422 `FLOW_NO_START_NODE` / 400 `FLOW_FAILED`), while this seam still answers `200` with the result in `data` for every outcome ([#9446](https://github.com/objectstack-ai/objectstack/issues/9446)). Read `data.success` here, not the status |
12201220
| `authRequired: true` (or omitted) + anonymous caller | `401` `UNAUTHENTICATED`, the same envelope every seam answers |
12211221
| `rateLimit` armed and exhausted | `429` + `Retry-After`, never with a cache directive |
12221222
| `cacheTtl: 30` on a successful GET | `Cache-Control: private, max-age=30``private` is a security rule, not tuning: any response can be RLS-trimmed |

content/docs/references/api/contract.mdx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const result = ApiErrorSchema.parse(data);
2727

2828
| Property | Type | Required | Description |
2929
| :--- | :--- | :--- | :--- |
30-
| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| 'INVALID_FORMAT' \| 'VALUE_TOO_LONG' \| 'VALUE_TOO_SHORT' \| 'VALUE_OUT_OF_RANGE' \| … +283 more>` || Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) |
30+
| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| 'INVALID_FORMAT' \| 'VALUE_TOO_LONG' \| 'VALUE_TOO_SHORT' \| 'VALUE_OUT_OF_RANGE' \| … +285 more>` || Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) |
3131
| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) |
3232
| **message** | `string` || Readable error message |
3333
| **category** | `string` | optional | Error category (e.g. validation, authorization) |
@@ -179,7 +179,9 @@ const result = ApiErrorSchema.parse(data);
179179
* `FILE_NOT_FOUND`
180180
* `FILTER_TOKEN_UNKNOWN`
181181
* `FILTER_TOKEN_UNRESOLVED`
182+
* `FLOW_DISABLED`
182183
* `FLOW_FAILED`
184+
* `FLOW_NO_START_NODE`
183185
* `FORBIDDEN`
184186
* `FORM_NOT_FOUND`
185187
* `FORM_RESOLVE_FAILED`

content/docs/references/api/error-code-ledger.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,9 @@ const result = ErrorCode.parse(data);
284284
* `FILE_NOT_FOUND`
285285
* `FILTER_TOKEN_UNKNOWN`
286286
* `FILTER_TOKEN_UNRESOLVED`
287+
* `FLOW_DISABLED`
287288
* `FLOW_FAILED`
289+
* `FLOW_NO_START_NODE`
288290
* `FORBIDDEN`
289291
* `FORM_NOT_FOUND`
290292
* `FORM_RESOLVE_FAILED`

content/docs/ui/actions.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,7 @@ The endpoint dispatches on the **declared `type`**, exactly like the MCP
343343
| `type` | Over REST |
344344
|:---|:---|
345345
| `script` | Runs the registered handler / inline body. |
346-
| `flow` | Runs `target` on the automation engine, with your identity forwarded (a `runAs: 'user'` flow enforces RLS as you). Equivalent to `POST /api/v1/automation/:target/trigger`, without having to know the flow name. |
346+
| `flow` | Runs `target` on the automation engine, with your identity forwarded (a `runAs: 'user'` flow enforces RLS as you). Dispatches the same flow as `POST /api/v1/automation/:target/trigger`, without having to know the flow name. ⚠️ It does **not** answer the same way: any unsuccessful outcome comes back as **400** `FLOW_FAILED`, where the trigger route separates a run that failed (400) from one that was never dispatched (404 / 409 / 422) — see [#9446](https://github.com/objectstack-ai/objectstack/issues/9446). |
347347
| `api` | **400** — it dispatches on `target`; call that endpoint directly. |
348348
| `url` / `modal` / `form` | **400** — client-side navigation; there is nothing for the server to run. |
349349

packages/client/src/client.test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1425,6 +1425,68 @@ describe('ObjectStackClient.automation', () => {
14251425
expect(err.message).toContain('no_such_flow');
14261426
});
14271427

1428+
// [#9415] The ruling's remaining two rows, mirrored on the SDK because the
1429+
// SDK is where the distinction is consumed. Both are NEVER-DISPATCHED
1430+
// refusals: nothing ran, nothing was written, and a caller that retries a
1431+
// 422 is retrying an authoring defect. `err.code` is what a caller branches
1432+
// on — a pin asserting only that the promise rejected would stay green if
1433+
// both refusals collapsed onto one status, which is the whole thing the two
1434+
// new union members exist to prevent.
1435+
//
1436+
// Both spellings are pinned again for the reason the FLOW_FAILED pins give:
1437+
// `trigger()` reads `res.json()` while `execute()` reads `unwrapResponse()`,
1438+
// so a regression in either unwrap path is invisible from the other's test.
1439+
it('should reject with 409 FLOW_DISABLED when the flow is switched off (legacy trigger)', async () => {
1440+
const { client } = createMockClient({
1441+
success: false,
1442+
error: { code: 'FLOW_DISABLED', message: "Flow 'welcome_flow' is disabled", httpStatus: 409 },
1443+
}, 409);
1444+
1445+
const err: any = await client.automation
1446+
.trigger('welcome_flow', {})
1447+
.then(() => { throw new Error('expected the disabled flow to reject'); }, (e) => e);
1448+
1449+
expect(err.code).toBe('FLOW_DISABLED');
1450+
expect(err.httpStatus).toBe(409);
1451+
expect(err.message).toContain('is disabled');
1452+
// Not a failed RUN: the operator's remedy is to enable the flow, and
1453+
// FLOW_FAILED would send them to look at a run that never existed.
1454+
expect(err.code).not.toBe('FLOW_FAILED');
1455+
});
1456+
1457+
it('should reject with 422 FLOW_NO_START_NODE when the definition cannot run', async () => {
1458+
const { client } = createMockClient({
1459+
success: false,
1460+
error: { code: 'FLOW_NO_START_NODE', message: 'Flow has no start node', httpStatus: 422 },
1461+
}, 422);
1462+
1463+
const err: any = await client.automation
1464+
.execute('startless_flow', {})
1465+
.then(() => { throw new Error('expected the startless flow to reject'); }, (e) => e);
1466+
1467+
expect(err.code).toBe('FLOW_NO_START_NODE');
1468+
expect(err.httpStatus).toBe(422);
1469+
expect(err.message).toContain('no start node');
1470+
expect(err.code).not.toBe('FLOW_FAILED');
1471+
});
1472+
1473+
it('should keep the two never-dispatched refusals distinguishable from each other', async () => {
1474+
// One member per condition is the point of the #9415 widening; a caller
1475+
// deciding "retry after enabling" vs "fix the definition" reads exactly
1476+
// this difference.
1477+
const disabled: any = await createMockClient({
1478+
success: false,
1479+
error: { code: 'FLOW_DISABLED', message: 'x', httpStatus: 409 },
1480+
}, 409).client.automation.execute('f', {}).then(() => null, (e) => e);
1481+
const startless: any = await createMockClient({
1482+
success: false,
1483+
error: { code: 'FLOW_NO_START_NODE', message: 'x', httpStatus: 422 },
1484+
}, 422).client.automation.execute('f', {}).then(() => null, (e) => e);
1485+
1486+
expect(disabled.code).not.toBe(startless.code);
1487+
expect(disabled.httpStatus).not.toBe(startless.httpStatus);
1488+
});
1489+
14281490
it('should still resolve when a triggered flow succeeds', async () => {
14291491
// The other half of the contract: a successful dispatch is untouched,
14301492
// including the paused screen-flow shape the runner drives.

packages/client/src/index.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3038,6 +3038,17 @@ export class ObjectStackClient {
30383038
*
30393039
* A flow name the deployment does not hold rejects with **404** instead.
30403040
* A run that PAUSED at a screen node is not a failure and still resolves.
3041+
*
3042+
* **Since #9415 the two never-dispatched refusals are distinguishable**,
3043+
* completing the #9378 status table. Neither ran a single node, so
3044+
* neither carries a run summary and neither is `FLOW_FAILED`:
3045+
*
3046+
* | `err.httpStatus` | `err.code` | what happened | the caller's remedy |
3047+
* |:---|:---|:---|:---|
3048+
* | `409` | `FLOW_DISABLED` | the flow is switched off | enable it — the identical request then succeeds |
3049+
* | `422` | `FLOW_NO_START_NODE` | the stored definition has no `start` node | fix the flow; retrying cannot help |
3050+
* | `400` | `FLOW_FAILED` | the flow RAN and was rejected | read `err.details.summary` for the failing node |
3051+
* | `404` | — | no such flow in this deployment | check the name |
30413052
*/
30423053
trigger: async (triggerName: string, payload: any) => {
30433054
const route = this.getRoute('automation');
@@ -3197,7 +3208,10 @@ export class ObjectStackClient {
31973208
* `400` `FLOW_FAILED` (author text on `err.details.errorMessage`, per-node
31983209
* accounting on `err.details.summary`) instead of resolving with an inner
31993210
* `{ success: false }` under HTTP 200; an unknown flow rejects with `404`.
3200-
* See `automation.trigger` for the full shape — both call the same door.
3211+
* Since #9415, a DISABLED flow rejects with `409` `FLOW_DISABLED` and one
3212+
* whose definition has no `start` node with `422` `FLOW_NO_START_NODE` —
3213+
* both never dispatched, so neither is `FLOW_FAILED`.
3214+
* See `automation.trigger` for the full table — both call the same door.
32013215
*/
32023216
execute: async <T = any>(name: string, ctx?: Record<string, any>): Promise<T> => {
32033217
const route = this.getRoute('automation');
@@ -5377,7 +5391,9 @@ export class ScopedProjectClient {
53775391
* **breaking #9378 behaviour**: a run that ran and then failed REJECTS with
53785392
* `400` `FLOW_FAILED` (author text on `err.details.errorMessage`) instead of
53795393
* resolving with an inner `{ success: false }` under HTTP 200, and an
5380-
* unknown flow rejects with `404`. See that method for the full shape.
5394+
* unknown flow rejects with `404`. Since #9415 a disabled flow rejects with
5395+
* `409` `FLOW_DISABLED` and a definition with no `start` node with `422`
5396+
* `FLOW_NO_START_NODE`. See that method for the full table.
53815397
*/
53825398
execute: async <T = any>(name: string, ctx?: Record<string, any>): Promise<T> => {
53835399
const res = await this.parent._fetch(this.url(`/automation/${encodeURIComponent(name)}/trigger`), {

0 commit comments

Comments
 (0)