--- title: "Workflow interface" description: "Declaring a workflow's public contract — the named inputs and outputs a caller can rely on, independent of the canvas." source: https://flowdrop.io/docs/concepts/workflow-interface site: FlowDrop documentation --- # Workflow interface A workflow's **interface** is its declared public contract: the named inputs a caller must supply and the named outputs it gets back. It is what lets a workflow be invoked as a sub-workflow, an agent tool, an HTTP endpoint, or a host-side action, without the caller reverse-engineering the graph. This is a separate axis from [port exposure](/concepts/port-system). `exposed` controls whether a port is visible and wireable **inside** the canvas. The interface controls what is callable **from outside** the workflow entirely. The distinction mirrors Docker: `EXPOSE` documents a container's ports internally, while published ports are what the outside world actually reaches. A port can be exposed on the canvas without ever being published in the interface, and only exposed ports are eligible to be published at all — see [Rules](#rules) below. `Workflow.interface` is optional and additive. A workflow with no `interface` key behaves exactly as it does today — there is no declared contract, and nothing about existing workflows changes. ## Schema ```typescript interface PortBinding { nodeId: string; portId: string; } interface WorkflowInterfaceEntry { id: string; name?: string; description?: string; dataType: string; required?: boolean; // inputs only; ignored on outputs defaultValue?: unknown; examples?: unknown[]; // inputs only; author metadata, never validated schema?: object; // one port's JSON Schema fragment bindings: PortBinding[]; meta?: Record; } interface WorkflowInterface { inputs?: WorkflowInterfaceEntry[]; outputs?: WorkflowInterfaceEntry[]; } ``` | Field | Type | Description | | -------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------- | | `id` | `string` | Stable public name. Unique within its direction (`inputs` or `outputs`). See [Stable identity](#stable-identity). | | `name` | `string` | Display label for callers and UI. Defaults to `id` when absent. | | `description` | `string` | Human-readable summary of what this input or output represents. | | `dataType` | `string` | The port's **lane** — same vocabulary as [port data types](/concepts/port-system#built-in-data-types), not the JSON Schema type. See [Lane and schema](#lane-and-schema). | | `required` | `boolean` | Inputs only. Whether a caller must supply a value. Ignored on outputs. | | `defaultValue` | `unknown` | Default value used when a caller omits this input. | | `examples` | `unknown[]` | Example values a caller could supply — author metadata for docs and launch forms. Inputs only; never validated. | | `schema` | `object` | The port's JSON Schema fragment — one property's schema (`{ "type": "array" }`), not a whole object schema. See [Lane and schema](#lane-and-schema). | | `bindings` | `PortBinding[]` | The inner port(s) this entry resolves to. See [Bindings](#bindings). | | `meta` | `object` | Opaque, server-defined metadata. See [The `meta` passthrough](#the-meta-passthrough). | Array order in `inputs` and `outputs` is the caller-facing order — for example, the parameter order of a generated function signature or form. ## Lane and schema `dataType` and `schema` answer two different questions, and an entry carries both: - **`dataType` is the lane** — what the port *is*, and what it may connect to. It draws the handle's colour and decides edge compatibility. - **`schema` is the shape of the value** — a JSON Schema fragment for this one port. The two vocabularies overlap on `string`, `number`, `boolean` and `array`, which is why they are easy to confuse. They diverge exactly where it matters: ```json { "id": "history", "dataType": "messages", "schema": { "type": "array" }, "bindings": [{ "nodeId": "chat_1", "portId": "messages" }] } ``` A `messages` port is a JSON `array`; an `error` port is a JSON `object`. Reading `dataType` as a JSON Schema type gets you `array` and `object` — true, but not what the port is. Read `schema.type` when you want the JSON Schema word. `schema` carries the structural contract only. Anything the entry states itself — the lane, `name`, `description`, `examples`, `required` — is not repeated inside it. Both keys are **derived by the server** from the inner port each entry binds to, and both are **ignored on write**. Sending a different `dataType` does not change what the next read returns; the bound port decides. ## Bindings An interface entry does not point at an inner port directly by embedding a flag on it. Instead it owns a stable `id` and a separate `bindings` list of `{ nodeId, portId }` pointers into the graph. This indirection exists so that swapping or renaming a node does not silently break callers: the public `id` stays put even when the node underneath it changes. Every entry — input or output — resolves to exactly one binding. An entry with more than one binding is invalid. An entry with an **empty** `bindings` array is a valid, deliberate **draft** state: the contract slot exists, but nothing backs it yet. A binding carries no direction of its own. Which array the entry lives in decides the side of the node it points at: an entry under `inputs` binds to one of that node's input ports, an entry under `outputs` to one of its output ports. This matters because a `portId` only has to be unique within one side of a node — a node can declare both an input and an output named `message`, and the entry's direction is what distinguishes them. ```json { "id": "article_text", "name": "Article Text", "description": "The body text to summarize", "dataType": "string", "required": true, "bindings": [ { "nodeId": "text_input.1", "portId": "content" } ] } ``` ## Stable identity An entry's `id` is workflow-scoped and independent of whichever inner port currently backs it. This matters for two reasons: - **Node swaps and renames don't break callers.** If a node is swapped for another, or an inner port is renamed, only the `bindings` entry needs to change — the public `id` a caller depends on is untouched. - **On export to a server manifest, `id` becomes the manifest's wire identity** — the `name` a caller actually calls. Renaming an entry's `id` is therefore a **breaking change for callers**, not a cosmetic one, in the same way renaming a function parameter in a public API is. ## Rules - **Only canvas-exposed ports can be bound.** A port that isn't exposed is hidden, not wireable, and not runtime-overridable — publishing it in the interface would contradict that. The two axes compose in one direction only: the external interface is always a subset of what's exposed on the canvas (`external ⊆ internal`). - **Control-flow ports are not bindable.** Ports with the reserved `trigger` or `tool` data types, and a loop's `loop_back` port, are intra-graph orchestration rather than call signature. They never appear as binding candidates. - **Deleting a node does not shrink the contract.** If a node backing an interface entry is deleted, the entry's binding is left dangling rather than the entry being silently removed. A public contract can't shrink just because someone rearranged the canvas — a dangling entry is a visible problem to fix, not a quiet change to what callers can rely on. - **The interface is optional and additive.** Absence of `Workflow.interface` means the workflow declares no contract at all — not an empty one. Existing workflows, serialization, and exports are unaffected until an interface is added. ## The `meta` passthrough `meta` on a `WorkflowInterfaceEntry` is an opaque bag that the FlowDrop library never reads or interprets. It round-trips verbatim through load, save, and export. "Outside" doesn't mean one thing — a Drupal integration exposing a workflow as an action, an agent calling it over MCP, and a webhook accepting JSON all have different ideas of what publishing an input or output entails. The library's job is only to model *that* an interface exists; each server defines what "external" means for its own integration, using `meta` to carry that meaning: ```json { "id": "article_text", "dataType": "string", "bindings": [{ "nodeId": "text_input.1", "portId": "content" }], "meta": { "http": { "in": "query" }, "mcp": { "toolParam": true } } } ``` Keys under the `fd.` prefix are **reserved** for future use by the FlowDrop library itself. Don't use `fd.*` keys for your own server-specific conventions. ## What starts a run The interface describes a workflow's call signature — the parameters a run needs and the results it produces. It does not describe *what causes* a run to start. That's a separate, future concern: a trigger (a schedule, an incoming webhook, a host event) produces values that land in the interface's inputs, but a trigger is not itself an interface entry. A cron trigger might produce nothing at all; an "entity saved" event might produce a whole payload. The interface is the parameter list; a trigger is one of potentially several things that can fill it. ## Next Steps - [Port System & Data Types](/concepts/port-system) — the canvas-level `exposed` axis that interface bindings depend on - [Workflow Structure](/concepts/workflow-json) — where `interface` sits alongside `nodes`, `edges`, and `metadata` on the workflow document - [Node Structure](/concepts/node-json) — how nodes declare the ports that bindings point at