Skip to content

Commit d25a7ae

Browse files
authored
Merge branch 'main' into claude/issue-9362-cascade-probe-multiple-lookup
2 parents eee5f89 + 8131686 commit d25a7ae

93 files changed

Lines changed: 6505 additions & 641 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
'@objectstack/runtime': minor
3+
---
4+
5+
**BREAKING** — `POST /api/v1/automation/:name/runs/:runId/resume` refuses a request
6+
body carrying an unknown top-level key. The resume body's outer envelope is now a
7+
closed set: exactly `inputs`, `variables`, `output`, `branchLabel`.
8+
9+
Until now the route read the keys it knows and silently ignored the rest, so a body
10+
like `{"nodeId":"ask","values":{...}}` — no key of which the route reads — answered
11+
HTTP 200 `success:true` with the screen submission treated as empty: the run
12+
completed and the submitted value never reached the flow. A caller that guessed
13+
`values` for the key the route spells `inputs` got silence instead of a correction.
14+
15+
What changes on the wire:
16+
17+
- **A body with any unknown top-level key ⇒ `400` with `error.code:
18+
'VALIDATION_FAILED'`.** The message names the offending key(s) and the accepted
19+
set; `error.details.fields[]` carries one `unknown_field` entry per offending key.
20+
The request never reaches the flow engine, the suspension is untouched, and the
21+
same request with a corrected body is expected to succeed — this refusal sits on
22+
the retryable side beside `INVALID_SIGNAL` and `INVALID_SCREEN_INPUT`, and is
23+
deliberately not `FLOW_FAILED` (which the console treats as terminal, because it
24+
means the engine consumed the suspension and the run actually ran).
25+
- **Unchanged:** a body made only of accepted keys behaves exactly as before,
26+
including an empty body (a legal empty submission for a screen whose declared
27+
fields are all optional). The signal is still assembled field-by-field — never a
28+
body spread — so the service-authority marker stays unforgeable.
29+
30+
Any client already sending only the documented keys is unaffected. A client sending
31+
extra keys alongside a correct `inputs` now gets the located 400 above instead of
32+
having the extras silently dropped.
33+
34+
<!-- adr-0087: not-required (no-migration-prescription) retires no metadata surface: no Zod schema, no authorable key, no stored sys_metadata row changes shape, so `objectstack migrate meta` has nothing to rewrite and no ledger entry can be written for it. What changes is which HTTP request bodies one route accepts, and the only channel that reaches those callers is this changeset itself. -->
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. -->
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
---
2+
'@objectstack/service-automation': minor
3+
'@objectstack/runtime': minor
4+
'@objectstack/client': minor
5+
---
6+
7+
**BREAKING** — both automation `trigger` routes answer real HTTP status codes for a flow
8+
that ran and failed, instead of HTTP 200 wrapping an inner `{success: false}`.
9+
10+
This is the second and wider half of the migration the resume route shipped in the same
11+
release (#8684, merged 2026-08-17): one SDK-visible behaviour change, one migration note.
12+
The resume flip touched the screen-flow runner; this one touches the door every app
13+
dispatches flows through.
14+
15+
Until now a flow driven to a node failure answered:
16+
17+
```
18+
HTTP 200
19+
{"success":true,"data":{"success":false,"error":"Node 'create_opportunity' failed: …"}}
20+
```
21+
22+
The run genuinely failed; the transport reported success. A scripted or integration caller
23+
that branches on the HTTP status alone read a failed run as a successful one. This applies
24+
the `/actions` ruling (business failures must not ride HTTP 200 inside a double envelope)
25+
to `POST /api/v1/automation/:name/trigger` and to the legacy
26+
`POST /api/v1/automation/trigger/:name` — the shape `client.automation.trigger()` calls.
27+
Both doors answer through one mapper, so they cannot drift.
28+
29+
What changes on the wire:
30+
31+
- **A flow that ran and then failed ⇒ `400` with `error.code: 'FLOW_FAILED'`.** The node
32+
failure stays the human-readable `error.message`. The flow author's own `errorMessage`
33+
travels in `error.details.errorMessage` — one documented location, the same one the
34+
console reads — and the run's per-node accounting in `error.details.summary`. A flow
35+
whose `errorHandling.strategy` is `retry` answers the same way once its attempts are
36+
exhausted. `durationMs` is no longer carried on this response.
37+
- **A flow name the deployment does not hold ⇒ `404`,** answered before anything is
38+
dispatched, through the same registry probe `POST /:name/toggle` and `GET /:name` use.
39+
- **Unchanged:** a successful run still answers 200 with its result, and a run that PAUSED
40+
at a `screen` node still answers 200 with the next screen — a pause is not a failure.
41+
- **Also unchanged, pending a ruling:** a DISABLED flow and one with no start node still
42+
answer 200 with the inner failure. Both are exits that never dispatched anything, and
43+
telling them apart needs a producer-side classification the closed
44+
`AutomationResult.code` union cannot yet express. Tracked on #9378.
45+
46+
**`@objectstack/service-automation`:** `execute()` now stamps `status: 'failed'` on the
47+
results of runs that dispatched and were rejected — the same lifecycle verdict it already
48+
writes to the run log. Its never-dispatched exits carry no `status`, which is what lets a
49+
transport answer the two classes differently without inspecting the result's internals.
50+
51+
**`@objectstack/client`:** `client.automation.trigger()`, `client.automation.execute()` and
52+
`client.project(id).automation.execute()` now **reject** on a failed run instead of
53+
resolving with `{ success: false, error }` — the SDK throws on every non-2xx before
54+
unwrapping. Callers that inspected the resolved value must move to a `catch`:
55+
56+
```ts
57+
try {
58+
await client.automation.execute(flow, { params });
59+
} catch (err: any) {
60+
err.code; // 'FLOW_FAILED' (400) — the run ran and failed
61+
err.httpStatus; // 400 | 404
62+
err.message; // the node failure, verbatim
63+
err.details?.errorMessage; // the flow author's own message, when the flow declares one
64+
err.details?.summary; // which node failed
65+
}
66+
```
67+
68+
Raw-HTTP callers that treated `2xx` as success and never opened the inner envelope now see
69+
the failure they were already being told about, one level up.
70+
71+
<!-- adr-0087: not-required (no-migration-prescription) retires no metadata surface: no Zod schema, no authorable key, no stored sys_metadata row changes shape, so `objectstack migrate meta` has nothing to rewrite and no ledger entry can be written for it. What changes is an HTTP status plus an SDK method's promise contract, and the only channel that reaches those consumers is this changeset itself. -->
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@objectstack/metadata-protocol": minor
3+
---
4+
5+
`publishPackageDrafts` (Studio's "publish whole app", `POST /packages/:id/publish-drafts`) now reports the #4463 runtime authoring gate's non-blocking findings on each `published[]` element as an optional `advisories` key — the same element shape and omitted-when-empty discipline as the single-item publish door (#9176). An advisory-free batch's response bytes are unchanged, and `failed[]` elements are unaffected (an `error` finding still aborts the batch). Previously the batch door computed these per-draft findings and discarded them.

0 commit comments

Comments
 (0)