Skip to content

Latest commit

 

History

History
567 lines (432 loc) · 11.3 KB

File metadata and controls

567 lines (432 loc) · 11.3 KB

a3s-flow HTTP API Design

This document defines a REST-style HTTP surface for exposing a3s-flow through an Axum server.

Goals:

  • preserve the progressive API layering already present in FlowEngine
  • keep transport payloads close to the Rust API names
  • make dynamic node type management explicit and safe
  • support both editor-style clients and hosted workflow platforms
  • align the public route shape with the existing a3s-box API style

Box-aligned design principles

a3s-box exposes its UI/backend API as resource families under /api/box/*, for example:

  • /api/box/containers
  • /api/box/images
  • /api/box/networks
  • /api/box/volumes
  • /api/box/info
  • action routes such as /api/box/stop/:id and /api/box/pull/:ref

a3s-flow should follow the same transport philosophy:

  • use /api/flow/*, not a separate RPC-like namespace
  • group routes by resource family, not by abstract capability layer
  • use collection routes for snapshots and listings
  • use small action routes for lifecycle transitions
  • keep route names short and UI-friendly

The progressive aspect should exist in the resource model itself:

  1. catalog and validation
  2. execution creation and inspection
  3. event observation
  4. runtime control
  5. shared context mutation
  6. definition-backed execution

Base path

Recommended base path:

/api/flow

Examples below assume that prefix.

Error model

All non-2xx responses should use a stable JSON envelope:

{
  "error": {
    "code": "protected_node_type",
    "message": "node type is protected and cannot be removed: noop"
  }
}

Suggested error code mapping:

FlowError HTTP error.code
InvalidDefinition 400 invalid_definition
UnknownNode 400 unknown_node
ExecutionNotFound 404 execution_not_found
FlowNotFound 404 flow_not_found
InvalidTransition 409 invalid_transition
ProtectedNodeType 409 protected_node_type
Internal 500 internal
Terminated 409 terminated

Common response shapes

Execution state:

{
  "execution_id": "e6b7e7cf-3534-4d1b-b0d1-4e2c2788f905",
  "state": "running"
}

Terminal execution state:

{
  "execution_id": "e6b7e7cf-3534-4d1b-b0d1-4e2c2788f905",
  "state": "completed",
  "result": {
    "execution_id": "e6b7e7cf-3534-4d1b-b0d1-4e2c2788f905",
    "outputs": {
      "end": {
        "answer": "ok"
      }
    }
  }
}

Validation response:

{
  "valid": false,
  "issues": [
    {
      "node_id": "b",
      "message": "unknown node type 'does-not-exist'"
    }
  ]
}

Resource families

To stay consistent with a3s-box, the HTTP surface should be organized into these top-level families:

Family Purpose
/api/flow/info engine-level metadata and summary
/api/flow/nodes node catalog and runtime node type management
/api/flow/validate pre-flight validation
/api/flow/executions execution creation and snapshot lookup
/api/flow/events event streaming
/api/flow/context shared mutable execution context
/api/flow/definitions named/stored flow entry points

This gives a3s-flow the same “tabbed resource” feel as a3s-box.

L0 Discovery

GET /api/flow/info

Returns engine-level summary information plus the capabilities document.

Response 200:

{
  "engine": "a3s-flow",
  "version": "2026-03-22",
  "progressive_disclosure": true,
  "summary": "A3S Flow exposes a discoverable catalog of workflow node capabilities.",
  "node_count": 16,
  "capabilities": {
    "version": "2026-03-22",
    "progressive_disclosure": true,
    "summary": "A3S Flow exposes a discoverable catalog of workflow node capabilities.",
    "nodes": []
  }
}

GET /api/flow/capabilities

Returns engine.capabilities().

Response 200:

{
  "version": "2026-03-22",
  "progressive_disclosure": true,
  "summary": "A3S Flow exposes a discoverable catalog of workflow node capabilities.",
  "nodes": []
}

GET /api/flow/node-types

Returns a light-weight list of node type strings.

Response 200:

{
  "node_types": ["assign", "http-request", "llm", "noop"]
}

GET /api/flow/nodes

Returns engine.node_descriptors().

Response 200:

{
  "nodes": [
    {
      "node_type": "http-request",
      "display_name": "HTTP Request",
      "category": "integration",
      "summary": "Calls external HTTP APIs with configurable method, headers, and body.",
      "default_data": {
        "method": "GET",
        "url": "",
        "headers": {}
      },
      "fields": [
        {
          "key": "method",
          "kind": "string",
          "required": true,
          "description": "HTTP method."
        }
      ]
    }
  ]
}

L0.5 Runtime node type management

These endpoints control the engine's runtime node registry. They only affect future validations and executions.

Built-in node types are protected from deletion.

POST /api/flow/node-types

Registers or replaces a custom node type.

Because a dyn Node cannot be transported over HTTP directly, this endpoint is only suitable when the server supports a fixed set of server-side factories or plugins. The request identifies which server-known implementation to bind.

Request:

{
  "factory": "slow-test-node",
  "descriptor": {
    "display_name": "Slow Node",
    "category": "testing",
    "summary": "Sleeps briefly during tests.",
    "default_data": {
      "delay_ms": 10
    },
    "fields": [
      {
        "key": "delay_ms",
        "kind": "number",
        "required": false,
        "description": "Sleep duration in milliseconds."
      }
    ]
  }
}

Response 201:

{
  "node_type": "slow",
  "registered": true,
  "replaced": false
}

Notes:

  • factory is a transport-facing identifier resolved by the server
  • the server decides the actual Rust Node implementation and final node_type
  • if descriptor is omitted, the server may use the node's built-in metadata

DELETE /api/flow/node-type/:node_type

Deletes a runtime-registered node type.

Response 200:

{
  "node_type": "slow",
  "removed": true
}

Response 409 when attempting to delete a built-in type:

{
  "error": {
    "code": "protected_node_type",
    "message": "node type is protected and cannot be removed: noop"
  }
}

Response 404 when the type does not exist:

{
  "error": {
    "code": "node_type_not_found",
    "message": "node type not found: slow"
  }
}

Recommendation:

  • prefer 404 instead of 200 { removed: false } for HTTP clients
  • keep the Rust API permissive, but make the HTTP contract explicit

This follows the same singular-resource delete style that a3s-box uses with routes like /api/box/container/:id and /api/box/image/:ref.

L1 Pre-flight

POST /api/flow/validate

Request:

{
  "definition": {
    "nodes": [
      { "id": "a", "type": "noop" }
    ],
    "edges": []
  }
}

Response 200:

{
  "valid": true,
  "issues": []
}

L2 Execution

POST /api/flow/executions

Starts an inline flow definition.

Request:

{
  "definition": {
    "nodes": [
      { "id": "a", "type": "noop" }
    ],
    "edges": []
  },
  "variables": {
    "user_id": "u_123"
  }
}

Response 202:

{
  "execution_id": "e6b7e7cf-3534-4d1b-b0d1-4e2c2788f905",
  "state": "running"
}

GET /api/flow/execution/:id

Returns the latest execution snapshot.

Response 200:

{
  "execution_id": "e6b7e7cf-3534-4d1b-b0d1-4e2c2788f905",
  "state": "paused"
}

For box-style consistency, the collection route is plural and the single-item snapshot route is singular.

L3 Streaming

GET /api/flow/events/:id

Streams FlowEvent values.

Recommended transport:

  • Server-Sent Events for browser and CLI clients
  • WebSocket optional if bidirectional interaction is needed later

SSE event examples:

event: flow.started
data: {"execution_id":"e6b7e7cf-3534-4d1b-b0d1-4e2c2788f905"}
event: node.completed
data: {"execution_id":"e6b7e7cf-3534-4d1b-b0d1-4e2c2788f905","node_id":"a","output":{}}
event: flow.completed
data: {"execution_id":"e6b7e7cf-3534-4d1b-b0d1-4e2c2788f905","result":{"outputs":{"a":{}}}}

This keeps events as their own resource family instead of nesting them under execution resources. That matches the flat, UI-oriented style used by the existing a3s-box API.

L4 Runtime control

POST /api/flow/pause/:id

Response 200:

{
  "execution_id": "e6b7e7cf-3534-4d1b-b0d1-4e2c2788f905",
  "state": "paused"
}

POST /api/flow/resume/:id

Response 200:

{
  "execution_id": "e6b7e7cf-3534-4d1b-b0d1-4e2c2788f905",
  "state": "running"
}

POST /api/flow/terminate/:id

Response 202:

{
  "execution_id": "e6b7e7cf-3534-4d1b-b0d1-4e2c2788f905",
  "state": "terminating"
}

Action routes are intentionally short and command-like here because that is how a3s-box exposes lifecycle operations such as /api/box/stop/:id.

L5 Shared context

GET /api/flow/context/:id

Response 200:

{
  "context": {
    "approval": "granted"
  }
}

PUT /api/flow/context/:id/:key

Request:

{
  "value": "granted"
}

Response 200:

{
  "key": "approval",
  "updated": true
}

DELETE /api/flow/context/:id/:key

Response 200:

{
  "key": "approval",
  "removed": true
}

L6 Named flows

POST /api/flow/run/:name

Request:

{
  "variables": {
    "topic": "ai infra"
  }
}

Response 202:

{
  "execution_id": "e6b7e7cf-3534-4d1b-b0d1-4e2c2788f905",
  "state": "running",
  "flow_name": "daily-briefing"
}

This mirrors a3s-box action naming more closely than /definitions/:name/executions.

Handler mapping

Suggested internal handler-to-engine mapping:

HTTP handler FlowEngine call
GET /info capabilities() + local summary
GET /capabilities capabilities()
GET /node-types node_types()
GET /nodes node_descriptors()
POST /node-types register_node_type[_with_descriptor]() via server plugin registry
DELETE /node-type/:node_type unregister_node_type()
POST /validate validate()
POST /executions start()
GET /execution/:id state()
GET /events/:id start_streaming() or server-side subscription bridge
POST /pause/:id pause()
POST /resume/:id resume()
POST /terminate/:id terminate()
GET /context/:id get_context()
PUT /context/:id/:key set_context_entry()
DELETE /context/:id/:key delete_context_entry()
POST /run/:name start_named()

Open questions

  1. Runtime registration over HTTP requires a server-side plugin/factory registry because arbitrary Rust Node trait objects cannot be uploaded directly as JSON.
  2. Event streaming for an existing execution may require the server to keep a live subscription registry if clients attach after start().
  3. If named-flow storage becomes mutable over HTTP later, that should likely be a separate resource family:
/v1/flow/definitions
  1. Authentication and multi-tenant isolation should be applied at the router layer, not embedded into flow payloads.