# FlowDrop Workflow Specification 1.0-draft > The rules a FlowDrop workflow obeys — how a workflow is written, stored, validated > and executed — stated independently of any one implementation. It is a target: where > an implementation disagrees with a rule, the rule is what is intended. It carries no > implementation status, and confers no certification. Every rule is published as markdown at its own URL with `.md` appended, e.g. https://flowdrop.io/spec/rules/gr-store/store-2.md. The whole corpus in one file is at https://flowdrop.io/spec/llms-full.txt, and as structured data — every field, plus backlinks and the identifiers this specification has declined — at https://flowdrop.io/spec/rules.json. Identifiers (`STORE-2`, `R6.a`) are permanent and are the correct way to cite a rule. ## Reference - [Conventions](https://flowdrop.io/spec/conventions.md): vocabulary, requirement levels and the references every rule would otherwise repeat. Read this before quoting a rule. - [Glossary](https://flowdrop.io/spec/glossary.md): the terms rules are written in. ## GR-STORE — STORE (Part I) - [STORE-1: A request body is bounded before anything is parsed](https://flowdrop.io/spec/rules/gr-store/store-1.md): The cheapest refusals come first. A body that is too large, too deeply nested, or not JSON at all is turned away before any workflow-level meaning is read out of it. - [STORE-2: A workflow must be named](https://flowdrop.io/spec/rules/gr-store/store-2.md): Every workflow carries a name, on creation and on every update. The name is what a person uses to find it again, so the system refuses to store one without it. - [STORE-3: A client-supplied id never overwrites an existing workflow](https://flowdrop.io/spec/rules/gr-store/store-3.md): A create request may supply the workflow's `id`. If a workflow with that id already exists, the request is refused with 409 and the stored workflow is left exactly as it was. An implementation must ne… - [STORE-4: Absent collections default to empty, and an update touches only what it sends](https://flowdrop.io/spec/rules/gr-store/store-4.md): An update is partial. What a caller does not send, it does not change, which is what lets an editor save one part of a workflow without holding the whole of it. - [STORE-5: A workflow refused by validation is never partially stored](https://flowdrop.io/spec/rules/gr-store/store-5.md): A workflow that fails validation on create or on update is refused with 422 carrying `success: false`, a human-readable `error`, and `details`, a list of `{code, message, locator}` entries, one per er… - [STORE-7: Stored node metadata carries the type anchor and nothing else](https://flowdrop.io/spec/rules/gr-store/store-7.md): Storage constrains a node's `data.metadata` to a single key, `node_type_id`. A node's `data.config` is unconstrained at storage: its shape is the node type's business and is judged by validation, not … - [STORE-9: Import gates run in a fixed order and roll back what they generated](https://flowdrop.io/spec/rules/gr-store/store-9.md): Importing a bundle can create node types before it knows whether the workflow is acceptable. The fixed order, and the rollback, are what keep a refused import from leaving debris behind. - [STORE-10: Read-path repairs must never run on the save path](https://flowdrop.io/spec/rules/gr-store/store-10.md): Loading a workflow to run it may quietly repair it. Doing the same on the way in would hide exactly the conditions validation exists to report. - [STORE-11: Status vocabularies are closed sets of strings](https://flowdrop.io/spec/rules/gr-store/store-11.md): Job, pipeline, session and message status are each a closed vocabulary. A status is a string, and one spelling serves everywhere: what is persisted is what every JSON payload carries and what every ev… - [STORE-12: A finished turn leaves the session completed, not idle](https://flowdrop.io/spec/rules/gr-store/store-12.md): `idle` means a session was created and has never executed. A session whose turn has finished is released as `completed`. The two are distinct states, and an implementation must not use `idle` to mean … - [STORE-13: One name per concept for a node's type](https://flowdrop.io/spec/rules/gr-store/store-13.md): A node has a type it is an instance of and a type it is drawn as. They are different things, and a payload that spells both the same way cannot be read without knowing who wrote it. - [STORE-14: Every surface that returns a workflow returns one object](https://flowdrop.io/spec/rules/gr-store/store-14.md): List, create, read and update all publish the same workflow object. A client learns one shape, not four. - [STORE-6: Storing a workflow drops the editor's scratch state](https://flowdrop.io/spec/rules/gr-store/store-6.md): A canvas carries state that means something while someone is looking at it and nothing afterwards. Storing the workflow is where that state is dropped, which is why a client reading back what it just … - [STORE-8: A published contract version is three numbers](https://flowdrop.io/spec/rules/gr-store/store-8.md): The version is read by consumers that never read the contract itself, a cache validator among them, so it has to be a value that always exists and always compares. - [STORE-15: Searching the workflow list matches a literal substring of the name](https://flowdrop.io/spec/rules/gr-store/store-15.md): The term is text to find, not a pattern to interpret, so a name containing a percent sign is found by searching for a percent sign. ## GR-API — API (Part I) - [API-1: Every JSON door applies the same body gate, and reports its refusals](https://flowdrop.io/spec/rules/gr-api/api-1.md): A caller should not have to learn which endpoint bounds its input. Every door that takes a JSON body applies the same limits and gives the same answer when they are exceeded. - [API-2: The turn door and the launch door judge inputs identically](https://flowdrop.io/spec/rules/gr-api/api-2.md): A session turn's `inputs` are checked exactly as a launch's are: an undeclared key is refused, a declared required input is enforced, values are checked against the declared schema, and resolution int… - [API-3: A session with no workflow is a conflict, not a server error](https://flowdrop.io/spec/rules/gr-api/api-3.md): A request against a session that has no associated workflow is refused with 409 on every door that serves that session; it is a problem with the session's state, the same refusal family as a turn refu… - [API-4: Node configuration could be checked ahead of save](https://flowdrop.io/spec/rules/gr-api/api-4.md): A node's configuration could be checked ahead of save, by submitting the configuration on its own to a per-node-type validation endpoint. That surface is withdrawn: judging a configuration without the… - [API-5: The paginated envelope is a third envelope with four fixed keys](https://flowdrop.io/spec/rules/gr-api/api-5.md): Two envelopes carry a result and a refusal. A paginated result is a third, and its pagination block belongs to the envelope rather than to the rows it sits beside. - [API-6: Paging is clamped silently, and the clamped values are what is reported](https://flowdrop.io/spec/rules/gr-api/api-6.md): A paginated door caps `limit` at 100 and floors `offset` at 0. Out-of-range paging is corrected, never refused: there is no 400 for it. The corrected values are what the pagination block reports, so a… - [API-7: A generic failure is a last resort, never a design](https://flowdrop.io/spec/rules/gr-api/api-7.md): A catch-all failure answer is a reporting device. Where it stands in for a refusal the door could have named, it renders "this endpoint has never worked" indistinguishable from "the server hiccupped". - [API-8: Every refusal carries a stable machine-readable code](https://flowdrop.io/spec/rules/gr-api/api-8.md): A client has to be able to tell one refusal from another without reading English. Codes are the contract; the message is for a person. ## GR-VAL — VAL (Part I) - [R5.a: A workflow is bounded to 500 nodes](https://flowdrop.io/spec/rules/gr-val/r5-a.md): A workflow must not contain more than 500 nodes. A workflow that does is refused with the error code `R5_TOO_MANY_NODES`, and the save does not take effect. - [R5.b: A workflow is bounded to 1000 edges](https://flowdrop.io/spec/rules/gr-val/r5-b.md): A workflow must not contain more than 1000 edges. A workflow that does is refused with the error code `R5_TOO_MANY_EDGES`, and the save does not take effect. - [R5.c: Every node carries an id](https://flowdrop.io/spec/rules/gr-val/r5-c.md): A node without an id cannot be referred to by an edge, an exposure entry or a runtime result, so a workflow containing one is refused rather than stored. - [R1.a: A node's executor must exist](https://flowdrop.io/spec/rules/gr-val/r1-a.md): A node whose node type resolves to an executor the implementation does not provide is refused with the error code `R1_PLUGIN_MISSING`. The verdict is reached from the executor's declaration alone; the… - [R1.b: A node with no resolvable node type is not judged by R1](https://flowdrop.io/spec/rules/gr-val/r1-b.md): Nodes that carry no node type anchor at all (notes and other non-executable decoration) are legitimate, and must not be refused for having no executor. - [R1.c: A missing executor is reported once per node](https://flowdrop.io/spec/rules/gr-val/r1-c.md): A node whose executor is missing yields exactly one R1 error, however many ports the node exposes. The defect is a property of the node, and is never re-reported per port. - [R6.b: Required config keys must be present](https://flowdrop.io/spec/rules/gr-val/r6-b.md): The list of required keys is the one the node type's derived config schema publishes, not the node type's raw required flags; a parameter that can be fed by an edge is deliberately not required in con… - [R6.c: An unknown config key warns but does not block](https://flowdrop.io/spec/rules/gr-val/r6-c.md): A key in a node's `config` that the node's derived config schema does not declare does not refuse the save. It yields a warning at `node.{id}.config.{key}` saying the value is ignored at execution, an… - [R6.d: A config value must match its declared type](https://flowdrop.io/spec/rules/gr-val/r6-d.md): R6.d through R6.h are one check with several arms. They share a code and a locator and stop at the first violation, so a reader of the result sees at most one error per config key. - [R6.e: A config value must be one of its enumerated values](https://flowdrop.io/spec/rules/gr-val/r6-e.md): Where a config value's schema declares an `enum`, the value must be one of the listed values. Comparison is exact and applies no type coercion, so the number `1` does not satisfy an `enum` listing the… - [R6.f: Numeric bounds apply to numbers only](https://flowdrop.io/spec/rules/gr-val/r6-f.md): A config value below its schema's `minimum` or above its `maximum` is refused with `R6_CONFIG_INVALID` at `node.{id}.config.{key}`. Bounds are evaluated only for a value that is a JSON number; a strin… - [R6.g: Length bounds count characters, never bytes](https://flowdrop.io/spec/rules/gr-val/r6-g.md): A limit measured in bytes would depend on the alphabet the author writes in, so the same word would fit one implementation and not another. - [R6.h: A pattern mismatch is refused, a broken pattern is not](https://flowdrop.io/spec/rules/gr-val/r6-h.md): A pattern the implementation cannot compile is a defect in the node type, not in the workflow, and the author who cannot fix it must not be blocked by it. - [R6.i: A null config value means unset](https://flowdrop.io/spec/rules/gr-val/r6-i.md): A config value of `null` means the key is unset. It satisfies a required key, and no schema constraint is evaluated against it, so a `null` never fails type, `enum`, bounds or `pattern`. - [R6.j: No node type, no schema-driven config verdict](https://flowdrop.io/spec/rules/gr-val/r6-j.md): When a node's node type does not resolve there is no derived config schema to judge its `config` against, so the schema-driven checks of R6 (required keys, type, `enum`, bounds, `pattern`, and the unk… - [R6.l: A malformed secret reference warns the author](https://flowdrop.io/spec/rules/gr-val/r6-l.md): Writing `{{ secrets.NAME }}` instead of `${{ secrets.NAME }}` produces a literal string at execution rather than a secret, silently and with no error. The warning exists to catch the missing `$` while… - [R6.k: There is no separate per-executor config verdict](https://flowdrop.io/spec/rules/gr-val/r6-k.md): A per-executor configuration-validation step was once part of the node executor contract. It decided nothing, and it is withdrawn: an implementation must not gate a save on one. Save-time verdicts on … - [R8.a: An edge's source node must exist](https://flowdrop.io/spec/rules/gr-val/r8-a.md): An edge whose `source` does not name a node in the workflow is refused with `R8_EDGE_SOURCE_MISSING` at `edge.{index}`, where `{index}` is the edge's position in the workflow's edge list. An empty `so… - [R8.b: An edge's target node must exist](https://flowdrop.io/spec/rules/gr-val/r8-b.md): An edge whose `target` does not name a node in the workflow is refused with `R8_EDGE_TARGET_MISSING` at `edge.{index}`, where `{index}` is the edge's position in the workflow's edge list. An empty `ta… - [R8.c: Two dangling endpoints are two errors](https://flowdrop.io/spec/rules/gr-val/r8-c.md): An edge whose `source` and `target` are both missing yields two errors, one for each endpoint, both at the same `edge.{index}` locator. The endpoints are judged independently, so neither failure suppr… - [R7.a: An edge may not target a hidden input port](https://flowdrop.io/spec/rules/gr-val/r7-a.md): An edge whose target port is declared by the target node's node type but is not exposed on that node is refused with `R7_EDGE_TARGET_NOT_EXPOSED` at `edge.{index}`, where `{index}` is the edge's posit… - [R7.b: An edge may not leave a hidden output port](https://flowdrop.io/spec/rules/gr-val/r7-b.md): An edge whose source port is declared by the source node's node type but is not exposed on that node is refused with `R7_EDGE_SOURCE_NOT_EXPOSED` at `edge.{index}`. - [R7.c: A node instance decides which of its ports are exposed](https://flowdrop.io/spec/rules/gr-val/r7-c.md): One chain answers "is this port exposed?" everywhere it is asked: for edges and for the workflow's exposure map alike. - [R7.d: A port the node type does not declare is outside exposure checking](https://flowdrop.io/spec/rules/gr-val/r7-d.md): A port that appears on an edge but is not declared in the node type's metadata (a dynamic or author-defined port) is out of scope for R7, and an edge over it is not refused for being unexposed. - [R7.e/f: Exposure checking is skipped per endpoint, not per edge](https://flowdrop.io/spec/rules/gr-val/r7-e-f.md): Skipping the whole edge would let one broken end hide a real fault at the other. Each end is judged on its own, so an edge can carry a dangling-source error and a hidden-target error at once. - [R4.a: An exposed workflow port must be named in the permitted alphabet](https://flowdrop.io/spec/rules/gr-val/r4-a.md): An exposure name is a caller-facing parameter name. Restricting it to a small alphabet keeps it usable as a key in every context a caller passes it through. - [R4.b: Exposure names are unique per side](https://flowdrop.io/spec/rules/gr-val/r4-b.md): Workflow exposure names must be unique within a side. Inputs and outputs are independent, so one name may appear once as an input and once as an output. A repeat within one side is refused with `R4_NA… - [R4.c: An exposure entry must name a node that exists](https://flowdrop.io/spec/rules/gr-val/r4-c.md): A workflow exposure entry's `node_id` must name a node in the workflow. An entry that does not is refused at `schema.{side}.{index}`, and the entry's remaining checks are skipped, since none of them c… - [R4.d: An exposure entry must name a port the node declares](https://flowdrop.io/spec/rules/gr-val/r4-d.md): A workflow exposure entry's `port` must be declared by the named node: as an input parameter for an entry on the input side, as an output for one on the output side. An entry naming an undeclared port… - [R4.e: An exposure name may not collide with a reserved runtime name](https://flowdrop.io/spec/rules/gr-val/r4-e.md): R4.a's alphabet admits names beginning with underscores, which is where the runtime's own injected parameters live. Without this rule an author could claim one of them and shadow it. - [W-T: An edge leaving a terminal node warns](https://flowdrop.io/spec/rules/gr-val/w-t.md): A terminal node ends the run, so nothing downstream of it will execute. The edge is legal (the author may be mid-edit), but it is almost certainly not what they meant. - [VAL-LAUNCH: The validator runs again at launch, before anything is created](https://flowdrop.io/spec/rules/gr-val/val-launch.md): A definition can become invalid after it was saved: a node type removed, a workflow written by a path that skipped validation. Re-checking at launch means an invalid workflow fails as a refusal, not a… - [R2: Node ids are unique within a workflow](https://flowdrop.io/spec/rules/gr-val/r2.md): A workflow is read by keying its nodes on their ids. Two nodes sharing one id would silently collapse to whichever was read last, so the duplicate is refused at save instead. - [R3: Every edge carries a unique id](https://flowdrop.io/spec/rules/gr-val/r3.md): Every edge in a workflow must carry an `id`, and edge `id`s must be unique within the workflow. An edge with no `id` is refused with `R3_EDGE_MISSING_ID`, a repeated `id` with `R3_EDGE_DUPLICATE_ID`, … - [R9: A node's node type anchor must resolve](https://flowdrop.io/spec/rules/gr-val/r9.md): R9 and R1 are deliberately disjoint: a node type that does not exist is R9's, a node type that exists but has no executor is R1's. One defect earns one report. - [R10: A workflow may not expose a port hidden on its node](https://flowdrop.io/spec/rules/gr-val/r10.md): Hidden means hidden in both directions. R7 stops an edge reaching a hidden port; R10 stops the workflow's public surface reaching one. - [R11: A configured expression must be valid for its engine at save](https://flowdrop.io/spec/rules/gr-val/r11.md): An expression that cannot parse can only fail once the workflow is already running. Catching it at save turns a runtime failure into an editing error. - [R12: A node may not be wired to itself](https://flowdrop.io/spec/rules/gr-val/r12.md): An edge whose non-empty `source` equals its `target` is refused with `R12_EDGE_SELF` at `edge.{index}`. This holds whatever ports the two ends name, so wiring a node's own output into its own input is… - [R13: Two identical edges between the same ports are refused](https://flowdrop.io/spec/rules/gr-val/r13.md): Duplicate detection compares what the document literally says, not what it means. Two spellings of the same logical connection are therefore two distinct edges, and both are kept. - [R6.a: Config is a JSON object](https://flowdrop.io/spec/rules/gr-val/r6-a.md): Every other rule about config addresses it by key, which presumes an object. This is the rule that says so, and it holds for nodes no schema is judging. ## GR-EDGE — EDGE (Part I) - [EDGE-1: A handle encodes the node, the direction and the port](https://flowdrop.io/spec/rules/gr-edge/edge-1.md): A wire carries no declared type. Everything the system deduces about it, it deduces from the two handles, so how a handle is spelled and split is grammar, not detail. - [EDGE-2: No edge key is structurally required](https://flowdrop.io/spec/rules/gr-edge/edge-2.md): An edge is accepted structurally whatever keys it carries: an absent key takes its empty default: the empty string for an endpoint or a handle, the empty list for a collection. An edge left without en… - [EDGE-3: A trigger edge is one whose target port is the trigger port](https://flowdrop.io/spec/rules/gr-edge/edge-3.md): An edge is a trigger edge when, and only when, its target handle ends with `-input-trigger`. Nothing else marks a wire as a trigger. - [EDGE-4: A loopback edge targets the port named loop_back](https://flowdrop.io/spec/rules/gr-edge/edge-4.md): An edge is a loopback edge when, and only when, its target handle ends with `-input-loop_back`. The reserved port name is `loop_back`: a handle ending with `-input-loopback` is an ordinary edge. - [EDGE-5: A declared edge type wins over the handle, if it is recognised](https://flowdrop.io/spec/rules/gr-edge/edge-5.md): An edge may name its own kind instead of leaving it to the handles. Because a named kind overrides what the handles say, a name nobody recognises is refused rather than carried through. - [EDGE-6: An error edge routes a failure instead of raising it](https://flowdrop.io/spec/rules/gr-edge/edge-6.md): An edge whose source handle ends with `-output-error` is an error edge. When a node that has one fails, the failure is routed rather than raised: the node's job is recorded as failed and marked as hav… - [EDGE-7: A tool-only node is excluded from the execution graph](https://flowdrop.io/spec/rules/gr-edge/edge-7.md): A node is tool-only when it has at least one outgoing edge and every one of its outgoing edges is a tool edge. A node with no outgoing edges is not tool-only, and neither is one with any non-tool outg… - [EDGE-8: A condition on an edge is tolerated, warned about, and ignored](https://flowdrop.io/spec/rules/gr-edge/edge-8.md): A condition on an edge is a removed feature. A stored `edge.data.condition` is tolerated and round-trips unchanged, and it gates nothing: the edge always routes. A non-empty string condition is report… - [EDGE-9: Two edge vocabularies, and they never mix](https://flowdrop.io/spec/rules/gr-edge/edge-9.md): An edge is written one way on the wire and another way in the execution record. Each form has its own key names, and a key from the wrong bag produces something nothing reads. ## GR-SCHEMA — SCHEMA (Part I) - [SCH-1: A node's ports come from its two declared schemas](https://flowdrop.io/spec/rules/gr-schema/sch-1.md): Everything an editor draws on a node (its input ports and its output ports) is derived from two schemas the node type declares. Nothing else is a source. - [SCH-2: Gate flags come from the node type, and default off](https://flowdrop.io/spec/rules/gr-schema/sch-2.md): Whether a parameter is connectable, configurable or required is the node type's answer, not the declared schema's. A parameter the node type never mentions reaches neither derived schema. - [SCH-3: One parameter schema splits into two derived schemas](https://flowdrop.io/spec/rules/gr-schema/sch-3.md): A parameter belongs to the derived input schema if and only if it is `connectable`, and to the derived config schema if and only if it is `configurable`. Both, one, or neither is legal. - [SCH-4: A hidden parameter is dropped from everything](https://flowdrop.io/spec/rules/gr-schema/sch-4.md): A parameter marked hidden is dropped from both derived schemas and from the config defaults, overriding whatever the node type says about `connectable` and `configurable`. Hiding does not suppress a r… - [SCH-5: Reserved config-only parameters always carry a default](https://flowdrop.io/spec/rules/gr-schema/sch-5.md): `dynamicInputs`, `dynamicOutputs` and `branches` are reserved config-only parameters and are never input ports. Where a node type opts one in by marking it `configurable`, it enters the config schema … - [SCH-6: Which required list a required parameter lands in](https://flowdrop.io/spec/rules/gr-schema/sch-6.md): A required parameter is emitted into the input schema's `required` list if and only if it is not `configurable`, and into the config schema's `required` list if and only if it is not `connectable`. - [SCH-7: An output is exposed unless the node type says otherwise](https://flowdrop.io/spec/rules/gr-schema/sch-7.md): A declared output survives into a node's metadata unless the node type marks that output not exposed. Where the node type says nothing about it, the output is exposed: the default is fail-open. - [SCH-8: How a schema property becomes a port](https://flowdrop.io/spec/rules/gr-schema/sch-8.md): One mapping turns a schema property into the port an editor draws, so every schema-derived port on every node is built the same way. - [SCH-9: A union type resolves to its first non-null member](https://flowdrop.io/spec/rules/gr-schema/sch-9.md): Where a property's type is an array of types, its lane is derived from the first member that is not `null`. An array that is empty, or that holds only `null`, resolves to the sink. Every member must i… - [SCH-10: The map from a schema type to a port lane](https://flowdrop.io/spec/rules/gr-schema/sch-10.md): A port's lane is what an editor colours it by and what it checks a wire against. It is derived from the property's schema type by one closed map. - [SCH-10.a: A site's lane configuration overlays the shipped one](https://flowdrop.io/spec/rules/gr-schema/sch-10-a.md): A site may recolour, rename or add lanes. What it stores is a patch on the shipped vocabulary; serving it as a replacement freezes the vocabulary at the moment it was saved. - [SCH-11: Every node but a start node gets a trigger input](https://flowdrop.io/spec/rules/gr-schema/sch-11.md): A reserved `trigger` input port is injected on every node type except a start node type, unless the node type already declares one, and carries display order 100. - [SCH-12: Every node but a terminal node gets a trigger output](https://flowdrop.io/spec/rules/gr-schema/sch-12.md): A reserved `trigger` output port is injected on every node type except a terminal node type, unless the node type already declares one, and carries display order 100. - [SCH-13: A tool-exposed node gets a tool output](https://flowdrop.io/spec/rules/gr-schema/sch-13.md): A reserved `tool` output port is injected on a node type if and only if that node type is exposed as a tool, and carries display order 110. - [SCH-14: Every executable node gets a hidden error output](https://flowdrop.io/spec/rules/gr-schema/sch-14.md): A reserved `error` output port is injected on every node type except a non-executable one, carries display order 120, and ships hidden. - [SCH-15: A reserved port states its exposure only when it diverges](https://flowdrop.io/spec/rules/gr-schema/sch-15.md): An injected reserved port declares `x-exposed-by-default: false` only where that diverges from the default of true. Where it is exposed by default it says nothing, and a reader takes silence as expose… - [SCH-16: What each reserved port defaults to on the canvas](https://flowdrop.io/spec/rules/gr-schema/sch-16.md): The reserved ports default to not exposed (the unified `input` and `output`, the `tool` output, the `error` output and the `loop_back` input), except the `trigger` input and the `trigger` output, whic… - [SCH-17: The unified input port](https://flowdrop.io/spec/rules/gr-schema/sch-17.md): A unified `input` port is prepended to a node's ports where its node type exposes one. It carries the `json` lane, and its description enumerates the node's connectable parameter keys only. - [SCH-18: The unified output port](https://flowdrop.io/spec/rules/gr-schema/sch-18.md): A unified `output` port is prepended to a node's ports where its node type exposes one. It carries the `json` lane, and its description enumerates the node's exposed output keys only. - [SCH-19: A unified port's description skips trigger and hidden keys](https://flowdrop.io/spec/rules/gr-schema/sch-19.md): Both unified-port key enumerations skip the `trigger` port and every hidden parameter. A hidden port does not exist on the instance, so it never appears in a unified port's description. - [SCH-20: Visual type is the node type's, and is offered where there is a choice](https://flowdrop.io/spec/rules/gr-schema/sch-20.md): A node's visual type comes from its node type, never from a declared schema. Where the node type supports more than one visual type, a reserved `nodeType` string property is written into the config sc… - [SCH-21: The reserved config properties every node carries](https://flowdrop.io/spec/rules/gr-schema/sch-21.md): `instanceTitle` and `instanceDescription` are injected into every node's config schema; `maxRetries` and `ports` into an executable node's config schema only. Each injection is skipped where the node … - [SCH-22: A config-edit descriptor appears only where one is provided](https://flowdrop.io/spec/rules/gr-schema/sch-22.md): A node's metadata carries a `configEdit` descriptor only where its node type provides one. A node type that provides none carries no such key. - [SCH-23: When a node has a ui schema, and when it has none](https://flowdrop.io/spec/rules/gr-schema/sch-23.md): The ui schema is what turns a flat config form into grouped sections. Whether there is one at all is decided by a single question about the final schema. - [SCH-25: Enrichment anchors on the node type and changes nothing else](https://flowdrop.io/spec/rules/gr-schema/sch-25.md): A node is enriched from the node type it records, and from nothing else. Enrichment replaces the node's metadata and sets the node's type to the universal node constant; the node's config, id, label a… - [SCH-26: The catalog an editor reads](https://flowdrop.io/spec/rules/gr-schema/sch-26.md): The node catalog served to an editor lists enabled node types only, sorted by category and then by name. - [SCH-27: A served port omits everything that matches the default](https://flowdrop.io/spec/rules/gr-schema/sch-27.md): The wire shape is lean by divergence, which means a client cannot read absence as "unknown". Absence is the default, and the defaults are fixed here. - [SCH-28: A node's executor is resolved from its node type](https://flowdrop.io/spec/rules/gr-schema/sch-28.md): The executor for a node is resolved from that node's node type. An executor named in stored node metadata is never trusted. - [SCH-29: Stored metadata is never authoritative at run time](https://flowdrop.io/spec/rules/gr-schema/sch-29.md): Before a workflow runs, every node is re-enriched from its live node type and the workflow is normalized from the result. Metadata stored with the workflow is never authoritative at run time. - [SCH-31: Config field order applies inside a ui schema only](https://flowdrop.io/spec/rules/gr-schema/sch-31.md): `x-config-order` orders config fields ascending; a property whose value is absent or not numeric counts as 0, and ties keep declaration order. It takes effect inside a generated ui schema only: where … - [SCH-32: Every re-enterable node gets a hidden loop-back input](https://flowdrop.io/spec/rules/gr-schema/sch-32.md): Re-entry is a property of the graph, not of a handful of node types. Every node that can be re-entered declares the port; it just ships hidden, so no canvas gains a re-entry handle until an author ask… - [SCH-33: A schema that will not load never rewrites stored parameters](https://flowdrop.io/spec/rules/gr-schema/sch-33.md): A momentary failure to load a node type's declared schema once looked exactly like "this node type declares nothing", and a save then wrote that emptiness back as permanent configuration loss. - [SCH-34: A property's type is a JSON Schema type, not a port lane](https://flowdrop.io/spec/rules/gr-schema/sch-34.md): The two vocabularies overlap on four words, which is why a lane name written into `type` looks correct, derives the wrong lane, and is caught by nothing. - [SCH-35: Lane derivation is total, and an undeclared port is the sink](https://flowdrop.io/spec/rules/gr-schema/sch-35.md): "What does an undeclared port mean?" once had seven disagreeing answers. It has one: the sink. - [SCH-36: One declaration site for the shipped lane vocabulary](https://flowdrop.io/spec/rules/gr-schema/sch-36.md): The lanes an implementation ships are declared in exactly one place, and every reader derives from that declaration rather than mirroring it: the derivation's range, the served payload and any picker … - [SCH-37: A control port declares its lane, never a schema type](https://flowdrop.io/spec/rules/gr-schema/sch-37.md): `trigger`, `tool` and `loop_back` carry no value, so no JSON Schema type describes them. A control port declares `x-data-type` and no `type`, and a reserved-port injection assigns the lane by the port… - [SCH-38: A loop-back input is a control sink, and `any` is retired](https://flowdrop.io/spec/rules/gr-schema/sch-38.md): `any` and `trigger` differed in one way only (one had compatibility rules built into it from every other lane), and that is a property of being the sink, not of being `any`. - [SCH-38.a: A retired lane keeps one release of served-but-disabled compatibility](https://flowdrop.io/spec/rules/gr-schema/sch-38-a.md): A lane leaves the declarable vocabulary as soon as nothing ships it, but leaves the served payload only a major version later, because the two lists answer different questions: what a port may newly d… - [SCH-39: Compatibility is asymmetric, and the sink needs rules both ways](https://flowdrop.io/spec/rules/gr-schema/sch-39.md): A lane with no rules accepts only its own, which is why the sink (a lane worn by outputs as well as inputs) once refused every wire drawn into it. - [SCH-40: A port's lane is consumed at a public boundary](https://flowdrop.io/spec/rules/gr-schema/sch-40.md): The lane a port declares is not only a colour on a canvas. It is the shared answer to "what may a caller supply?" at the endpoints that start a workflow and take a turn in a session. - [SCH-41: The lane vocabulary is the served payload, not the shipped set](https://flowdrop.io/spec/rules/gr-schema/sch-41.md): What a port may declare and what an implementation ships are different questions. Answering the first with the second is how a site could add a lane it could never use. - [SCH-42: A shape is a named JSON Schema, and the shape id is the lane id](https://flowdrop.io/spec/rules/gr-schema/sch-42.md): Shapes let a lane say what it carries (an order, an entity, a remote contract) without adding a second vocabulary an editor would have to reconcile with lanes. - [SCH-43: Shape compatibility is nominal and mostly derived](https://flowdrop.io/spec/rules/gr-schema/sch-43.md): Two shapes match because they carry the same name, never because their schemas happen to agree; otherwise edge legality would drift every time a schema gained a field. - [SCH-44: A shape refines another declaration of its lane; four ids are reserved](https://flowdrop.io/spec/rules/gr-schema/sch-44.md): Where two sources declare the same lane id, the site's own declaration wins over a code-declared shape, and both win over the shipped entry. The winner replaces the entry whole rather than key by key,… - [SCH-46: A shape's schema is served on the lane, not on every port](https://flowdrop.io/spec/rules/gr-schema/sch-46.md): One copy of each shape's schema, on the lane entry, keeps it fresh when a site edits a shape and leaves the per-port slot free for the narrower thing that actually needs it. - [SCH-24: A node type that fails to build does not fail the request](https://flowdrop.io/spec/rules/gr-schema/sch-24.md): One unbuildable node type costs the reader that node's metadata, not the whole response. - [SCH-45: A stored lane overlay is checked when it is stored](https://flowdrop.io/spec/rules/gr-schema/sch-45.md): The overlay is the one piece of the vocabulary a site writes by hand, so it is the one place a mistake should be reported to the person making it. ## GR-CFG — CFG (Part I) - [CFG-1: An unrecorded gate flag is off](https://flowdrop.io/spec/rules/gr-cfg/cfg-1.md): A node type records per-parameter gate flags: whether a parameter may receive a wire, whether it appears in the config form, whether it is required. Where a flag was never recorded, the answer is no. - [CFG-2: An unrecorded default-exposure flag means exposed](https://flowdrop.io/spec/rules/gr-cfg/cfg-2.md): Default exposure carries the opposite polarity to the gate flags, deliberately. A port nobody has decided about is visible, so a parameter added after a node type was last saved does not silently vani… - [CFG-3: A parameter's effective default comes from the node type, then the schema](https://flowdrop.io/spec/rules/gr-cfg/cfg-3.md): A parameter's effective default is the node type's recorded default where that default is non-null, otherwise the processor's schema default, otherwise null. A node-type default recorded as null is no… - [CFG-4: Priority 1, a value delivered on a wire](https://flowdrop.io/spec/rules/gr-cfg/cfg-4.md): The first place a parameter's value can come from is the run itself: a value another node sent down a wire, or a value the launch payload supplied. - [CFG-5: Priority 2, the author's saved config](https://flowdrop.io/spec/rules/gr-cfg/cfg-5.md): Where priority 1 does not apply, a parameter takes its value from the node's saved configuration when the parameter is not internal, is configurable, and the configuration contains a key of that name.… - [CFG-6: Priority 3, the effective default, unconditionally](https://flowdrop.io/spec/rules/gr-cfg/cfg-6.md): Where neither priority 1 (CFG-4) nor priority 2 (CFG-5) applies, a parameter takes its effective default (CFG-3). This fallback is unconditional: no gate flag suppresses it. - [CFG-7: An explicit null at a higher priority wins](https://flowdrop.io/spec/rules/gr-cfg/cfg-7.md): Sending null is a decision, not a silence. A node that emits null on a wire has said something, and what it said beats whatever the author saved. - [CFG-8: Internal parameters take runtime values only](https://flowdrop.io/spec/rules/gr-cfg/cfg-8.md): Names prefixed `__` are the system's own channel into a processor. They sidestep the gate flags, which also means an author can never see or set one; choosing the prefix is choosing that semantics, no… - [CFG-9: A required parameter that resolves to null fails the node](https://flowdrop.io/spec/rules/gr-cfg/cfg-9.md): Where a parameter is required and the resolution chain (CFG-4 to CFG-6) yields null, the node execution fails with a missing-parameter error naming the parameter. The check runs after the whole chain … - [CFG-10: A resolved value is validated against the parameter schema](https://flowdrop.io/spec/rules/gr-cfg/cfg-10.md): A resolved value that is not null is validated against the parameter's schema. Any violation fails the node execution with a validation error carrying the name of the constraint that failed. `type` an… - [CFG-11: Type checking matches exactly and never coerces](https://flowdrop.io/spec/rules/gr-cfg/cfg-11.md): A `type` check compares the value against the named type exactly: a value of the wrong type fails, and is never coerced to make it pass. A type name the implementation does not recognise passes rather… - [CFG-12: An unrecognised format passes rather than fails](https://flowdrop.io/spec/rules/gr-cfg/cfg-12.md): A `format` an implementation recognises is enforced: a value that does not match it fails validation (CFG-10). A `format` name the implementation does not recognise passes, so a schema written against… - [CFG-13: A processor sees its declared parameters and nothing else](https://flowdrop.io/spec/rules/gr-cfg/cfg-13.md): The resolved parameter set is shaped by the processor's own schema, not by whatever happens to be in the stored config. A key nobody declared cannot arrive by being typed into config. - [CFG-14: Every declared parameter is present, null when unresolved](https://flowdrop.io/spec/rules/gr-cfg/cfg-14.md): Every parameter the processor's schema declares is present in the resolved parameter set. One that resolves to no value is present with the value null; it is never absent. - [CFG-15: A dynamic input is forwarded verbatim](https://flowdrop.io/spec/rules/gr-cfg/cfg-15.md): Dynamic ports are the author's own additions to one node instance. They are carried through untouched, which is also why the type an author declares on one is documentation and nothing more. - [CFG-16: The unified input port is decomposed before resolution](https://flowdrop.io/spec/rules/gr-cfg/cfg-16.md): A node may take all its inputs as one bundled object on the reserved `input` port. That bundle is unpacked into individual named inputs before any parameter is resolved, so the rest of the chain canno… - [CFG-17: A hidden port cannot be filled through the bundle](https://flowdrop.io/spec/rules/gr-cfg/cfg-17.md): Decomposition filters on `connectable` alone, so a hidden port's key can survive the unpacking. Resolution then applies the exposure gate a second time. Two guards, one outcome: a hidden port is not f… - [CFG-18: Published config defaults cover configurable parameters only](https://flowdrop.io/spec/rules/gr-cfg/cfg-18.md): A node type's published metadata carries a config default for a parameter only where the parameter is configurable and its effective default (CFG-3) is non-null. No entry is published for a parameter … ## GR-EXPO — EXPO (Part I) - [EXPO-1: Exposure resolves the same way everywhere it is consulted](https://flowdrop.io/spec/rules/gr-expo/expo-1.md): Exposure is asked about in a lot of places: resolving parameters, deciding what an agent may fill, validating a saved workflow, stripping outputs, drawing the author's toggles. All of them must get th… - [EXPO-2: An instance override decides exposure; otherwise the default does](https://flowdrop.io/spec/rules/gr-expo/expo-2.md): A port is exposed on a node instance when the instance's direction-scoped overrides in `data.config.ports` contain an entry for that port carrying an `exposed` key: that key's value is the answer. Whe… - [EXPO-3: Overrides are scoped by direction](https://flowdrop.io/spec/rules/gr-expo/expo-3.md): Exposure overrides are scoped by port direction: an output port's override is read from the `outputs` map, and an input port's (that of every other direction) from the `inputs` map. An input and an ou… - [EXPO-4: A node nobody has touched stores no overrides](https://flowdrop.io/spec/rules/gr-expo/expo-4.md): A node instance on which no port has been shown or hidden stores no `ports` map at all. Every one of its ports therefore resolves to its default exposure (EXPO-2), and adding a port to the node type l… - [EXPO-5: Only an exact false suppresses default exposure](https://flowdrop.io/spec/rules/gr-expo/expo-5.md): The schema extension that suggests hiding a port is read by identity, not by truthiness. Anything that is not the boolean false leaves the port exposed, so a malformed or half-migrated value fails tow… - [EXPO-6: A processor's exposure suggestion is authoring input only](https://flowdrop.io/spec/rules/gr-expo/expo-6.md): A processor may suggest that a port ship hidden. That suggestion is consumed once, when a node type's exposure values are first written, and never again, so a consumer reading a node type's published … - [EXPO-7: Published schemas carry the flag only where the default is hidden](https://flowdrop.io/spec/rules/gr-expo/expo-7.md): Where a node type's stored default exposure for a port is false, the schema it publishes carries `x-exposed-by-default: false`. Where it is anything else, the key is absent from the published schema r… - [EXPO-8: A port with no stored decision is exposed, whatever the processor suggested](https://flowdrop.io/spec/rules/gr-expo/expo-8.md): A known seam, recorded so nobody is surprised by it. It is the price of the fail-open polarity: a port nobody has decided about shows up rather than disappearing. - [EXPO-10: Hiding an input port changes precedence, it does not unset the parameter](https://flowdrop.io/spec/rules/gr-expo/expo-10.md): A value arriving at a hidden input is ignored, not rejected. The parameter still gets a value (the author's, or the default), so hiding a port never turns a working node into a failing one. - [EXPO-11: Output stripping covers declared output ports only](https://flowdrop.io/spec/rules/gr-expo/expo-11.md): Hiding an output means its value never leaves the node. The pass that enforces this works from the processor's declared output schema, so keys that are not declared ports (dynamic outputs, reserved co… - [EXPO-12: Control outputs are never stripped](https://flowdrop.io/spec/rules/gr-expo/expo-12.md): The reserved control outputs `active_branches` and `state_update` are never removed from a node's result by exposure stripping. They are the engine's own channel, not ports an author shows or hides, a… - [EXPO-13: A node type can hide an output outright](https://flowdrop.io/spec/rules/gr-expo/expo-13.md): Where a node type marks an output port `exposed: false`, that output is stripped from every instance's result unconditionally, and no instance override can restore it. Otherwise the instance's own ove… - [EXPO-14: A hidden output's value never reaches anything downstream](https://flowdrop.io/spec/rules/gr-expo/expo-14.md): Stripping is the first thing that happens to a result, which is what makes the invariant hold rather than nearly hold. A value removed before anything else looks at it cannot leak through a checkpoint… - [EXPO-15: A model may fill only visible, unwired parameters](https://flowdrop.io/spec/rules/gr-expo/expo-15.md): Where a node is offered to an agent node as a callable tool, a parameter is fillable by the model when it is connectable and exposed and no data edge already feeds that port. A hidden parameter, a non… - [EXPO-17: Port display order is cosmetic](https://flowdrop.io/spec/rules/gr-expo/expo-17.md): A port's declared display order is presentational metadata for whatever draws the node. No execution, resolution, validation or exposure decision may depend on it, and reordering ports must not change… - [EXPO-16: Seeding a port's default-exposure decision](https://flowdrop.io/spec/rules/gr-expo/expo-16.md): A suggestion fills a decision nobody has made yet, and only for a port that is actually there. ## GR-DYN — DYN (Part I) - [DYN-1: Dynamic ports are opt-in, and a node that does not opt in has none](https://flowdrop.io/spec/rules/gr-dyn/dyn-1.md): Dynamic ports let an author add connection points to one node instance beyond what its processor declares. Nothing gets them by accident: a node type has to ask for them, and the definitions start emp… - [DYN-2: A dynamic port declares a name, a label and a data type](https://flowdrop.io/spec/rules/gr-dyn/dyn-2.md): Every dynamic port definition carries a `name`, a `label` and a `dataType`. The `dataType` defaults to `mixed`, and the set an editor offers an author is exactly those data-type lanes that carry a val… - [DYN-3: Dynamic port names are constrained and unique across the node](https://flowdrop.io/spec/rules/gr-dyn/dyn-3.md): A dynamic port name begins with an ASCII letter and continues with ASCII letters, digits or underscores. It must not be a reserved name, and it must be unique across the union of the node's dynamic in… - [DYN-4: An unconnected dynamic input resolves to null, with its key present](https://flowdrop.io/spec/rules/gr-dyn/dyn-4.md): A dynamic input port with nothing wired into it resolves to null, and its key is present in the node's resolved inputs. Nothing arriving on the wire is not the same as the port being absent. - [DYN-5: A declared parameter wins a name collision with a dynamic port](https://flowdrop.io/spec/rules/gr-dyn/dyn-5.md): Where a dynamic port's name is also the name of a parameter the node's processor declares, the declared parameter wins and the dynamic port is ignored. An author cannot shadow a declared parameter by … - [DYN-6: Dynamic ports carry no exposure state and are not addressable from outside](https://flowdrop.io/spec/rules/gr-dyn/dyn-6.md): Exposure is a property of a port the processor declares. A dynamic port is not declared, so there is nothing to hide and nothing to strip, and for the same reason nothing an outside caller can name. - [DYN-7: Dynamic port definitions are configuration, never a wireable input port](https://flowdrop.io/spec/rules/gr-dyn/dyn-7.md): The reserved parameter holding a node's dynamic port definitions is never a wireable input port. It appears in the node's configuration schema only where the node type marks it configurable, taking it… ## GR-MEM — MEM (Part I) - [MEM-1: Appending a tool result twice appends it once](https://flowdrop.io/spec/rules/gr-mem/mem-1.md): An agent loop that retries a step, or an orchestrator that re-delivers a result, must not double the conversation. The buffer decides by the tool call's id, so a repeat is a no-op rather than a second… - [MEM-2: An assistant turn is a duplicate only when every call it declares is known](https://flowdrop.io/spec/rules/gr-mem/mem-2.md): The assistant side of the same guard. Dropping a turn that declares one new call would lose that call, so a partial overlap is kept. - [MEM-3: A user turn heals tool calls that were never answered](https://flowdrop.io/spec/rules/gr-mem/mem-3.md): A crashed or interrupted loop leaves an assistant turn declaring a call with no result. Most providers reject that history outright, so the buffer closes the pair before the conversation moves on. - [MEM-4: Assembling messages concatenates in declared port order](https://flowdrop.io/spec/rules/gr-mem/mem-4.md): The node that joins several message sources into one list. Port order is the message order, and an unwired source adds nothing at all. - [MEM-5: Normalizing a conversation makes it sendable to a provider](https://flowdrop.io/spec/rules/gr-mem/mem-5.md): Stored history and provider history are not the same shape. Normalization is the one place that reconciles them, and it reports what it had to drop. - [MEM-6: A user-scoped memory read or write without a real user refuses](https://flowdrop.io/spec/rules/gr-mem/mem-6.md): A memory bucket keyed by identity is only meaningful when there is an identity. Degrading to a shared bucket is how one caller's conversation ends up in another's history, so the scope refuses instead… - [MEM-7: A refused scope reaches no storage backend](https://flowdrop.io/spec/rules/gr-mem/mem-7.md): The refusal is only worth having if every consumer honours it identically, and if nothing is written on the way out. - [MEM-8: A session's memory principal is its owner or nobody](https://flowdrop.io/spec/rules/gr-mem/mem-8.md): Memory follows the conversation, not whoever happens to be driving it this turn. An unowned conversation therefore has no user memory at all. - [MEM-10: A real tool result replaces the placeholder that stood in for it](https://flowdrop.io/spec/rules/gr-mem/mem-10.md): A healed message says a call was interrupted. If the result later arrives, the buffer must show the answer, not the guess. - [MEM-11: Both spellings of a tool call's id are read](https://flowdrop.io/spec/rules/gr-mem/mem-11.md): A buffer written from a raw provider payload spells the id differently from one written by the buffer itself. Reading only one spelling makes every guard blind to the other. - [MEM-12: Concurrent buffer appends are serialized, and never fatal](https://flowdrop.io/spec/rules/gr-mem/mem-12.md): Two branches appending at once would each read the buffer before the other's turn and write back a version missing it. Serializing the read-modify-write prevents that, but losing a turn is worse than … - [MEM-13: A session-scoped memory read or write without a real session refuses](https://flowdrop.io/spec/rules/gr-mem/mem-13.md): The other identity scope, closing the same leak. A session scope that degraded would splice every identity-less execution path into one shared conversation history. - [MEM-14: An append reports how much of it was new](https://flowdrop.io/spec/rules/gr-mem/mem-14.md): Every drop the buffer makes is silent, and the total message count looks identical across two identical passes. The delta is the only evidence a caller has that a turn actually landed. - [MEM-16: Text becomes a message under three closed rules](https://flowdrop.io/spec/rules/gr-mem/mem-16.md): The adapter every text producer needs to reach a message-shaped node. Its defaults are chosen so a malformed turn never reaches a provider. - [MEM-9: Driving a session is a write, and an absent identity owns nothing](https://flowdrop.io/spec/rules/gr-mem/mem-9.md): A turn spends the owner's memory, so the right to watch a conversation is not the right to continue it. And the caller with no identity is not a caller whose identity happens to be zero. - [MEM-15: A denial is final, and a message is not editable](https://flowdrop.io/spec/rules/gr-mem/mem-15.md): A denial by these access rules is final. Where an implementation offers extension points that contribute to an access decision, none of them may grant what these rules have denied. A session message i… ## GR-MAN — MAN (Part I) - [MAN-1: A workflow's launch inputs are declared, never inferred](https://flowdrop.io/spec/rules/gr-man/man-1.md): The launch surface is the author's decision, written down. If it were derived from what the nodes happen to expose, adding a parameter to a node would widen what the outside world may send. - [MAN-2: A manifest entry names an input and binds it to a port](https://flowdrop.io/spec/rules/gr-man/man-2.md): Three fields say what the input is called and where it goes; the rest is documentation for whoever calls the workflow. - [MAN-3: However the manifest was written, one validator refuses a bad entry](https://flowdrop.io/spec/rules/gr-man/man-3.md): An authoring surface can offer only the ports it knows are valid; an API caller submits whatever it likes. Both land on the same stored shape, so the refusal has to live in one place. - [MAN-5: The published contract carries structure and nothing else](https://flowdrop.io/spec/rules/gr-man/man-5.md): What a caller is told about an input is built from the bound port's own schema, reduced to the keys that describe the value's shape. Annotations are re-attached when the contract is read, not stored i… - [MAN-6: The author's words win over the node's](https://flowdrop.io/spec/rules/gr-man/man-6.md): A node's own annotation describes the port in general. The author describes what this workflow means by it, so the author's text is the one a caller sees. - [MAN-7: Only a flat default is published](https://flowdrop.io/spec/rules/gr-man/man-7.md): A default is published so a caller knows what happens if they omit the input. Anything with nested structure is dropped rather than half-published, and the rest of the fragment survives the drop. - [MAN-8: An entry that cannot be built is skipped, not fatal](https://flowdrop.io/spec/rules/gr-man/man-8.md): One broken entry must not cost the workflow its whole contract, so the build drops it and carries on. - [MAN-9: A workflow used as a node cannot build its own contract forever](https://flowdrop.io/spec/rules/gr-man/man-9.md): Workflows compose, so a contract build can walk into itself. The guard is per path, not per build, so an honest diamond still resolves. - [MAN-10: A declared input name wins over an internal node-keyed key](https://flowdrop.io/spec/rules/gr-man/man-10.md): The two input shapes can collide on the same key. The declared manifest is the workflow's public face, so it decides. - [MAN-11: A declared input delivers to the port its entry binds](https://flowdrop.io/spec/rules/gr-man/man-11.md): The manifest is a mapping, and this is the mapping it performs. - [MAN-12: The node-keyed input shape is internal and unreachable from outside](https://flowdrop.io/spec/rules/gr-man/man-12.md): Addressing a node and port directly bypasses the manifest entirely, which is the whole of the launch boundary. It stays available to the system's own callers and to nobody else. - [MAN-13: An undeclared launch key is refused, and its value never delivered](https://flowdrop.io/spec/rules/gr-man/man-13.md): Silently ignoring an input a caller believed in is the worst of the options: the run proceeds with the caller's intent missing and nothing said. - [MAN-14: Strictness polices the caller, never the stored manifest](https://flowdrop.io/spec/rules/gr-man/man-14.md): A caller can fix the key they sent. They cannot fix a manifest entry someone else stored, so failing their launch over it helps nobody. - [MAN-15: Launch inputs are checked in one fixed order and answered once](https://flowdrop.io/spec/rules/gr-man/man-15.md): A caller gets one problem to fix at a time, in the order that makes the next attempt useful: what you sent that does not exist, then what you did not send, then what is wrong with what you sent. - [MAN-16: The value check enforces type and enum, and declares the rest](https://flowdrop.io/spec/rules/gr-man/man-16.md): A published contract says more than the launch boundary enforces. That is a real distinction and worth stating, so a caller does not read an unenforced keyword as a guarantee. - [MAN-17: Declared outputs are collected by the same mapping, in reverse](https://flowdrop.io/spec/rules/gr-man/man-17.md): The output side of the manifest, with one distinction that matters: a node that produced null produced something, and is not the same as a node that produced nothing. - [MAN-18: The contract is rebuilt when the manifest changes, and versioned when it is](https://flowdrop.io/spec/rules/gr-man/man-18.md): Rebuilding is triggered by the manifest, not by the schemas underneath it, so a caller reads a contract that was published deliberately rather than one that drifts. - [MAN-19: A workflow declared asynchronous cannot be launched and waited on](https://flowdrop.io/spec/rules/gr-man/man-19.md): Asking to wait for a result the workflow has already said it will not deliver inline is a mistake worth catching before anything runs. - [MAN-20: The API maps a client interface onto the stored manifest](https://flowdrop.io/spec/rules/gr-man/man-20.md): What an editor calls an interface entry and what the server stores as a manifest entry are the same thing under two vocabularies. This is the mapping, including what it deliberately ignores. - [MAN-21: A contract entry states the port's lane as well as its schema](https://flowdrop.io/spec/rules/gr-man/man-21.md): A port's lane and its JSON Schema type are two different vocabularies. Answering one with the other made a contract entry contradict the port it was bound to. ## GR-LANG — LANG (Part I) - [LANG-1: Extraction engines query the context; transformation engines bind it](https://flowdrop.io/spec/rules/gr-lang/lang-1.md): The engines an author can choose split into two families, and the split decides what the surrounding data means to the expression they write. - [LANG-2: A node's expression engine must be one the implementation provides](https://flowdrop.io/spec/rules/gr-lang/lang-2.md): A node that embeds an expression selects its engine through the reserved `engine` parameter, whose permitted values are exactly the engine identifiers the implementation provides. A workflow naming an… - [LANG-3: An empty expression is not an error](https://flowdrop.io/spec/rules/gr-lang/lang-3.md): An empty expression is valid: it passes validation, and it produces a defined result rather than a failure. A mapper output whose expression is empty is null, whatever the engine. An extractor whose p… - [LANG-4: A context that is not a map reaches a transformation engine as `data`](https://flowdrop.io/spec/rules/gr-lang/lang-4.md): Where the evaluation context is not a map, a transformation engine receives it bound to the single variable `data`. - [LANG-6: Expression-language evaluation fails loudly](https://flowdrop.io/spec/rules/gr-lang/lang-6.md): An expression-language expression that cannot be parsed, that cannot be evaluated, or that references a name absent from the context fails: it raises an error and yields no value. - [LANG-7: Twig yields an escaped string and treats a missing variable as empty](https://flowdrop.io/spec/rules/gr-lang/lang-7.md): A Twig template always produces a string, HTML-escaped unless the value is piped through `raw`. A variable absent from the context renders as the empty string rather than failing. A template that cann… - [LANG-8: A property path that cannot be read yields null](https://flowdrop.io/spec/rules/gr-lang/lang-8.md): Property paths trade diagnosis for calm: nothing raises, and a reader cannot tell a missing value from a stored one. - [LANG-9: A JSONPath query never raises; it falls back to the caller's default](https://flowdrop.io/spec/rules/gr-lang/lang-9.md): JSONPath evaluation never raises. A query that is well-formed but matches nothing, and a query that cannot be compiled or cannot be run, both yield the default the caller supplied, null where none was… - [LANG-10: Expression-language validation is a syntax check only](https://flowdrop.io/spec/rules/gr-lang/lang-10.md): Validating an expression-language expression checks its syntax, with every identifier the expression mentions treated as declared. A reference to a name that will not exist when the workflow runs ther… - [LANG-11: Property-path validation accepts everything evaluation can resolve](https://flowdrop.io/spec/rules/gr-lang/lang-11.md): Validating a property path parses it with the same parser evaluation uses, after the same dot-to-bracket normalization. Every path evaluation could resolve therefore validates, and only a path that ca… - [LANG-12: A path that does not begin with `$` always passes JSONPath validation](https://flowdrop.io/spec/rules/gr-lang/lang-12.md): Under the jsonpath engine, validation checks a `$`-rooted query. A path that does not begin with `$` validates unconditionally. - [LANG-13: Twig validation is a compile check only](https://flowdrop.io/spec/rules/gr-lang/lang-13.md): Validating a Twig template compiles it. A template that compiles but fails only while rendering validates successfully. - [LANG-14: A leading `$` decides JSONPath from property path](https://flowdrop.io/spec/rules/gr-lang/lang-14.md): A path is a JSONPath query if and only if the string, once trimmed of surrounding whitespace, begins with `$`. Every other path is a property path. - [LANG-15: A single JSONPath match is unwrapped unless the path selects many](https://flowdrop.io/spec/rules/gr-lang/lang-15.md): Arity is read off the path, not off the result, so a query written to select many keeps a list shape even on the day it matches exactly one thing. - [LANG-16: A string context is parsed as JSON where it parses](https://flowdrop.io/spec/rules/gr-lang/lang-16.md): Where the evaluation context is a string, it is parsed as JSON and the parsed value is what the path queries. A string that is not valid JSON is used as-is. The string `null` parses to null, like any … - [LANG-17: Dot and bracket path segments are interchangeable everywhere](https://flowdrop.io/spec/rules/gr-lang/lang-17.md): A property path may be written with dot segments, bracket segments, or a mixture of the two. An implementation normalizes them to a single form before parsing, and every place it reads a property path… - [LANG-18: A trigger mapping value can be escaped as a literal](https://flowdrop.io/spec/rules/gr-lang/lang-18.md): In a trigger mapping, a value prefixed `literal:` or wrapped in matching single or double quotes is taken as the literal string it spells, and is not extracted from the context. The escape applies to … - [LANG-19: Extracting all matches for a property path always yields a list](https://flowdrop.io/spec/rules/gr-lang/lang-19.md): Extracting all matches for a property path yields a list: a single result is wrapped in a one-element list, and a null result or no match yields the empty list. - [LANG-21: A failed mapper expression fails the node and names the port](https://flowdrop.io/spec/rules/gr-lang/lang-21.md): When the expression for a mapper output fails, the node fails, the failure names the output port whose expression failed, and no partial output map is produced. - [LANG-22: An extraction error fails the node and never takes the default](https://flowdrop.io/spec/rules/gr-lang/lang-22.md): When an extraction fails (a path the engine cannot evaluate, an engine failure), the node fails and the failure routes to the error edge, naming the path and the engine. The configured default is not … - [LANG-23: Only a `$`-rooted JSONPath query unwraps a list to its first match](https://flowdrop.io/spec/rules/gr-lang/lang-23.md): An extractor reduces a list result to its first match only where its engine is jsonpath, the path is `$`-rooted, and extract-all is off. A property-path engine delivers a list result whole, even for a… - [LANG-24: A null extraction is a miss, not a failure](https://flowdrop.io/spec/rules/gr-lang/lang-24.md): Extraction engines cannot tell a stored null from no match at all, so the extractor treats both the same way, and only null, never a falsy value. - [LANG-25: Shaper sentinels resolve before any engine is consulted](https://flowdrop.io/spec/rules/gr-lang/lang-25.md): A shaper source is checked for sentinels before its engine is consulted. `_NOW_` yields the current timestamp in ISO 8601 form; `_NULL_` yields null; `_EMPTY_ARRAY_` yields the empty list; `_EMPTY_OBJ… - [LANG-26: A prompt template that fails to render fails the node](https://flowdrop.io/spec/rules/gr-lang/lang-26.md): When a prompt template fails to render (it cannot be compiled, it cannot be loaded, or it fails while rendering), the node fails and the failure routes to the error edge, preserving the original cause… - [LANG-27: A switch compares its value; it does not evaluate it](https://flowdrop.io/spec/rules/gr-lang/lang-27.md): Despite the parameter's name, a switch gateway runs no expression engine. The value is matched against each branch as it stands. - [LANG-28: A condition node's operators are a closed set, and case folding spares the pattern](https://flowdrop.io/spec/rules/gr-lang/lang-28.md): A condition node compares with exactly six operators (`equals`, `not_equals`, `contains`, `starts_with`, `ends_with` and `regex`) declared as an enumeration. A workflow naming an operator outside that… - [LANG-29: An expression error fails the node; an empty result does not](https://flowdrop.io/spec/rules/gr-lang/lang-29.md): One policy across every node that evaluates an author's expression, so a broken expression is visible and routable rather than silently absorbed. - [LANG-20: Testing whether a path exists](https://flowdrop.io/spec/rules/gr-lang/lang-20.md): The test answers for every path, including the two that have no obvious answer: one that cannot be walked, and one with nothing in it. ## RT-CMP — CMP (Part II) - [CMP-1: Compilation is one operation with one failure surface](https://flowdrop.io/spec/rules/rt-cmp/cmp-1.md): Compiling a stored workflow produces the executable plan in a fixed stage order. A caller that hands over an invalid workflow sees one kind of failure, whichever stage detected it. - [CMP-2: Compilation always re-enriches node metadata from the live node types](https://flowdrop.io/spec/rules/rt-cmp/cmp-2.md): Stored node metadata is a cache, and an author-editable one. Compilation refreshes it from the node types themselves so a run can never be planned against stale or attacker-supplied metadata. - [CMP-3: Structure preconditions are checked before anything is planned](https://flowdrop.io/spec/rules/rt-cmp/cmp-3.md): The cheapest checks run first, so a workflow that cannot possibly execute is rejected before any graph work happens. - [CMP-4: An edge's type is derived, never declared](https://flowdrop.io/spec/rules/rt-cmp/cmp-4.md): The type of an edge is derived from the handles it connects. An edge does not declare its own type, and a declared type on an edge is not authoritative. - [CMP-5: Only tool, loopback and agent-result cycles are legal](https://flowdrop.io/spec/rules/rt-cmp/cmp-5.md): Rejecting every other cycle is what makes the forward graph acyclic. Loop membership is defined by reachability sweeps over that acyclic graph, so loosening this rule would make loop extent ill-define… - [CMP-6: Which nodes reach the execution graph](https://flowdrop.io/spec/rules/rt-cmp/cmp-6.md): A loop head needs a forward entry edge: being reachable by loopback alone is not enough to be scheduled, even though every re-enterable node type carries a loopback port. - [CMP-7: Trigger dependencies displace data dependencies for ordering](https://flowdrop.io/spec/rules/rt-cmp/cmp-7.md): Where a node has both trigger dependencies and data dependencies, its execution dependencies are its trigger dependencies alone. Data dependencies order a node only when it has no trigger dependency. - [CMP-8: Execution order is a topological order, and no more than that](https://flowdrop.io/spec/rules/rt-cmp/cmp-8.md): Two nodes with no dependency between them may run in either relative order. An author who needs one before the other must say so with an edge. - [CMP-9: Tool names are unique per consumer, checked at compile time](https://flowdrop.io/spec/rules/rt-cmp/cmp-9.md): For each node that consumes tools, the names of the tools wired to it must be unique across the flattened set of leaf tools it will see. A collision refuses compilation. Passthrough tools are exempt f… - [CMP-10: A node with tools wired to it must be able to receive them](https://flowdrop.io/spec/rules/rt-cmp/cmp-10.md): A node that has tools wired to it must be of a type that accepts tools. Wiring a tool to a node type that cannot consume tools refuses compilation. - [CMP-11: The compiled plan keeps each node's node type identity](https://flowdrop.io/spec/rules/rt-cmp/cmp-11.md): For each node it will execute, the compiled plan records both the node type the node declares and the processor selected to run it. The runtime resolves a node's definition by node type identity. ## RT-ERR — ERR (Part II) - [ERR-1: A node-level failure becomes an error output, not a thrown failure](https://flowdrop.io/spec/rules/rt-err/err-1.md): The error edge is a data channel, not an exception channel. A node that fails in a routable way still finishes; the verdict travels on its output. - [ERR-2: Retryability is marked by presence, not by a boolean](https://flowdrop.io/spec/rules/rt-err/err-2.md): The absence of the marker is load-bearing: the retry gate tests for the literal value true, so anything else (including an explicit false) means do not retry. - [ERR-3: A failure that is not node-level fails the run](https://flowdrop.io/spec/rules/rt-err/err-3.md): A failure that is not converted into an error output escapes the node: the node is recorded as failed, no output exists for it, no error edge is followed, and the run fails. The failure reported to th… - [ERR-4: An interrupt propagates unchanged](https://flowdrop.io/spec/rules/rt-err/err-4.md): An interrupt is a request for something outside the run (human input, an external resolution), not a failure. Treating it as one would route it down an error edge. - [ERR-5: A node may stop the run successfully](https://flowdrop.io/spec/rules/rt-err/err-5.md): Stopping is a deliberate early finish, not a failure. Everything the run had not yet done is abandoned, and the run reports success. - [ERR-6: A node's output must be serializable throughout](https://flowdrop.io/spec/rules/rt-err/err-6.md): Checked over the node's whole output, before any unexposed value is filtered out, so a value the node type does not expose is policed exactly like one it does. - [ERR-7: An error edge replaces the node's output with an error envelope](https://flowdrop.io/spec/rules/rt-err/err-7.md): The envelope is the whole contract between a failing node and its handler: a handler can be written against it without knowing which node type failed. - [ERR-8: An error with nowhere to go fails the run](https://flowdrop.io/spec/rules/rt-err/err-8.md): Where a node produces an error output and has no error edge, its unit of work is recorded as failed without the error-routed marker, and the run fails. - [ERR-9: A run whose every failure was handled completes](https://flowdrop.io/spec/rules/rt-err/err-9.md): The verdict is computed from unhandled failures, not from failures. One unrouted failure fails the run no matter how many routed ones accompany it. - [ERR-10: A routed failure activates error edges only, and only when it is current](https://flowdrop.io/spec/rules/rt-err/err-10.md): Two independent staleness rules guard the handler, because a loop can both re-run a node and route a failure from an earlier round. - [ERR-11: Retry is opt-in, in place, and immediate](https://flowdrop.io/spec/rules/rt-err/err-11.md): There is no backoff and no delay, so a retry is only ever appropriate where the node is idempotent. That is the author's contract, not the runtime's. - [ERR-12: Direct synchronous execution has no error-handling divergence](https://flowdrop.io/spec/rules/rt-err/err-12.md): Withdrawn in favour of ERR-13. The rule promised that direct synchronous execution does not record an error output and continue where the other strategies route it; ERR-13 states the uniform requireme… - [ERR-13: Every execution strategy routes errors identically](https://flowdrop.io/spec/rules/rt-err/err-13.md): A workflow that handles its own failures must mean the same thing whichever engine runs it, including the direct synchronous one. ## RT-ORC — ORC (Part II) - [ORC-1: Four execution strategies, each with a stable identifier](https://flowdrop.io/spec/rules/rt-orc/orc-1.md): The same workflow can be run four ways. Which way is a deployment choice, not a property of the workflow. - [ORC-2: How the execution strategy for a run is resolved](https://flowdrop.io/spec/rules/rt-orc/orc-2.md): Every configured step is checked before it is honoured, so an engine that is configured but not available cannot capture the default and strand a run. - [ORC-3: An unknown strategy identifier falls back, and says so](https://flowdrop.io/spec/rules/rt-orc/orc-3.md): A request for a strategy identifier the implementation does not know falls back to the default strategy and records a warning. Where no fallback strategy is available either, the request fails rather … - [ORC-4: State graph is the interactive-session default only](https://flowdrop.io/spec/rules/rt-orc/orc-4.md): It is a caller's default, offered by the session surface, not a rung in the global chain, so a background run never silently becomes a state-graph run. - [ORC-5: A strategy's capabilities are declared, not asked for](https://flowdrop.io/spec/rules/rt-orc/orc-5.md): The declaration is the single source. A strategy that behaves statefully but does not declare it is treated as stateless, and the behaviours gated on the capability (checkpoint storage among them) are… - [ORC-7: Direct synchronous execution refuses a workflow that loops](https://flowdrop.io/spec/rules/rt-orc/orc-7.md): The engine walks the compiled order once and never re-enters a node, so it cannot iterate a loop. It refuses the workflow rather than running the body once and reporting success. - [ORC-8: Naming a firing trigger drops the other triggers](https://flowdrop.io/spec/rules/rt-orc/orc-8.md): Where a run's initial data names a trigger node, direct synchronous execution runs that trigger node and drops the workflow's other trigger nodes. Where it does not, the compiled order runs verbatim. … - [ORC-9: A pipeline engine is a ready-work loop](https://flowdrop.io/spec/rules/rt-orc/orc-9.md): Re-entering a node on a later pass is what makes loops possible at all, and the empty pass is what defines the end of the run. - [ORC-10: A run has a scheduler budget, and exhausting it pauses rather than fails](https://flowdrop.io/spec/rules/rt-orc/orc-10.md): This budget counts scheduler passes across the whole run. It is not a per-loop round count and not the hard safety valve; the three exist separately and resolve differently on resume. - [ORC-11: How a run's terminal status is decided](https://flowdrop.io/spec/rules/rt-orc/orc-11.md): A single ladder, in precedence order, so a run that both failed and paused reports the failure. - [ORC-12: Asynchronous execution returns immediately](https://flowdrop.io/spec/rules/rt-orc/orc-12.md): Asynchronous execution returns the status `queued` to its caller immediately and executes no node in the calling request. It may seed the run from a snapshot of already-completed work. - [ORC-13: Resuming after an interrupt re-enters through the run's own strategy](https://flowdrop.io/spec/rules/rt-orc/orc-13.md): When an interrupt is resolved, the run resumes through the strategy the run declared, never through a different one. - [INT-11: A terminal run refuses re-entry; re-running makes a new run](https://flowdrop.io/spec/rules/rt-orc/int-11.md): This is what makes cancellation durable: no requeue or rerun can resurrect a dead run and fire its side-effecting nodes again. - [INT-12: Only a budget pause resumes itself](https://flowdrop.io/spec/rules/rt-orc/int-12.md): The absence of a pause reason is the marker of a human pause. Do not give a human pause a reason: that is what tells the machinery to leave it alone. - [ORC-14: A unit of work is claimed atomically](https://flowdrop.io/spec/rules/rt-orc/orc-14.md): Without this, two workers can promote and execute the same unit of work, and a side-effecting node fires twice. - [ORC-15: A cancelled run is announced like any other terminal outcome](https://flowdrop.io/spec/rules/rt-orc/orc-15.md): Cancellation leaves the run through its own path, so it is easy to forget to announce. Anything watching for a run to finish must see a cancelled run finish. ## RT-BR — BR (Part II) - [BR-1: An edge is gated only when the source actually made a branch decision](https://flowdrop.io/spec/rules/rt-br/br-1.md): Gateways decide which paths stay alive, but most edges in a workflow are not branch edges at all. This rule says exactly when a branch decision is allowed to stop an edge, so that everything else keep… - [BR-2: The three cases in which a branch decision never gates an edge](https://flowdrop.io/spec/rules/rt-br/br-2.md): Read on its own, "follow the edge if its port is active" would strand every edge that has nothing to do with branching. These three cases are checked first and answer before the active-branch list is … - [BR-3: An active-branch list is a list of names, trimmed and lower-cased](https://flowdrop.io/spec/rules/rt-br/br-3.md): A source's active-branch list is a list of strings. A bare string is one branch name taken verbatim and is never split on commas or any other separator. Entries that are not strings are dropped. Survi… - [BR-4: An outcome matching no branch is loud, never a silent gate-off](https://flowdrop.io/spec/rules/rt-br/br-4.md): A gateway whose configured branches do not cover the value it produced used to emit a branch name no port carried, gating off everything downstream with nothing said. The target is that this cannot ha… - [BR-5: Branch gating applies to trigger and data edges alike](https://flowdrop.io/spec/rules/rt-br/br-5.md): Branch gating applies to an edge regardless of the kind of port it arrives at. A trigger edge and a data edge leaving the same gated port are both gated, on the same test. - [BR-6: A stale source does not satisfy an edge](https://flowdrop.io/spec/rules/rt-br/br-6.md): Inside a loop, a source can be completed and branch-active and still be speaking for the wrong round. Two independent staleness tests sit on every edge, and a source gated by either does not satisfy. - [BR-7: A node whose triggers are all unsatisfied is skipped, not executed](https://flowdrop.io/spec/rules/rt-br/br-7.md): This is the line the whole branching design rests on: an untaken branch does not merely fail to fire, it terminates cleanly, visibly, and without failing the run. ## RT-DATA — DATA (Part II) - [DATA-1: An edge delivers one named output port to one named input port](https://flowdrop.io/spec/rules/rt-data/data-1.md): An edge delivers a value from a named output port on its source node to a named input port on its target node. Both port names are taken from the edge's endpoint handles; an endpoint that names no por… - [DATA-2: Several sources on one port resolve to a single latest value](https://flowdrop.io/spec/rules/rt-data/data-2.md): A merge point in a workflow is not a collector. A port fed by three edges is still one port, and what a node reads there is one value. - [DATA-3: A trigger edge carries no data](https://flowdrop.io/spec/rules/rt-data/data-3.md): A trigger edge conveys only that its source completed. It delivers no value, and no input port on its target is ever filled by one. - [DATA-4: When a node is ready to run](https://flowdrop.io/spec/rules/rt-data/data-4.md): Readiness is the whole of a workflow's scheduling contract: it decides what runs, in what order, and what a node is guaranteed to have in hand when it does. - [DATA-5: Loopback and tool edges create no execution dependency](https://flowdrop.io/spec/rules/rt-data/data-5.md): An edge into the reserved `loop_back` input port, and an edge that wires a tool to a consumer, create no execution dependency. Neither is considered when the graph is checked for circular dependencies… - [DATA-6: Initial data fills a node's ports only where no edge did](https://flowdrop.io/spec/rules/rt-data/data-6.md): Initial data supplied with a run, keyed by node identifier, fills that node's input ports underneath anything an edge delivered: an edge-delivered value wins, including an edge-delivered `null`. An en… - [DATA-7: Inside a node, a wire outranks configuration outranks the default](https://flowdrop.io/spec/rules/rt-data/data-7.md): Within a node, a value delivered on an input port outranks the value the author configured on that node, which outranks the port's schema default. Presence is decided key by key at every level, so an … - [DATA-8: Unexposed outputs are stripped where they are produced](https://flowdrop.io/spec/rules/rt-data/data-8.md): A node's unexposed outputs are removed at the point the node produces them, before anything else observes the result: before the unified output port is composed, before the result is checked for seria… - [DATA-9: The unified output port carries exposed outputs only](https://flowdrop.io/spec/rules/rt-data/data-9.md): The unified `output` port composes a node's exposed outputs and nothing else. Keys prefixed with `_` and the reserved `trigger` key are excluded from it. An output key the node type does not configure… - [DATA-10: A tool edge binds a tool, it does not deliver data](https://flowdrop.io/spec/rules/rt-data/data-10.md): An edge on the reserved `tool_availability` port makes the source's tools available to its target as tool bindings. It is not a data delivery and fills no input port. Where a node forwards the tools i… - [DATA-11: A tool's model-facing schema hides the parameters the workflow already fixed](https://flowdrop.io/spec/rules/rt-data/data-11.md): The tool schema offered to a model omits every parameter the workflow has already decided: one pinned in the tool node's configuration, and one fed by an edge. The model is asked only for the paramete… - [DATA-12: Tools reach only a node that declares itself a tool consumer](https://flowdrop.io/spec/rules/rt-data/data-12.md): Tools are handed to a node only where the node type declares that it consumes them. Wiring tools to a node type that neither consumes nor forwards them is a validation error; a node type that forwards… ## RT-INT — INT (Part II) - [INT-1: An interrupt pauses the run and reports itself in full](https://flowdrop.io/spec/rules/rt-int/int-1.md): When a node stops to ask a question, the caller gets a complete answer about what happened: which question is outstanding, which run holds it, and everything that finished before it. - [INT-2: The interrupt a run reports is the persisted one](https://flowdrop.io/spec/rules/rt-int/int-2.md): An interrupt is persisted before the run reports it, and the identifier and status a caller sees are read from the persisted record, never from the in-flight signal that requested the pause, which car… - [INT-3: Resolving an interrupt resumes only a paused run](https://flowdrop.io/spec/rules/rt-int/int-3.md): Answering a question must never restart a run that is already moving. The reset and the resume are two steps in that order, which is what keeps two executors off the same run. - [INT-4: A node resumes only when all four ownership conditions hold](https://flowdrop.io/spec/rules/rt-int/int-4.md): Handing an answer to the wrong node, or to a node that never asked, is worse than asking again. So the fallback is a fresh execution, which at most re-asks. - [INT-5: Cancel and pause signals are observed between job iterations](https://flowdrop.io/spec/rules/rt-int/int-5.md): A signal never interrupts a job mid-flight. It is observed at the boundary between iterations, which is why a cancelled run has no half-executed node. - [INT-6: What a run snapshot contains, and that it never fails the run](https://flowdrop.io/spec/rules/rt-int/int-6.md): A snapshot is a best-effort record of progress. It is precise about what it carries, and it is never allowed to be the reason a run breaks. - [INT-7: The workflow version is a structural digest](https://flowdrop.io/spec/rules/rt-int/int-7.md): Two implementations must agree on whether a snapshot still fits its workflow. Moving a node on the canvas is not a change to the workflow it snapshots. - [INT-8: Snapshot validation reports named errors and non-fatal warnings](https://flowdrop.io/spec/rules/rt-int/int-8.md): Validating a snapshot against a workflow definition answers with codes a caller can act on, and draws a hard line between what invalidates a snapshot and what is merely worth saying. - [INT-9: One execution has at most one stored snapshot](https://flowdrop.io/spec/rules/rt-int/int-9.md): Storing a snapshot is an upsert keyed by execution identifier: an existing record for the same execution is overwritten in place, so one execution never accumulates two snapshot records. Cleanup delet… - [INT-10: A queued run seeded from a snapshot performs only the remaining work](https://flowdrop.io/spec/rules/rt-int/int-10.md): Handing a partially finished run to a queue must not re-fire what already ran. The seeded jobs are marked as borrowed, so nothing later mistakes them for work this run performed. - [INT-13: A checkpoint round-trip preserves cancellation](https://flowdrop.io/spec/rules/rt-int/int-13.md): Whether a run was cancelled or completed cannot be re-derived after the fact; "finished" looks identical either way. So the outcome is recorded, not inferred. - [INT-14: A terminal snapshot is not resumable](https://flowdrop.io/spec/rules/rt-int/int-14.md): A finished run stays finished. Resuming from a snapshot of one would re-fire side-effecting nodes against a run whose outcome is already recorded and already announced. - [INT-15: An engine that cannot use a snapshot refuses it, never restarts](https://flowdrop.io/spec/rules/rt-int/int-15.md): Silently discarding a snapshot and starting over looks like resilience and is the opposite: it re-runs every side effect the snapshot recorded as already done. - [INT-16: A refused signal says why in a code a client can act on](https://flowdrop.io/spec/rules/rt-int/int-16.md): Three different refusals share one status code, and a client has to tell them apart to say anything useful to an operator. The discriminator is a stable code, not the wording of a message. - [INT-17: A terminal outcome reaps the signals it never observed](https://flowdrop.io/spec/rules/rt-int/int-17.md): A signal is accepted whenever the run is still alive, but the run only looks at it between iterations. A run that finishes first leaves the request pending against a corpse. - [INT-18: An expired outward interrupt ends the run it was holding open](https://flowdrop.io/spec/rules/rt-int/int-18.md): An outward interrupt is the only thing that can resume the run that raised it. Once it expires the resume key is gone, and without this rule the run would sit paused forever: never terminal, never ann… - [INT-19: A machine answers an outbound wait through its own route](https://flowdrop.io/spec/rules/rt-int/int-19.md): A remote system cannot use the route a human uses; it has no session and so cannot present the token that route requires. It gets a route built for its threat model, and an authority that buys it noth… - [INT-20: Call-and-wait creates the interrupt before it makes the call](https://flowdrop.io/spec/rules/rt-int/int-20.md): The order is the rule. The callback address is built from the interrupt's identifier, so there is nothing to tell the remote until the interrupt exists, and a fast remote can answer before the outboun… - [INT-22: An interrupt is published as 24 keys, every one always present](https://flowdrop.io/spec/rules/rt-int/int-22.md): One shape, from every endpoint that publishes an interrupt. A consumer reads a key rather than testing whether it is there, because an absent value is present and null. - [INT-23: No expiry is a sentinel, and the two expiry paths differ on purpose](https://flowdrop.io/spec/rules/rt-int/int-23.md): An interrupt with no expiry waits indefinitely, and must survive every sweep. When one does expire, whether that ends the run depends on which path expired it, and that asymmetry is the rule, not an o… ## RT-GATE — GATE (Part II) - [RT-GATE-1: A gated node never executes without consent for that exact call](https://flowdrop.io/spec/rules/rt-gate/rt-gate-1.md): The gate sits at the single point every node execution passes through, so it covers a node scheduled in the graph and the same node invoked as a tool by an agent loop. The operator approves the resolv… - [RT-GATE-2: Consent is consumed exactly once, declines included](https://flowdrop.io/spec/rules/rt-gate/rt-gate-2.md): An approval authorises one execution. An agent loop re-issuing the same call is asked again every time, which is the point. - [RT-GATE-3: Consent binds to the node and its exact resolved arguments](https://flowdrop.io/spec/rules/rt-gate/rt-gate-3.md): The operator approved one call, not "this node from now on". If the arguments drift, the approval no longer describes what would happen. - [RT-GATE-4: A declined node routes a structured verdict out its error port](https://flowdrop.io/spec/rules/rt-gate/rt-gate-4.md): A decline is a real outcome an author can handle, delivered where every other node failure is delivered. It is deliberately not a pair of branch ports that appear and disappear with a configuration fl… - [RT-GATE-5: A declined tool call is model-recoverable, never run-fatal](https://flowdrop.io/spec/rules/rt-gate/rt-gate-5.md): Failing a whole agent run because a person said "no" to one tool call would be wrong. The model is told, and carries on. - [RT-GATE-6: A gate pause leaves no phantom failure in the tool trail](https://flowdrop.io/spec/rules/rt-gate/rt-gate-6.md): A pause is not a failure, and the record of a tool call must not say otherwise; a mislabelled attempt either holds a finished run open or trips the run's unhandled-failure check. - [RT-GATE-7: Whether a node asks is a governance decision, resolved in order](https://flowdrop.io/spec/rules/rt-gate/rt-gate-7.md): Requiring confirmation is a decision an administrator makes about a node type, not a property of the code that runs it. The executor's own declaration is only the fail-safe used when governance has no… - [RT-GATE-8: Governance can revoke a waiver already stored](https://flowdrop.io/spec/rules/rt-gate/rt-gate-8.md): A waiver was legal when the author stored it. Whether it is still permitted is asked at the moment the gate would fire, against the settings that hold now. - [RT-GATE-9: Dynamic escalation can add an approval but never remove one](https://flowdrop.io/spec/rules/rt-gate/rt-gate-9.md): Data flowing into a node (including arguments a model filled) may raise the bar for that execution. It may never lower it. - [RT-GATE-11: A gated node whose executor cannot be resolved never executes](https://flowdrop.io/spec/rules/rt-gate/rt-gate-11.md): Fail-closed here means error, not ask. Reading a missing executor as "no side effects" silently ungates the node; asking about a call that can never run trains operators to rubber-stamp. - [RT-GATE-12: A resolved secret never persists in the gate prompt](https://flowdrop.io/spec/rules/rt-gate/rt-gate-12.md): The prompt an operator reads is stored verbatim. A credential that was substituted into an argument must not be stored along with it, while the consent must still bind to the real call. - [RT-GATE-13: A gate question belongs to the initiator, and an ownerless one is findable](https://flowdrop.io/spec/rules/rt-gate/rt-gate-13.md): A question assigned to whoever happened to persist it (a background worker with no identity) matches nobody's inbox and is never answered. A run with no initiator at all needs somewhere for its questi… - [RT-GATE-14: Confirmation governance is its own grant](https://flowdrop.io/spec/rules/rt-gate/rt-gate-14.md): Being able to rename a node type must not imply being able to disarm its gate. And an actor who cannot see the setting must not be able to change it by saving the form it is hidden from. - [RT-GATE-15: A shipped side-effecting node type states its policy](https://flowdrop.io/spec/rules/rt-gate/rt-gate-15.md): "Has side effects" and "an operator should approve this" are two different questions that happen to coincide for an outbound call and diverge for a memory write. Leaving a shipped node type undecided … ## RT-TOOL — TOOL (Part II) - [RT-TOOL-1: A tool result carries artifacts alongside its data, never inside it](https://flowdrop.io/spec/rules/rt-tool/rt-tool-1.md): A tool can return something structured (a chart, a record, a file reference) that the model should not have to read as prose. It travels beside the result, not inside it. - [RT-TOOL-2: A tool node emits artifacts on a reserved key that never reaches the model](https://flowdrop.io/spec/rules/rt-tool/rt-tool-2.md): A tool node's output may carry the reserved `artifacts` key, a list of `{type, payload}` maps. It is a system channel, exempt from output-port exposure, and so survives whether or not the author expos… - [RT-TOOL-3: Artifacts leave a tool call on their own port, apart from the model's prose](https://flowdrop.io/spec/rules/rt-tool/rt-tool-3.md): A node that invokes tools surfaces every call's artifacts on a dedicated `tool_artifacts` output port, as a list of exactly `{type, payload, tool_call_id}` entries built key by key, so a tool can neit… - [RT-TOOL-4: A turn's artifacts are persisted on its message and reported on its result](https://flowdrop.io/spec/rules/rt-tool/rt-tool-4.md): The artifacts a run collected are persisted on the last assistant message that run wrote, once per run. A turn's result aggregates the persisted artifacts of every assistant message in the turn, so th… - [RT-TOOL-6: Artifacts are bounded once, at the point they are collected](https://flowdrop.io/spec/rules/rt-tool/rt-tool-6.md): An implementation bounds the artifacts one run may collect and enforces that bound at the single point of collection; nothing downstream re-checks what one run collected. It must accept a payload of a… - [RT-TOOL-7: A pause does not destroy artifacts, and does not deliver them twice](https://flowdrop.io/spec/rules/rt-tool/rt-tool-7.md): Artifacts produced before a run pauses are held durably against the turn that produced them rather than discarded, and are never exposed on the wire while they are held. The next write-back for the sa… - [RT-TOOL-8: A model's tool arguments are normalized against the tool's own schema](https://flowdrop.io/spec/rules/rt-tool/rt-tool-8.md): Models hand back arguments that are nearly right: a JSON array as a string, a value escaped twice. Normalization repairs exactly the cases the tool's own schema can vouch for, and leaves the rest for … - [RT-TOOL-9: A tool call executes at most once per run](https://flowdrop.io/spec/rules/rt-tool/rt-tool-9.md): A tool call has side effects. One node can be reached twice in a run (by a fan-in, or by a re-ask after a human approved a gated call), and the same batch of calls arrives with it. - [RT-TOOL-10: A tool-calling pass reports whether it did any work](https://flowdrop.io/spec/rules/rt-tool/rt-tool-10.md): An all-repeat pass returns a full list of tool messages and a successful outcome while having invoked nothing, so an agent loop gated on "messages non-empty" re-enters forever. This is the port that a… - [RT-TOOL-5: Artifact collection is opened and released per run](https://flowdrop.io/spec/rules/rt-tool/rt-tool-5.md): Collecting artifacts means holding payloads in memory, so the run that opens collection is the run that ends it, on every way out. ## RT-SG — SG (Part II) - [SG-1: State merges field by field, and a state value is never mutated](https://flowdrop.io/spec/rules/rt-sg/sg-1.md): A state update is merged into the current state field by field: `messages` appends, `data` and `metadata` merge key by key, and every other field is replaced. A field the update omits keeps its curren… - [SG-2: A node sees run state only where it asks for it, and none of it is persisted](https://flowdrop.io/spec/rules/rt-sg/sg-2.md): Three separate guarantees. The runtime input handed to a node carries the reserved internal names `__state__`, `__messages__` and `__data__`, plus `__iterator__` and `__current_item__` whenever the st… - [SG-3: A node's state update is applied through the reducers once it completes](https://flowdrop.io/spec/rules/rt-sg/sg-3.md): A node's `state_update` output is applied to the run state through the same field-by-field reducers as any other update, once the node has completed. It is a control output and is never removed by out… - [SG-4: A ForEach node initializes its iterator once and completes on an empty list](https://flowdrop.io/spec/rules/rt-sg/sg-4.md): A ForEach node initializes its iterator on the round where the state carries none, and requires items to iterate. A value that is not a list is wrapped as a single item, and the items are re-indexed s… - [SG-5: loop_back is a value on a ForEach node and a bare signal everywhere else](https://flowdrop.io/spec/rules/rt-sg/sg-5.md): On a ForEach node the reserved `loop_back` input carries the round's item result, standing in for an explicit `item_result` input wherever that is absent. On every other node type the reserved `loop_b… - [SG-6: Routing is decided per edge and dispatched per target](https://flowdrop.io/spec/rules/rt-sg/sg-6.md): Whether to follow an edge is decided edge by edge, but dispatch is per target node: several followed edges arriving at one target produce a single dispatch of that target, not one per edge. - [SG-7: A loop's budget counts rounds of the loop, not executions of a node](https://flowdrop.io/spec/rules/rt-sg/sg-7.md): Re-entry over a loopback edge happens only while the edge's branch is active and the loop still has budget: either its iterator has more items, or the number of rounds the loop has run is below the co… - [SG-8: A node executed more than once keys its results by occurrence](https://flowdrop.io/spec/rules/rt-sg/sg-8.md): A node's first execution in a run is reported under its bare node identifier; each later execution is reported under `{nodeId}:{n}`, where `n` is that node's zero-based count of prior executions in th… - [SG-9: An edge condition never decides routing](https://flowdrop.io/spec/rules/rt-sg/sg-9.md): A condition stored on an edge never gates dispatch: not on any edge, and not on a loopback edge, the one place dispatch genuinely decides. The edge is stored, the edge is followed, and a warning is re… - [SG-10: A loopback driven from the trigger port belongs to no branch](https://flowdrop.io/spec/rules/rt-sg/sg-10.md): Where a loopback edge's source is the reserved `trigger` port, the re-entry it drives is attributed to no branch. - [SG-11: Every node completion writes a checkpoint, and the run writes a last one](https://flowdrop.io/spec/rules/rt-sg/sg-11.md): A run writes a checkpoint as each node completes, each chained to the checkpoint before it, and a final checkpoint named `workflow_end` when the run ends. An implementation offers at least an ephemera… - [SG-12: Where a run's starting state comes from](https://flowdrop.io/spec/rules/rt-sg/sg-12.md): A run's starting state is resolved in a fixed order: an explicit initial snapshot; else the checkpoint the caller named (an identifier that resolves to nothing is a warning and the run starts fresh, w… - [SG-13: A run records the configuration it resolved and resumes on it](https://flowdrop.io/spec/rules/rt-sg/sg-13.md): A run records the execution configuration it resolved, including the thread it runs on and the checkpoint store it uses. A later resume or deferred launch of that run rebuilds its configuration from t… - [SG-14: Exceeding the iteration budget ends the run, it does not pause it](https://flowdrop.io/spec/rules/rt-sg/sg-14.md): A state update that would push the run's iteration count strictly past the configured maximum ends the run. The budget's own last iteration is allowed; only exceeding it ends the run. The run then rep… - [SG-15: A run reports the identifier of the run it created](https://flowdrop.io/spec/rules/rt-sg/sg-15.md): The execution identifier a run reports is the identifier of the run record the engine created, and it replaces any identifier the caller supplied. That identifier is the one a client polls for the run… - [SG-16: What a loop is (its body, and that it is keyed by its head)](https://flowdrop.io/spec/rules/rt-sg/sg-16.md): A loopback edge on a canvas draws a cycle, but the loop is the set of nodes that cycle actually turns. Nearly everything about rounds, budgets and staleness rests on this definition. - [SG-17: A loop's extent is fixed at launch and every execution carries its rounds](https://flowdrop.io/spec/rules/rt-sg/sg-17.md): A run computes its loops once, when it starts, and works from that result for the rest of its life: an author editing the workflow cannot redraw the body of a loop that is already several rounds deep.… - [SG-18: Nested loops keep separate rounds, tangled ones are merged](https://flowdrop.io/spec/rules/rt-sg/sg-18.md): Where one loop's body is a strict subset of another's, the two stay distinct and each carries its own round, ordered outermost first. Advancing an outer loop's round resets every loop nested inside it… - [SG-19: A source from an earlier round does not satisfy an edge](https://flowdrop.io/spec/rules/rt-sg/sg-19.md): Inside a loop, a completed node is not necessarily a current one. Without this, a consumer on round N happily reads the value its source produced on round N-1. - [SG-20: A data port no live producer can still fill fails its consumer](https://flowdrop.io/spec/rules/rt-sg/sg-20.md): The round barrier on its own turns a gateway that routes away from an in-loop source into a silent hang. This is the clause that makes it terminate, and the restrictions are what keep it from failing … ## RT-PIPE — PIPE (Part II) - [PIPE-1: Run status is read from persisted state, on two routes and one envelope](https://flowdrop.io/spec/rules/rt-pipe/pipe-1.md): The live picture of a run is whatever has been persisted for it, not what some in-memory tracker happens to remember. A poll therefore answers the same value to every caller, including one served by a… - [PIPE-2: The job status summary has one shape on every path](https://flowdrop.io/spec/rules/rt-pipe/pipe-2.md): A run's `job_status_summary` is `total` followed by one integer counter per defined job status, in a fixed order, with no status omitted. The same key set and order is published when the jobs are read… - [PIPE-3: Status vocabulary is guarded in the summary and raw in the node counts](https://flowdrop.io/spec/rules/rt-pipe/pipe-3.md): Two counters in the same payload count the same jobs on different terms, and a consumer that reads one as if it were the other draws the wrong conclusion. - [PIPE-4: Node statuses are keyed by workflow node id and collapse every iteration](https://flowdrop.io/spec/rules/rt-pipe/pipe-4.md): This is what an editor looks a badge up by, so the key has to be the id the canvas holds. A node that ran many times still has one entry, with the per-iteration picture inside it. - [PIPE-5: A run's jobs are the ones the run itself names](https://flowdrop.io/spec/rules/rt-pipe/pipe-5.md): Every surface that publishes jobs answers from the run's own list of them, so the full document, the job list and the job status summary can never disagree about which jobs a run has. - [PIPE-6: Authorization is decided before the handler, and travels with its cacheability](https://flowdrop.io/spec/rules/rt-pipe/pipe-6.md): A response cached for one principal must never be served to another. That holds only if the authorization decision and the cache metadata describing what it depended on stay together. - [PIPE-7: One job entry shape, published identically by every surface](https://flowdrop.io/spec/rules/rt-pipe/pipe-7.md): Three surfaces publish a job. They publish one key set in one order, because two copies of a formatter had already drifted apart once. - [PIPE-8: Published execution context is filtered, not forwarded](https://flowdrop.io/spec/rules/rt-pipe/pipe-8.md): Whatever an engine parks on a run would otherwise become public the moment it is written, and after one release it cannot be withdrawn without breaking consumers. - [PIPE-9: A timestamp is never fabricated](https://flowdrop.io/spec/rules/rt-pipe/pipe-9.md): A run that has not finished has no finish time, and a missing creation time is missing. Publishing "now" in place of either makes a consumer believe something that did not happen. ## RT-PLAY — PLAY (Part II) - [PLAY-1: Playground sessions and messages are addressed and published by UUID](https://flowdrop.io/spec/rules/rt-play/play-1.md): Every playground route parameter naming a session or a message is a UUID, and every `id` published (a session row's, a message row's, and a message row's `sessionId`) is that UUID, never an internal r… - [PLAY-2: Creating a session answers the same row the list publishes](https://flowdrop.io/spec/rules/rt-play/play-2.md): A client can insert a creation response straight into its list without re-fetching, which is what makes one shared row shape load-bearing rather than tidy. - [PLAY-3: The message poll has its own envelope, with the flags at the top level](https://flowdrop.io/spec/rules/rt-play/play-3.md): The three flags beside the data are what a polling client reads to decide whether to fetch again and whether the turn is over. Folding them into the shared pagination block would break every such clie… - [PLAY-4: One message row, three doors, base keys always present](https://flowdrop.io/spec/rules/rt-play/play-4.md): The message list, the single-message read and the send acknowledgement publish the same message row. Its base keys (`id`, `sessionId`, `role`, `content`, `timestamp`, `status`, `sequenceNumber`, `node… - [PLAY-5: The session list is ownership-scoped, and an emptied filter means none](https://flowdrop.io/spec/rules/rt-play/play-5.md): The failure mode this rules out is an explicit request for no sessions answered with every sibling session in the workflow. ## RT-SNAP — SNAP (Part II) - [SNAP-1: The snapshot API carries two casings, and acceptance is the lenient half](https://flowdrop.io/spec/rules/rt-snap/snap-1.md): Envelope keys are snake_case and the snapshot document inside them is camelCase, so `execution_id` and `executionId` are published by the same read one nesting level apart. Both are read by consumers;… - [SNAP-2: Snapshot access is decided on the snapshot, and absence answered first](https://flowdrop.io/spec/rules/rt-snap/snap-2.md): Reading or deleting a snapshot loads it by execution id and asks it for view or delete authority: a snapshot that does not exist is `404`, one the caller may not reach is `403`, and absence is answere… ## RT-META — META (Part II) - [META-1: The category list is the standard envelope, and its name is a machine name](https://flowdrop.io/spec/rules/rt-meta/meta-1.md): This is the list that fills an editor's node sidebar. A consumer that treats `name` as human-readable renders a machine name to an author. - [META-3: A failed read answers a fixed message and reports the real one](https://flowdrop.io/spec/rules/rt-meta/meta-3.md): The category door was the counter-example: it returned whatever the storage layer said and logged nothing, so a class name or a failed query was published to any caller who could read the list. - [META-4: The workflow schema door publishes a bare document](https://flowdrop.io/spec/rules/rt-meta/meta-4.md): A workflow's declared input and output ports, read by an editor before it can draw the workflow. It is not wrapped in the standard envelope, and its snake_case is as much a contract as the playground'… - [META-5: An unknown workflow's schema is a bare error document](https://flowdrop.io/spec/rules/rt-meta/meta-5.md): A schema read for a workflow that does not exist answers `404` with the body `{"error": "Workflow not found."}`: one key, no `success` key, and the trailing full stop part of the literal. Absence is a… - [META-6: The entity tag carries the body variant, not just the schema version](https://flowdrop.io/spec/rules/rt-meta/meta-6.md): A missing variant in the tag was a live cross-repo cache bug: a client holding the plain body asked for the annotated one, was told it was unchanged, and went on serving a document with every title an… - [META-7: A conditional schema request is exact string equality and nothing more](https://flowdrop.io/spec/rules/rt-meta/meta-7.md): A partial wildcard implementation would report "unchanged" to a client that holds no copy at all, which is worse than answering unconditionally. - [META-8: The two schema variants are cached on deliberately different terms](https://flowdrop.io/spec/rules/rt-meta/meta-8.md): The annotated document reattaches translated text from live plugins, so a shared copy would serve one interface language's annotations to a reader in another. - [META-9: The editor metadata doors are read-only and gated before the handler](https://flowdrop.io/spec/rules/rt-meta/meta-9.md): Neither door has a handler-side check to fall back on, so what the surface declares is the whole access contract. ## RT-OCX — OCX (Part II) - [OCX-1: External invocation builds flat initial data](https://flowdrop.io/spec/rules/rt-ocx/ocx-1.md): The payload has to arrive where the trigger's advertised output schema says it is. Keying it by node id put it somewhere only an expression naming that node could reach. - [OCX-2: External results are retrieved by polling, and only by polling](https://flowdrop.io/spec/rules/rt-ocx/ocx-2.md): A contract of omission: the connector dispatches nothing outbound. Widening it means taking on delivery, timeouts, retry and de-duplication, which belong elsewhere. - [OCX-3: Polls are scoped by a positive marker, defined once](https://flowdrop.io/spec/rules/rt-ocx/ocx-3.md): Scoping by the presence of a trigger configuration served cron, entity and form runs (output data included) to any external platform that asked. - [OCX-4: External invocation honours the configured pipeline identity and mode](https://flowdrop.io/spec/rules/rt-ocx/ocx-4.md): An externally invoked run resolves its pipeline identifier and pipeline mode from the trigger's orchestrator settings, exactly as every other trigger kind does. It must not construct an identifier of … - [OCX-6: The connector validates nothing it publishes](https://flowdrop.io/spec/rules/rt-ocx/ocx-6.md): The published payload schema is metadata for the calling platform to render and enforce. Enforcement belongs to that platform and to the workflow, not to the adapter in between. - [OCX-7: One execution identifier correlates the invocation with every poll](https://flowdrop.io/spec/rules/rt-ocx/ocx-7.md): This single identifier is what makes an asynchronous round trip work through a synchronous invocation signature. - [OCX-8: A poll reports every terminal run, cancellation included](https://flowdrop.io/spec/rules/rt-ocx/ocx-8.md): A poll reports runs in any terminal status, not only successful and failed ones (a cancelled run is terminal and is reported), so a polling caller always reaches an end state. A run that is genuinely … - [OCX-9: Polls page, and paging never strands the caller](https://flowdrop.io/spec/rules/rt-ocx/ocx-9.md): A first poll used to select every terminal run the installation had ever produced and load the lot, which is an out-of-memory failure rather than a slow query. - [OCX-5: A trigger's condition set is a closed vocabulary](https://flowdrop.io/spec/rules/rt-ocx/ocx-5.md): A connector with its own data to carry has one place to put it, so a reader can tell a declared condition from a connector's private key. ## RT-NET — NET (Part II) - [NET-1: An outbound URL is checked before the request, and the check is pinned to it](https://flowdrop.io/spec/rules/rt-net/net-1.md): Validating a name and then dialling the name is not a check: a hostile resolver answers publicly for the check and privately for the request. - [NET-2: Every redirect hop is re-validated before it is taken](https://flowdrop.io/spec/rules/rt-net/net-2.md): Pinning binds the original host only. Without this, a public host answering a redirect to a link-local metadata address walks straight past a guard that has already reported success. - [NET-3: The redirect posture is stated, not inherited](https://flowdrop.io/spec/rules/rt-net/net-3.md): These restate what most HTTP clients already default to, deliberately: the posture is a decision, not whatever the client happens to ship. ## RT-MD — MD (Part II) - [MD-1: Markdown source can never reach the reader as markup](https://flowdrop.io/spec/rules/rt-md/md-1.md): A markdown-to-HTML conversion usually runs over text a language model wrote, so the encoding is a safety boundary rather than a formatting nicety. Encoding the whole input once, before any block or in… - [MD-2: Encoded exactly once, and quoted where a quote would end an attribute](https://flowdrop.io/spec/rules/rt-md/md-2.md): The counterpart to encoding the whole input up front: nothing downstream may encode a second time, and the few places a value is interpolated into an attribute need the quote characters that the text … ## RT-CRON — CRON (Part II) - [CRON-1: A cleared schedule means not scheduled, and every reader says so](https://flowdrop.io/spec/rules/rt-cron/cron-1.md): A schedule is invisible until it fires or fails to, so the component that decides a trigger is due and the one that reports when it will next run must never describe the same stored value differently.… - [CRON-2: Every reason a schedule has no next run carries a stable code](https://flowdrop.io/spec/rules/rt-cron/cron-2.md): The report of a trigger's next run is the only view of a schedule an operator has. A report that cannot distinguish "fine, nothing due" from "will never fire" is the diagnostic being absent exactly wh… ## RT-TRIG — TRIG (Part II) - [TRIG-1: Overlap is judged against the workflow's own unfinished runs](https://flowdrop.io/spec/rules/rt-trig/trig-1.md): The overlap decision is made unattended, and a wrong answer is either a duplicated production run or a silently dropped one. The four policies are four genuinely different behaviours, and the buffer i… - [TRIG-2: Jitter is rolled once and never re-rolled](https://flowdrop.io/spec/rules/rt-trig/trig-2.md): Spreading trigger load must not become a way for a trigger never to fire. The delay is drawn on the pass that finds the trigger due and then held to, however many passes follow. - [TRIG-3: A skip is recorded, but it is not an execution](https://flowdrop.io/spec/rules/rt-trig/trig-3.md): "When did this last actually run" is the question a trigger's record exists to answer, and it has to survive any number of skips in a row. ## RT-ST — ST (Part II) - [ST-1: The status surface answered inside a success envelope](https://flowdrop.io/spec/rules/rt-st/st-1.md): Retired with the polled status surface. It answered with a `{success, data}` envelope on success and `{success: false, error}` on failure, alongside the HTTP status code. - [ST-2: An execution was reported by id and status](https://flowdrop.io/spec/rules/rt-st/st-2.md): Retired with the polled status surface. It reported an execution as `{execution_id, status}`, answered 404 for an execution it held no record of, and 500 where the reporting itself failed. - [ST-3: Node statuses passed through, and an untracked run was empty rather than absent](https://flowdrop.io/spec/rules/rt-st/st-3.md): Retired with the polled status surface. It passed the recorded node statuses through unchanged, and answered a request for an execution it held no record of with an empty map rather than a 404. - [ST-4: The detail view carried derived run metrics](https://flowdrop.io/spec/rules/rt-st/st-4.md): Retired with the polled status surface. Its detail view carried metrics (total, completed, failed and pending node counts and a total execution time), each derived from the node records it held. - [ST-5: An execution was reported with a fixed field set](https://flowdrop.io/spec/rules/rt-st/st-5.md): Retired with the polled status surface. An execution was reported with the fields status, workflow_id, node_count, start_time, end_time, total_execution_time and error, spelled in snake_case. - [ST-6: A node was reported with a fixed field set](https://flowdrop.io/spec/rules/rt-st/st-6.md): Retired with the polled status surface. A node was reported with the fields status, node_id, node_type, start_time, end_time, execution_time, error and output. - [ST-7: The status vocabulary was closed](https://flowdrop.io/spec/rules/rt-st/st-7.md): Retired with the polled status surface. Its status vocabulary was initialized, idle, running, completed, failed and interrupted. The vocabulary itself outlived the surface as the status set carried on… - [ST-8: An update naming an unknown run registered it rather than dropping it](https://flowdrop.io/spec/rules/rt-st/st-8.md): Retired with the polled status surface. An update naming an execution or a node it held no record of registered that execution or node instead of discarding the update, so a run started outside the re… - [ST-9: Each transition carried a payload defined for it](https://flowdrop.io/spec/rules/rt-st/st-9.md): Retired with the polled status surface. A transition to running carried the node type and start time, to completed the execution time and output size, to failed the error, and to interrupted the inter…