Action Type Inputs/Outputs for Flow Workflows — Design
Date: 2026-08-31 Status: Approved (design); pending implementation plan Area: Action Types, Apitomy Flow workflow integration
Summary
Axiom Action Types can be used as action nodes in an Apitomy Flow workflow, but a Flow action node
is defined in terms of typed inputs and outputs, and an Action Type currently has no first-class
way to declare either. Today the only machine-readable I/O contract on an Action Type is a single
free-text inputSchema string that is stored and round-tripped but never parsed, validated, or
enforced anywhere; there is no output concept at all. As a result Axiom's Flow SPI provider supplies
only value/label/description to the editor, and workflow authors must hand-author each node's
input bindings and output rows with no guidance.
This design gives Action Types an optional, structured way to declare inputs and outputs that aligns
1:1 with Flow's node contract, wires those declarations into the Flow editor, and plumbs them through
execution end-to-end (named input template binding on the way in, a validated JSON output contract on
the way out). It also adds a workflowEnabled opt-in flag so authors curate which Action Types appear
in the Flow palette.
Goals
- Let an Action Type optionally declare typed inputs and outputs.
- Feed those declarations into the Flow editor so node input-binding and output rows are auto-generated.
- Enforce the declared contract at runtime: validate resolved inputs before running, validate produced outputs after.
- Make each named input individually addressable in prompt/script templates.
- Auto-append the output contract to the agent prompt so authors do not hand-write "return JSON like…".
- Let authors curate which Action Types are eligible for use as workflow nodes.
- Preserve full backward compatibility: Action Types that declare nothing behave exactly as today.
Non-Goals
- No change to how Flow itself renders nodes, ports, or edges — we only supply data it already consumes.
- No dedicated MCP
set-outputtool; output capture uses a JSON result convention (see §3). - No nested/complex schema language (enums, nested objects) beyond Flow's flat type union.
- No retroactive breaking of published workflow definitions when a flag is later toggled off.
Background: current state
Established during design exploration (file references current as of this date):
- Action Type model — OpenAPI schemas
ActionType/NewActionType(common/api/src/main/resources/openapi.json), entitycore/src/main/java/io/apitomy/axiom/core/entities/ActionTypeEntity.java(@Table("action_type")), validatorcore/src/main/java/io/apitomy/axiom/core/services/ActionTypeValidator.java, REST implapp/src/main/java/io/apitomy/axiom/app/rest/ActionResourceImpl.java. The entity hasinputSchema(TEXT,input_schema) but no output field.labelsare persisted via an@ElementCollectionchild tableaction_type_label(migrationV35) — the precedent this design follows. - Flow contract —
@apitomy/flow-uiActionTypeDescriptorcarries optionalinputs?: ActionTypeField[]andoutputs?: ActionTypeField[], whereActionTypeField = { name, type: 'string'|'number'|'boolean'|'object', required?, description? }. - SPI gap —
ui/src/pages/WorkflowDefinitionDetailPage.tsximplements theEditorSpi.actionTypesprovider by mapping each Axiom Action Type to{ value, label, description }only;inputs/outputsare left empty, so the editor falls back to free-form authoring. - Execution bridge —
app/src/main/java/io/apitomy/axiom/app/WorkflowExecutionService.javaturns a waiting action node into aTaskEntitywhoseactionTypeis the node's action type name and whoseinputis the JSON of the engine-resolvedconfig.inputsmap;app/src/main/java/io/apitomy/axiom/app/TaskExecutionService.javaruns the task against the matchingActionTypeEntity(agent prompt viapromptTemplate/{{managerInput}}, or script viascriptTemplate). On completiontask.output(a string) is parsed into a map and returned asNodeResult.output, which the engine merges into workflow context for downstream nodes.
Design
1. Declaration model & API
Add two optional, ordered collections and one flag to the Action Type.
Fields
inputs: ActionTypeField[]— ordered list of declared inputs.outputs: ActionTypeField[]— ordered list of declared outputs.workflowEnabled: boolean— opt-in eligibility as a Flow workflow node (defaultfalse).
ActionTypeField shape (1:1 with Flow, so the SPI mapping is a passthrough):
| Field | Type | Notes |
|---|---|---|
name |
string (required) | Must be a valid identifier (usable as a template key / EL identifier). |
type |
enum (required) | string | number | boolean | object. Mirrors Flow exactly. |
required |
boolean | Defaults to false. |
description |
string | Optional. |
OpenAPI first (per the repo's contract-first rule): define an ActionTypeField schema and add
inputs, outputs, and workflowEnabled to both ActionType and NewActionType. Regenerate the
JAX-RS interfaces and beans. Remove inputSchema from both schemas.
Persistence — follow the action_type_label precedent:
- Map
inputsandoutputsas@ElementCollectionof an@EmbeddableActionTypeField, into child tablesaction_type_inputandaction_type_output, each with columns(action_type_id, ordinal, name, type, required, description). Use@OrderColumn(name = "ordinal")to preserve author order.@JoinColumn(name = "action_type_id")withON DELETE CASCADE. - Add
workflow_enabled BOOLEAN NOT NULL DEFAULT FALSEtoaction_type. - Drop the
input_schemacolumn.
Child tables (rather than a JSON TEXT column like environment) were chosen because author order is
meaningful and the fields benefit from being real, queryable rows; it also matches the existing label
pattern.
Validation — extend ActionTypeValidator:
- Within each of
inputsandoutputs: fieldnamerequired and a valid identifier;names unique within the list;typeis one of the allowed values. inputSchemavalidation (currently absent) is removed along with the field.
2. Flow editor wiring & palette curation
SPI provider (WorkflowDefinitionDetailPage.tsx): extend the ActionTypeDescriptor mapping to
populate inputs and outputs from the Action Type's new fields. Because the Axiom ActionTypeField
shape matches Flow's, this is a near-passthrough map ({ name, type, required, description }), not a
parse. Update the frontend ActionType interface in ui/src/config/api.ts to add inputs,
outputs, and workflowEnabled, and remove inputSchema.
Behavioral effect: once a node's config.actionType matches a descriptor that carries inputs,
Flow's PropertiesPanel auto-generates the per-input EL-expression binding fields and renders the
declared outputs read-only. Action Types that declare no inputs/outputs still fall back gracefully to
the free-form panel, so existing definitions are unaffected.
Palette curation (workflowEnabled): add a workflowEnabled filter to the existing
listActionTypes query (server-side, consistent with how executionMode and labels are already
filtered in ActionResourceImpl.listActionTypes). The SPI provider requests only workflow-enabled
Action Types, so the Flow palette shows only the curated subset.
Scope of the flag: workflowEnabled governs palette visibility only. A published workflow
definition that already references an Action Type by name keeps working even if the flag is later
turned off — the flag is not consulted at execution time and does not retroactively invalidate
existing definitions.
3. Execution: inputs in, outputs out
The data path already exists (resolvedInputs → task.input → prompt/script; task.output → map →
NodeResult). This design makes it named and validated. All new behavior is gated on the Action
Type actually declaring inputs/outputs; when it declares none, execution is byte-for-byte as today.
Inputs (into the action):
- The engine resolves the node's
config.inputs(name → EL expression) into aresolvedInputsmap, which Axiom serializes totask.input. This stays as-is on the wire. - In
TaskExecutionService, before running, validatetask.inputagainst the Action Type's declaredinputs: everyrequiredinput must be present; values are best-effort checked/coerced against the declaredtype. A missing required input (or type mismatch that cannot be coerced) fails the task fast with a clear message, which surfaces as aFAILEDnode and fails the run. - Template binding: expose each named input to
promptTemplate/scriptTemplateas{{inputs.NAME}}. The legacy whole-payload{{input}}/{{managerInput}}placeholders remain intact for user- and manager-triggered tasks that do not originate from a workflow.
Outputs (out of the action):
- Axiom auto-appends the output contract to the run instructions: for agents, an
Axiom-generated section is appended to the prompt instructing the agent to emit, as its final result,
a JSON object keyed by the declared output names; for scripts, the
scriptTemplateis expected to print that JSON object to stdout (or a known result file). Authors do not hand-write these instructions. - On completion, Axiom parses
task.outputas that JSON object and validates it against the declaredoutputs: required outputs present, types checked, extra keys allowed (lenient). A missing required output or malformed JSON fails the task (fail fast). - The validated map becomes
NodeResult.outputexactly as today, so the engine merges it into workflow context and downstream nodes bind to it through the EL expressions Flow already supports.
Failure strictness: fail fast on a missing required input, an uncoercible input type, missing required output, or malformed output JSON. No warn-and-continue mode.
Backward compatibility: Action Types with no declared inputs/outputs skip both validation steps
and behave exactly as today (whole-blob input, free-form string output). Non-workflow tasks are
unaffected.
4. Authoring UI
In the Action Type detail page (ui/src/pages/ActionTypeDetailPage.tsx, a PatternFly <Tabs>):
- Add the
workflowEnabledcheckbox on the Info tab, next to the existinguserTriggerable/managerTriggerablecheckboxes. - Add two new tabs, Inputs and Outputs, inserted in JSX order after Environment and before
the Prompt Template / Script tabs (PatternFly orders tabs by position; each gets its own
eventKey). Titles show counts for consistency with "Allowed Tools ({n})": Inputs ({n}) / Outputs ({n}). - Both tabs render only when
workflowEnabledis checked, using the same conditional-rendering pattern already applied to the mode-specific Prompt Template / Script / Allowed Tools tabs. UncheckingworkflowEnabledhides them. - Each tab hosts a repeatable row editor:
name,type(dropdown: string/number/boolean/object),required(checkbox),description, with add / remove / reorder (order persisted via@OrderColumn). - Client-side mirrors of the validator rules (unique names, valid identifier) provide immediate
feedback; the server-side
ActionTypeValidatorremains the source of truth (HTTP 422 on violation, matching the existing pattern). - Remove the old
inputSchemafield from the form.
5. Migration, import/export, seed data
- Flyway migration (next
Vnumber, H2-compatible and idempotent per the repo's migration constraints): createaction_type_inputandaction_type_outputchild tables (withordinalorder columns and cascading FKs toaction_type), addworkflow_enabled BOOLEAN NOT NULL DEFAULT FALSE, and dropinput_schema. No auto-conversion ofinput_schemadata (it is unenforced, H2-only/dev data); confirm no seed/import fixture depends on it before dropping. - Import/export (
ImportExportService): replaceinputSchemahandling withinputs,outputs, andworkflowEnabledso round-tripping stays complete. - Seed data (
SeedDataInitializer): audit for any Action Type that setsinputSchemaand migrate those to the structured fields; opportunistically mark obviously workflow-suited seed Action Types asworkflowEnabled.
Data flow (end to end)
- Author declares
inputs/outputson an Action Type and checksworkflowEnabled. - Flow editor requests workflow-enabled Action Types; the SPI provider returns descriptors carrying
the typed
inputs/outputs; the editor auto-generates the node's input-binding fields and read-only output rows. - At runtime the engine resolves the node's input expressions to a
resolvedInputsmap →task.input. TaskExecutionServicevalidatestask.inputagainst declared inputs (fail fast), binds each named input into the template as{{inputs.NAME}}, and auto-appends the output contract.- The agent/script emits a JSON object keyed by declared output names →
task.output. - On completion, Axiom validates the output JSON against declared outputs (fail fast) and returns it as
NodeResult.output; the engine merges it into workflow context for downstream nodes.
Backward compatibility
- Action Types with no declared inputs/outputs: unchanged execution and editor behavior.
workflowEnableddefaults tofalse; existing Action Types are simply absent from the Flow palette until curated, and existing published workflows that reference them by name continue to run.- Removal of
inputSchemais safe because it was never enforced; import/export and seed paths are updated in the same change.
Testing strategy
- Validator (JUnit 5): unique/valid input & output names, invalid
typerejected, required-field rules. - Persistence: round-trip an Action Type with ordered inputs/outputs; verify
@OrderColumnordering and cascade delete of child rows. - REST: create/update/get an Action Type with the new fields; 422 on validation violations.
- SPI mapping (frontend): descriptor carries mapped inputs/outputs; palette excludes
non-
workflowEnabledtypes. - Execution: input validation fail-fast on missing required input;
{{inputs.NAME}}binding; auto-appended output contract present; output JSON validated; malformed/missing required output fails the task; Action Types with no declared I/O bypass validation unchanged. - Migration: applies cleanly on H2; child tables and
workflow_enabledpresent;input_schemadropped.
Open questions
None outstanding — all design decisions resolved during exploration.