Build An Extension
Flow Steward extensions are YAML-defined provider bundles with an isolated runtime entrypoint. An extension can add external system integrations, custom workflow triggers, builder operations, tools, skills, and operator UI pages without importing provider code into the Flow Steward server process.
Core does not bundle a reference extension. Start with the CLI scaffold, this
guide's small provider-free example, or the
Example Showcase extension, which demonstrates
every extension feature in one bundle. Extension IDs such as
com.acme.crm-sync map to folders such as com_acme_crm_sync (dots and hyphens
become underscores). For coding-agent assistance, install the
Extension Authoring Skill.
An extension is also the place for custom code in workflows. Flow Steward has no inline code step: write the logic in an extension, expose it as an action, and call it from a workflow with the Extension tools step. See Custom code.
What An Extension Contains
A complete extension usually has these parts:
| Area | Purpose |
|---|---|
extension.yaml | Runtime manifest. Declares identity, kind, features, entrypoints, scopes, host contract, network destinations, schemas, UI, actions, queries, callbacks, events, workflow triggers, and optional Contract v2 refs. |
icon.png | Required logo next to extension.yaml: a square PNG, 32–512 px on a side (128×128 recommended), at most 100 KB. The manifest has no icon field; validate, bundle and install refuse a bundle without a valid one. extensions create writes a default. |
extension-descriptor.yaml | Distribution descriptor. Used for marketplace or private import metadata, published artifacts, runtime compatibility, provenance, and commerce metadata. |
schemas/*.schema.yaml | JSON Schema documents written in YAML. They define configuration, secrets, subscription settings, action inputs, and result payloads. |
ui/ui_manifest.yaml | UI root manifest. Lists extension pages and imports page files. |
ui/pages/*.yaml | Operator-facing pages. Pages compose components and bind them to queries and actions. |
ui/components/*.yaml | Reusable component definitions such as forms, grids, health panels, callback setup panels, and markdown blocks. |
ui/actions/actions.yaml | Action operation catalog. Defines command-style operations the UI or workflows can invoke. |
ui/queries/queries.yaml | Query operation catalog. Defines read-style operations used by UI components and runtime callers. |
contracts/*.yaml | Extension Contract v2 fragments for builder operations, connection types, resource queries, step UI, event bindings, and artifact policies. |
locales/<language>.yaml | Optional translations of the extension's readable text: flat key: text maps. |
artifacts/ | Optional workflows, agents, skills, tools, datasets and saved queries published through extension-descriptor.yaml. |
main.py | Runtime entrypoint. Reads one JSON request from stdin and writes one JSON response to stdout. |
health.py | Healthcheck command. Verifies imports, files, and provider readiness. |
handlers/ | Extension-owned backend logic. Split provider operations into modules here when needed. |
requirements.in | Optional direct Python dependency input used by extensions lock-deps. |
wheels/ | Bundled runtime dependency wheels (Linux x86_64 and aarch64) for offline install/update. Filled by bundle --with-wheels. |
requirements-dev.txt, dev-wheels/ | Test-only dependencies for extensions test. dev-wheels/ is never packaged. |
scripts/smoke_check.py | Fast executable smoke script for local and CI sanity checks. |
tests/ | Pytest regression suite for compilation, descriptor validation, contract validation, runtime behavior, and integration safety. |
Flow Steward extension authoring files are YAML-only. JSON is still used for runtime stdin/stdout envelopes, REST payloads, database JSONB fields, CLI JSON output, and fixtures.
Choose a reference
The contract authority is the target application's
docs/standards/extension-manifest-standard.yaml together with its compiler and
validators. Two references follow it:
| Reference | Use it for |
|---|---|
| Acme CRM Sync example | The smallest valid bundle: a deterministic task provider with one page, three actions, one query and a smoke check. No dependencies, no network. |
| Example Showcase extension | Every extension feature in one synthetic bundle: UI pages, forms, actions, queries, settings and secrets, OAuth, Contract v2 operations, a workflow trigger, event bindings, callbacks, tools, workflows with a hook, health, translations, tests. |
Core ships neither in its extensions/ folder.
Extension development belongs in a Development source environment with Docker available, not in a Compact runtime/admin container. See Local Development.
Example Showcase Extension
The Example Showcase (flowsteward.example-showcase) is a reference bundle for
extension authors. It is synthetic: it calls no real service unless you switch
its mock mode off. Install it from Extensions › Marketplace, or read and
copy its source:
git clone https://github.com/Flow-Steward/ext-example-showcase.git
Browse it online at github.com/Flow-Steward/ext-example-showcase. Its README explains each feature in detail. What it shows and where:
| Feature | Showcase files | What to look at |
|---|---|---|
| Runtime manifest, host contract, network destinations | extension.yaml | kind: tool_provider, features tool, action, query, workflow_trigger, artifact_bundle; runtime.contract_version: extension_host_v2; egress hosts and connection fields; optional dependency on another extension. |
| Logo | icon.png | 128×128 PNG next to the manifest. |
| UI pages | ui/ui_manifest.yaml, ui/pages/*.yaml | Four pages: Overview, Configuration, Operations, Demo Artifacts. |
| UI components | ui/components/*.yaml | markdown, health_panel, stats_row, callback_setup, form (settings, secrets, test request, batch, notification) and grid (workflows, agents, tools, operations). |
| Settings and secrets | schemas/config.schema.yaml, schemas/secrets.schema.yaml, schemas/subscriptions.schema.yaml | Non-secret settings with a conditional rule (allowed_hosts is required when mock_mode is off); password, API token and webhook secret stored as secrets and never echoed. |
| Queries | ui/queries/queries.yaml, handlers/queries.py | provider_health, load_connection_settings, connection_status, list_demo_workflows, list_demo_agents, list_demo_tools, list_example_resources, operation_contracts. |
| Actions | ui/actions/actions.yaml, handlers/actions.py | UI-only save_connection_settings, save_secrets; workflow-visible test_external_request, send_review_notification, batch_score_records, run_demo_tool, invoke_tool, oauth_profile. |
| Workflow operations (Extension tools step) | contracts/operation_manifest.yaml, contracts/step_ui_manifest.yaml, contracts/resource_query_manifest.yaml | One Contract v2 operation per workflow-visible action: a source, a human_channel, a bulk_action with batching, and action operations; step form fields and resource pickers. |
| Connections and OAuth | contracts/connection_types.yaml | An API connection and an OAuth2 connection; oauth_profile receives only the access token. |
| Artifacts produced by operations | contracts/artifact_policies.yaml | Inputs/outputs an operation may read or write as files. |
| Workflow trigger and event bindings | workflow_triggers in extension.yaml, contracts/event_bindings.yaml | A provider trigger type with its config form; platform events (approval.requested, job.completed, interaction.received) bound to operations. |
| Callbacks | ui/components/callback_setup.yaml | Webhook registration panel and HMAC signing rules. |
| Tools | artifacts/tools/*.yaml, demo_workflow_tools.py | Deterministic tool artifacts (content, weather, product catalog) run through invoke_tool, including writing a file artifact. |
| Workflows, agents, skills, datasets | artifacts/, extension-descriptor.yaml | 14 demo workflows (branching, HTTP, child workflow, For each, Groups and parallel lanes, error route, hook), 5 agents, 5 skills, a dataset and a saved query. |
| Hooks (extension points in a shipped workflow) | artifacts/workflows/demo_order_with_client_hook.yaml | A protected author workflow with a hook where each project adds its own steps. |
| Health | health.py | Loads each runtime module and reports per-file checks. |
| Translations | locales/en.yaml, locales/lt.yaml | Flat key maps derived from ids. |
| Tests and scripts | tests/, .github/scripts/, release/checks.py | Pytest suites for the bundle, descriptor, demo tools and test wheels; smoke check, packaging and catalog check scripts; release-candidate checks over the public API. |
The showcase has no third-party runtime dependencies, so it has no wheels/
folder. For a real extension that bundles wheels for both architectures, see
the File Transfer extension
(github.com/Flow-Steward/ext-file-transfer-sftp).
Create A Scaffold
Create a new extension with the tooling CLI from the Flow Steward source tree:
./flow-steward extensions create com.acme.crm-sync \
--display-name "Acme CRM Sync" \
--description "Connects Acme CRM records to Flow Steward workflows." --confirm
The public scaffold starts as a tool_provider with tool/action features and a hidden save_settings action, not a task-ingress provider. It is intentionally small. It gives you the expected file layout,
minimal manifests, healthcheck, runtime entrypoint, smoke script, and pytest
test. After scaffolding, change the kind, features, schemas, UI, and handlers
to match the provider you are building.
Use these commands throughout development:
./flow-steward extensions validate com.acme.crm-sync
./flow-steward extensions test com.acme.crm-sync
./flow-steward extensions bundle com.acme.crm-sync --confirm
--confirm explicitly authorizes the shown local create/package change in a noninteractive session. In the reviewed native authoring CLI, an unconfirmed change can return review exit 10; it is not success, and installed launchers can reject earlier with different codes. Successful commands return 0. Stop on failed validation or testing before packaging.
When the extension needs third-party Python libraries, add direct pins to
requirements.in, lock the full wheel/hash set, and bundle wheels:
./flow-steward extensions lock-deps com.acme.crm-sync --confirm
./flow-steward extensions bundle com.acme.crm-sync --with-wheels --confirm
Naming Rules
The extension_id is the stable public id for the extension. Use a lowercase
dot-separated id:
extension_id: com.acme.crm-sync
The extension folder name must be derived from the id by replacing . and -
with _:
extensions/com_acme_crm_sync/
The validator rejects a bundle when the folder name does not match the canonical id. This prevents ambiguous package roots and avoids collisions when extensions are installed from zip archives.
Runtime Manifest
extension.yaml is the contract Flow Steward compiles and validates before an
extension can be enabled.
A complete minimal bundle, the Acme CRM Sync example, is published with this
documentation. Its ten files, all under /examples/com_acme_crm_sync/:
| File | Open or download |
|---|---|
extension.yaml | extension.yaml |
icon.png | icon.png |
main.py | main.py |
health.py | health.py |
requirements-dev.txt | requirements-dev.txt |
scripts/smoke_check.py | scripts/smoke_check.py |
ui/ui_manifest.yaml | ui/ui_manifest.yaml |
ui/pages/overview.yaml | ui/pages/overview.yaml |
ui/actions/actions.yaml | ui/actions/actions.yaml |
ui/queries/queries.yaml | ui/queries/queries.yaml |
Get and verify the example
Save each file at the same relative path under
extensions/com_acme_crm_sync/ in your Flow Steward source checkout. The folder
name must be exactly com_acme_crm_sync. Instead of copying files one by one,
you can start from the full Example Showcase
with git clone.
The --root value is the directory containing com_acme_crm_sync/, not the
extension folder itself. Validate, run the provider-free smoke check, and
bundle. Run each command separately and stop after any nonzero exit; a
successful bundle does not prove that validation or tests passed.
./flow-steward extensions validate com.acme.crm-sync --root extensions --health
./flow-steward extensions test com.acme.crm-sync --root extensions --allow-no-pytest
./flow-steward extensions bundle com.acme.crm-sync --root extensions --output-dir dist/extensions --confirm
The example has no tests/ folder, so extensions test needs
--allow-no-pytest; add pytest tests before you build on it.
Then, on a disposable Flow Steward installation, install the archive
dist/extensions/com.acme.crm-sync-0.1.0.zip with Extensions › Install
extension from file (see Bundle And Install), enable
it, run Health, and disable and uninstall it when you are done. Uninstall
shows its plan first; see the
Extension Operator Guide.
The exact published manifest is:
manifest_version: 2
lifecycle: {data_schema_version: 1, data_migrations: []}
extension_id: com.acme.crm-sync
kind: task_provider
version: 0.1.0
display_name: Acme CRM Sync
description: Minimal deterministic task-provider example for the public extension tutorial.
vendor: {name: Acme Example}
features: [task_ingress]
required_scopes:
- extension:invoke
- event:subscribe
event_subscriptions:
- event_type: job.completed
severity: info
entrypoint:
mode: subprocess
command: [python3, main.py]
health:
command: [python3, health.py]
timeout_seconds: 5
ui_manifest: ui/ui_manifest.yaml
query_manifest: ui/queries/queries.yaml
action_manifest: ui/actions/actions.yaml
runtime:
contract_version: extension_host_v2
egress: {}
python_requirements: []
Kinds And Features
Flow Steward currently supports these extension kinds:
| Kind | Use it when | Required feature |
|---|---|---|
task_provider | The extension ingests or creates work items for workflows. | task_ingress |
notification_provider | The extension sends notifications or receives external interaction events. | notify |
tool_provider | The extension exposes tool-style actions or read operations. | tool |
The validator checks that the selected kind and feature set make sense together.
For example, ingress_event is valid only for notification_provider.
Manifest Field Reference
| Field | Required | Description |
|---|---|---|
manifest_version | Yes | Use 2 for current extensions. |
extension_id | Yes | Canonical lowercase id. The folder name must match its normalized form. |
kind | Yes | One of task_provider, notification_provider, or tool_provider. |
version | Yes | Extension release version. Use semantic versioning for public releases. |
display_name | Yes | Human-readable name shown in the UI and marketplace. |
description | Yes | Short description for operators and marketplace listings. |
vendor.name | Yes | Canonical publisher identity. Compatibility aliases exist, but new manifests should use vendor.name; identity is not authorization. |
lifecycle | Yes | Data-schema version and ordered migrations; version 1 may use an empty migration list. |
features | Yes | Capability flags such as notify, tool, task_ingress, or ingress_event. |
required_scopes | Recommended | Host permissions the extension requires. Scope names must include a colon. |
event_subscriptions | Optional | Authoring selectors the extension wants delivered; compilation derives supported_events. Do not author supported_events directly. |
schemas | Optional | Named string paths to YAML schemas or actual inline JSON Schema objects. {ref: ...} is an inline object, not a file-reference form. |
entrypoint | Yes | Subprocess command used for query/action invocation. |
health | Yes | Healthcheck command. |
ui_manifest | Yes for manifest v2 | String path such as ui/ui_manifest.yaml. |
query_manifest | Required when queries are referenced | String path to query definitions. |
action_manifest | Required when actions are referenced | String path to action definitions. |
machine_auth | Optional | Machine auth settings for service-account style execution. |
account_scope | Optional | Account/project/workflow boundaries and subscription settings. |
callbacks | Optional | Callback registrations for webhooks or provider callbacks. |
workflow_triggers | Optional | Custom workflow trigger types contributed by the extension. |
dependencies.extensions | Optional | Other extensions this extension depends on. |
python_requirements | Optional | Full transitive Python wheel lock for the extension subprocess. |
runtime.contract_version | Yes | Must be extension_host_v2. Validation and install refuse a missing or different value (missing_host_contract, unsupported_host_contract). |
runtime.extension_contract_v2 | Optional | Refs to Contract v2 files used by builders and registry projections. |
egress | Recommended | Where the extension connects: hosts (bare host names or *.example.com) and connection_fields (<connection_type_id>.<field>). Shown to the administrator, not enforced. egress: {} says it connects nowhere; leaving it out installs with the egress_undeclared warning. |
runtime_grants | Optional | Least-needed host authority restricted to the extension's namespace. |
external_effects | Conditional | Active notification providers require the reserved delivery effect; not mandatory for every kind. |
project_state | Optional | Extension-owned project state declaration. |
listing | Optional in ordinary validation | Marketplace validation requires category and summary. |
release_stage | Optional | Defaults to active; placeholder delivery exemption does not bypass other validation. |
entrypoint.timeout_seconds | Optional | Invocation timeout, validated when supplied. |
All refs must stay inside the extension bundle and must point to YAML files. Absolute paths, path traversal, missing files, and JSON contract files are validation errors.
Kind-Specific Operation Contracts
Required capability flags are not the whole provider contract. A task_provider declares task_ingress; other kinds must not use that feature. The required operation declarations are:
| Operation | Required result fields | Required errors |
|---|---|---|
Action normalize_task_action | result.normalized | Common action errors plus invalid_payload |
Action fetch_attachments | result.attachments | Common action errors plus not_found |
Action sync_completion_status | result.status | Common action errors plus conflict |
Query provider_health | result.status, result.configured | not_configured, authentication_failed, provider_unavailable, timeout, internal_error |
Common action errors are not_configured, authentication_failed, rate_limited, provider_unavailable, timeout and internal_error. Declare fields/errors directly or inside the operation's contract. All task-ingress operation errors must belong to the supported catalog: not_configured, missing_credentials, authentication_failed, permission_denied, invalid_payload, unsupported_event, not_found, conflict, rate_limited, timeout, provider_unavailable, upstream_error, invalid_response, runtime_error, internal_error.
An active notification_provider requires notify and exactly one reserved deliver_notification action, Contract V2 operation and external effect:
| Declaration | Required contract |
|---|---|
| Action | Extension handler; workflow_visible: false; exactly event and route parameters, both required objects. |
| V2 operation | operation_kind: action; workflow_visible: false; exactly event and route inputs, both required with value_type: object; outputs include delivered (boolean) and provider_message_id (text). |
| External effect | operation_id: deliver_notification; effect_kind: message_delivery; nonempty channel; idempotency: required; test_mode_behavior: suppress. |
release_stage: placeholder is exempt from that active delivery contract, not from kind/features or other validation. Notification kind does not require blanket runtime_grants.
Optional notification feature ingress_event requires an ingress_event action declaring result.ingress_type and an event_subscriptions selector for external.comment.received. notification.interaction_received is recommended, not mandatory. Author subscriptions, not compiled supported_events.
Event Subscriptions
Use event_subscriptions when the extension should receive platform events:
event_subscriptions:
- event_type: job.completed
severity: info
- event_type: external.comment.received
project_id: 5b599d21-b522-4e78-80e2-41ffedb6b9b7
Subscription entries can be strings or objects. Object entries support event
type, severity, conditions, and optional project or workflow scope. The accepted
severity values are debug, info, warning, error, critical, and any.
warn is normalized to warning.
The compiler derives the compiled supported_events projection from these
authoring subscriptions. supported_events is not accepted as an authored
manifest key.
Dependencies
Declare extension dependencies when your provider requires another extension to be present:
dependencies:
extensions:
- extension_id: flowsteward.openclaw
min_version: 1.2.0
max_version_exclusive: 2.0.0
required: true
reason: Uses OpenClaw task references in workflow triggers.
The validator rejects self-references, duplicate dependency ids, and invalid semantic version ranges.
Callbacks
Callbacks declare externally callable endpoints that Flow Steward can register and route back into the extension:
callbacks:
- callback_id: provider_events
purpose: Receives Acme provider interaction events.
methods: [POST]
action_id: ingress_event
connection_scoped: true
auth:
type: shared_secret_header
header: X-Acme-Signature
Callback ids use lowercase letters, numbers, underscores, and hyphens. Callback
auth supports none, shared_secret_header, and hmac_sha256.
For HMAC, declare signature_header, timestamp_header, and
delivery_id_header; max_age_seconds defaults to 300 and accepts 30–3,600.
The host-generated registration secret signs UTF-8
<integer timestamp>.<delivery ID>. followed by the raw request body with
SHA-256 HMAC, encoded as lowercase hex (an optional sha256= prefix is accepted).
The delivery ID is claimed before dispatch; a replay returns HTTP 409.
Confirm that your provider supports this exact recipe and a configurable secret.
This is distinct from workflow webhook signature configuration and the generic
OAuth callback. See core/application/extensions/callbacks.py in the target
application source for the current ingress contract.
The operator UI can expose callback setup through a callback_setup component.
Flow Steward APIs create, rotate, and check registrations, while the extension
action handles the actual ingress payload.
Workflow Triggers
Workflow triggers add custom trigger types to the workflow trigger catalog:
workflow_triggers:
- trigger_type_id: acme_comment_received
provider_key: com.acme.crm-sync
display_name: Acme comment received
event_name: external.comment.received
provider_ingress_url_required: true
config_schema:
type: object
required: [project_key]
properties:
project_key:
type: string
ui_schema:
fields:
- name: project_key
label: Project key
widget: text
The catalog id is built from the provider key and trigger type id. Trigger readiness is evaluated from the declared config schema.
Distribution Descriptor
extension-descriptor.yaml is separate from extension.yaml. It is used when
an extension is imported, listed, published, or distributed through marketplace
or private channels.
Use it to declare:
| Descriptor section | Purpose |
|---|---|
schema_version | Descriptor schema version. Current descriptors use extension_descriptor_v1. |
extension_id | Must match the runtime manifest id. |
release_version | Release version for the distributable artifact. |
extension_type | High-level classification for listing and import. |
identity | Display name, description, and tags. |
vendor | Vendor name and support URL. |
listing | Visibility, category, and marketplace listing metadata. |
distribution | Channel, execution modes, self-hosting support, and import sources. |
runtime | Contract version, compatibility, auth mode, local or remote runtime details. |
python_requirements | Optional Python dependency lock for descriptor/import validation. |
artifacts | Workflows, agents, skills, tools, contracts, guardrails, and extension artifacts to publish. |
provenance | Origin metadata: exactly vendor_published, user_generated, or imported. These values describe provenance; they are not permission or trust tiers. |
commerce | Billing or plan metadata. |
The descriptor validator is strict. Most descriptor blocks reject unknown fields, remote HTTPS endpoints must be HTTPS unless explicitly allowed as insecure, and every artifact entry must have an id plus either a ref or snapshot.
Remote HTTPS descriptors must define these endpoint paths:
runtime:
contract_version: extension_host_v2
remote:
base_url: https://extensions.acme.example
endpoints:
handshake: /handshake
health: /health
query: /query
action: /action
emit_event: /events
Endpoint values are paths, not full URLs, and must not contain ...
Schemas
Extension schemas are JSON Schema documents written in YAML. They define the shape of data, not the visual layout.
Common schema files:
| File | Purpose |
|---|---|
schemas/config.schema.yaml | Non-secret provider settings such as base URL, tenant, labels, allowed hosts, and mock mode. |
schemas/secrets.schema.yaml | Secret fields such as API tokens, passwords, webhook secrets, and OAuth access tokens. Never echo secret values back to the UI. |
schemas/subscriptions.schema.yaml | Operator-configurable event subscription settings. |
Inline input or output schemas | Operation-specific request and response contracts. |
The validator resolves schema refs from the bundle root. Keep schema files in YAML and avoid refs that escape the bundle.
UI Manifest
The UI manifest is the root of the operator-facing extension UI:
version: '1'
pages:
- ref: pages/overview.yaml
- ref: pages/settings.yaml
- ref: pages/events.yaml
Each page has a page_id, title, description, and components:
page_id: settings
title: Settings
description: Configure Acme connection settings.
components:
- ref: ../components/connection_form.yaml
- ref: ../components/secrets_form.yaml
Component ids must be unique inside the extension. Page ids must also be unique. The compiler resolves component refs recursively and reports circular component references.
Component Kit
The validator recognizes the component kit below. Some component types are fully rendered by the current frontend; others are structural or reserved for newer UI surfaces.
| Component type | Purpose |
|---|---|
form | Operator input form bound to load, save, validate, or test actions. |
grid | Tabular data from a query, with optional row actions. |
stats_row | Compact metric row from static values or query results. |
health_panel | Provider/runtime health summary. |
callback_setup | Webhook or callback registration and status panel. |
markdown | Rendered documentation or guidance block. |
log_list | List of log-style messages with severities. |
section, columns, stack, action_bar, fieldset | Layout and grouping primitives. |
repeater | Repeated object or row input, often used inside forms. |
key_value, key_value_card, json_editor, secret_field | Data display or specialized input primitives. |
empty_state, alert, confirm_action, drawer_detail, status_badge, filter_bar | Interaction and display primitives. |
Forms
Forms use ui_schema for visual controls and data for runtime bindings:
component_id: connection_settings
type: form
title: Connection settings
schema_ref: ../../schemas/config.schema.yaml
ui_schema:
fields:
- name: base_url
label: Base URL
widget: url
required: true
- name: mock_mode
label: Mock mode
widget: boolean
data:
load_query: load_connection_settings
save_action: save_connection_settings
test_action: test_external_request
Action bindings must live under data. Top-level save_action,
validate_action, test_action, or load_query fields are rejected.
Valid form widgets include:
text, textarea, url, email, number, integer, boolean, select, radio_group,
checkbox_group, tag_picker, secret, json, key_value, repeater, event_selector,
project_selector, workflow_selector, provider_selector
The renderer currently has first-class handling for the common widgets used by the renderer, including text, textarea, select, boolean, integer, secret, and repeater-backed form sections. Other widgets are valid contract declarations and may require frontend support in the target Flow Steward version.
Repeaters
Use a repeater when the operator edits a list of objects:
- name: records
label: Records
widget: repeater
props:
min_items: 1
fields:
- name: record_id
label: Record id
widget: text
required: true
- name: urgent
label: Urgent
widget: boolean
Repeater fields support props such as unique_in_list and
immutable_on_edit.
Grids
Grids load rows from a query response:
component_id: resources
type: grid
title: Example resources
query_id: list_example_resources
columns:
- key: id
label: ID
type: id
- key: status
label: Status
type: badge
badge_map:
ready: green
blocked: red
row_actions:
- action_id: run_demo_tool
label: Run
scope: row
Supported column types are text, datetime, date, number, badge,
boolean, json_summary, link, and id.
The renderer accepts rows in result.items or provider-specific list fields.
For paginated data, return fields such as page, page_size, total, and
has_more.
Health Panels
Health panels call a query and render provider status:
component_id: provider_health
type: health_panel
query_id: provider_health
Return a result with status, provider, runtime, and optional checks.
Callback Setup
Use callback_setup to expose webhook registration:
component_id: callback_setup
type: callback_setup
callback_id: provider_events
title: Provider callback
The UI calls Flow Steward callback APIs to create, rotate, and check callback registrations. The declared callback action handles inbound provider payloads.
Queries And Actions
Queries are read operations. Actions are command operations. Both are declared in YAML and implemented by the extension runtime.
ui/queries/queries.yaml:
queries:
- query_id: provider_health
title: Provider health
description: Returns runtime and provider health.
required_scopes:
- extension:invoke
result_fields:
- result.status
- result.checks
handler:
mode: extension
ui/actions/actions.yaml:
actions:
- action_id: save_connection_settings
title: Save connection settings
description: Stores non-secret connection settings.
required_scopes:
- extension:invoke
mutates_platform: true
command_type: invoke_extension_action
workflow_visible: false
handler:
mode: extension
Common operation fields:
| Field | Applies to | Description |
|---|---|---|
query_id or action_id | Required | Unique operation id. |
title | Recommended | UI label and diagnostic name. |
description | Recommended | Operator-facing description. |
required_scopes | Recommended | Required host scopes. |
result_fields | Optional | Result paths the operation returns. |
error_codes | Optional | Known provider error codes. |
contract | Optional | Operation-specific contract metadata. |
handler.mode | Optional | extension for provider runtime, platform for host handling. |
mutates_platform | Actions | Set true when an action requests host-side mutation. |
command_type | Mutating actions | Required when mutates_platform is true. |
workflow_visible | Required for actions | Explicit boolean. true requires a same-ID workflow-visible Contract V2 operation with compatible action/input/output semantics. |
Operation ids must be unique. UI components that reference an action or query id must reference an id declared in the corresponding manifest.
Registering A New Action
To add an action:
- Add the action to
ui/actions/actions.yaml. - If the action has input, add or update a YAML schema.
- Bind the action from a component, workflow operation, callback, or Contract v2 operation.
- Implement a handler in the extension runtime.
- Return the canonical response envelope.
- Add a smoke check or pytest assertion.
- Run
extensions validateandextensions test.
For a UI form save action, the frontend sends component context, target information, current values, and form data to:
POST /api/extensions/{extension_id}/action/{action_id}
Use An Action As A Workflow Step
This is how custom code reaches a workflow. An action appears in the workflow builder under Extension tools when both of these declare the same id:
- The action in
ui/actions/actions.yamlwithworkflow_visible: true. - A Contract v2 operation in
contracts/operation_manifest.yamlwith the sameoperation_id, its inputs and outputs, referenced fromruntime.extension_contract_v2.operation_manifestinextension.yaml.
# ui/actions/actions.yaml
actions:
- action_id: score_lead
workflow_visible: true
title: Score lead
description: Scores a CRM lead with the team's own rules.
required_scopes: [extension:invoke]
handler: {mode: extension}
result_fields: [result.score]
error_codes: [invalid_payload]
mutates_platform: false
# contracts/operation_manifest.yaml
operations:
- operation_id: score_lead
display_name: Score lead
description: Scores a CRM lead with the team's own rules.
operation_kind: action
inputs:
- {name: lead, value_type: object, required: true}
outputs:
- {name: score, value_type: integer}
result_fields: [result.score]
error_codes: [invalid_payload]
The handler receives the mapped inputs in action.input and returns
{"ok": true, "result": {"score": 87}}. In the workflow, add an
Extension tools step, pick the installed extension and the operation, and
map inputs from workflow state. A visible action without a matching operation
fails validation (workflow_action_requires_contract_v2,
workflow_action_operation_mismatch). The Example Showcase's run_demo_tool
and invoke_tool are worked examples.
Runtime Backend Logic
Local extensions run out of process. Flow Steward starts the declared command, writes one JSON request to stdin, reads one JSON response from stdout, and then normalizes and validates the response.
A larger extension can separate its runtime like this:
main.py
reads stdin JSON
passes the request to handlers/router.py
writes the response envelope as JSON
handlers/router.py
dispatches by mode and query_id/action_id
handlers/common.py
provides ok(), fail(), action_input(), context helpers, and safe formatting
Query request shape:
{
"contract_version": "extension_host_v2",
"mode": "query",
"host": {
"contract_version": "extension_host_v2",
"operation": "query",
"scope": {
"account_id": "acct_123",
"project_id": "project_123",
"workflow_id": null,
"job_id": null,
"group_id": null,
"for_each_id": null,
"step_id": null,
"step_execution_id": null
},
"principal": {
"actor_id": "user_123",
"principal_type": "user"
}
},
"query": {
"query_id": "provider_health",
"page_id": "overview",
"component_id": "provider_health",
"context": {
"project_id": "project_123"
},
"params": {}
}
}
Action requests use the same host envelope and place operation data under
action:
{
"contract_version": "extension_host_v2",
"mode": "action",
"host": {
"contract_version": "extension_host_v2",
"operation": "command",
"scope": {
"account_id": "acct_123",
"project_id": "project_123",
"job_id": "job_123",
"group_id": null,
"for_each_id": "each_order",
"step_id": "save_order",
"step_execution_id": "3f0c9a52-6d1e-4c8e-9b7a-2e5d0f1a8c44"
}
},
"action": {
"action_id": "save_connection_settings",
"page_id": "settings",
"component_id": "connection_settings",
"context": {},
"target": {},
"input": {
"base_url": "https://api.acme.example",
"mock_mode": true
}
}
}
Every envelope carries the four execution-scope keys group_id,
for_each_id, step_id and step_execution_id. group_id or for_each_id
names the step's immediate container (at most one of them; both are null for a
root step), and all four are null outside a workflow run. The SDK's
runtime_context_from_payload refuses an envelope with another contract, a
missing scope key, or both containers set. There are no phases in this
contract. Runtime context can also include artifact grants and logger metadata
when an action runs inside a workflow.
Extension SDK
New extension runtime code should import platform helpers from the public SDK namespace:
from flowsteward_extension_sdk import (
assert_safe_remote_http_url,
open_pinned_url,
read_artifact_bytes,
runtime_context_from_payload,
write_artifact_bytes,
)
Do not import private core.* modules from extension runtime files, and do not
mutate sys.path to reach Flow Steward internals. The validator rejects those
patterns for new extensions. Platform primitives belong in
flowsteward_extension_sdk; third-party libraries belong in
python_requirements.
The SDK is flowsteward-extension-sdk 0.4.0. It exposes:
| SDK area | Use it for |
|---|---|
runtime_context_from_payload, RuntimeContext, RuntimeResources | Reading account, project, workflow, job, execution scope (Group, For each, step), logger, artifact storage and host-authorized resources from the request. Refuses envelopes that are not extension_host_v2. |
assert_safe_remote_http_url, assert_safe_remote_https_url, resolve_pinned_ips, build_pinned_opener, open_pinned_url, parse_retry_after | SSRF-resistant outbound HTTP(S) with DNS/IP pinning and private-address blocking; Retry-After parsing for rate limits. |
read_artifact_bytes, write_artifact_bytes, stream_artifact_bytes, write_artifact_stream, artifact_to_local_path and the dataset_* equivalents | Reading input files and writing output files through host-granted URLs, whole or streamed. |
ArtifactDatasetPlane and storage adapters | Local, in-memory or S3-backed artifact and dataset records in tests or controlled flows. |
report_progress | Showing "doing X, 40 of 120" on the running step in Job Details. Never raises; does nothing outside a run. |
host_mutation | Building a typed host mutation request that the host checks against runtime_grants. |
migration_result, MigrationEnvelope | Returning the result of a data migration action declared in lifecycle.data_migrations. |
handshake_result | The handshake answer of a remote (remote_https) runtime. |
ExtensionWorkflowHarness, resolve_integration_contract | Testing workflows and operation contracts inside the extension's own test suite. |
Example runtime context and artifact access:
from flowsteward_extension_sdk import (
ArtifactAccessError,
read_artifact_bytes,
runtime_context_from_payload,
write_artifact_bytes,
)
def process_artifact(request_payload: dict) -> dict:
ctx = runtime_context_from_payload(request_payload)
try:
source = read_artifact_bytes(request_payload, binding_key="input_csv")
result = source.upper()
output = write_artifact_bytes(
request_payload,
result,
binding_key="output_csv",
content_type="text/csv",
)
except ArtifactAccessError as exc:
return {
"ok": False,
"error_code": "artifact_access_failed",
"error": str(exc) or "Artifact access failed.",
"errors": [{"code": "artifact_access_failed", "message": str(exc) or "Artifact access failed."}],
}
return {
"ok": True,
"result": {
"project_id": ctx.project_id,
"artifact": output,
},
}
The entrypoint that serializes this helper result must exit 2 for ok: false
and 0 for ok: true.
The SDK is placed on the extension subprocess PYTHONPATH by the platform.
Do not list it in python_requirements or put it in wheels/. For your own
tests outside Flow Steward, keep the SDK wheel in dev-wheels/ and name it in
requirements-dev.txt; dev-wheels/ is never packaged.
Response Envelope
The subprocess exit status is part of the protocol. Successful operations exit
0 with ok: true. A domain failure exits 2 with ok: false, nonempty
error_code and error, and a nonempty errors list. Every entry has a code
and message; the first must match the top-level values exactly:
{"ok": false, "error_code": "invalid_input", "error": "A record ID is required.", "errors": [{"code": "invalid_input", "message": "A record ID is required."}]}
A bare ok: false response is not a valid domain-error envelope. Malformed
responses and mismatched exit statuses can be treated as runtime failures and
count against the circuit breaker. Keep diagnostics on stderr.
Every query and action should return this envelope:
{
"ok": true,
"result": {},
"commands": [],
"events": [],
"reload": [],
"errors": []
}
Field meanings:
| Field | Description |
|---|---|
ok | Boolean success flag. Missing ok is normalized as an error. |
result | Operation result object. UI components read from this object. |
commands | Host-side commands requested by the extension. |
events | Events emitted by the extension. Event types must be allowed by the compiler-derived supported_events projection; author selectors in event_subscriptions, never in a manifest supported_events field. |
reload | Component ids the UI should refresh after the action succeeds. |
errors | Structured provider errors. |
Known command types include:
start_workflow, pause_job, resume_job, cancel_job, retry_step,
request_approval, resolve_approval, enqueue_followup_job,
invoke_extension_action, update_external_status
Actions with mutates_platform: true must declare the command type they intend
to request.
Events
Events are how extensions connect provider activity to Flow Steward workflows and notifications.
Declare delivered events in the authoring event_subscriptions list:
event_subscriptions:
- event_type: external.comment.received
severity: info
- event_type: notification.interaction_received
severity: info
The extension may then return events from an action response:
{
"ok": true,
"events": [
{
"event_type": "external.comment.received",
"summary": "New Acme comment received",
"payload": {
"comment_id": "c_123",
"record_id": "r_456"
}
}
]
}
Platform workflow reaction events include:
job.completed, job.failed, job.step_started, job.step_completed,
job.step_failed, job.step_blocked, job.group_started, job.group_completed,
job.group_failed, job.approval_waiting, job.timer_waiting
Extension Contract v2 canonical events include:
notification.requested, notification.delivered, notification.failed,
approval.requested, approval.received, approval.expired,
interaction.received, job.started, job.completed, job.failed
Provider-owned external events are named by the extension. Use a clear namespace
such as external.task.created or external.comment.received.
The event envelope supports fields such as event_id, event_type,
correlation_id, occurred_at, account_id, severity, summary, payload,
project/workflow/job/group/For each/step ids, source, aggregate id, actor, URLs, and
metadata.
External APIs And OAuth
Keep live network behavior explicit and safe:
- Use mock mode by default for examples and tests.
- Require HTTPS for live provider requests.
- Keep credentials out of URLs.
- Restrict live hosts with an
allowed_hostsconfig field. - Cap response bodies and redact tokens in responses and logs.
- Store API tokens and OAuth access tokens as secrets, not config.
For example code, make live requests an explicit choice. Validate the target
host, keep credentials out of URLs, and redact token material from diagnostics.
The Acme CRM Sync example makes no live provider requests. The Example
Showcase's test_external_request shows a mock-by-default live request.
OAuth connections are described in Contract v2 connection_types. The runtime
action receives only the connection-scoped secret values it needs, for example
target.connection.secrets.access_token. Refresh tokens and client credentials
remain platform-managed.
Extension Contract V2
Extension Contract v2 is the builder-facing contract. It describes what the extension contributes to workflow builders and registry projections. It is not the same as the operator UI manifest.
Reference it from extension.yaml:
runtime:
extension_contract_v2:
connection_types:
ref: contracts/connection_types.yaml
operation_manifest:
ref: contracts/operation_manifest.yaml
step_ui_manifest:
ref: contracts/step_ui_manifest.yaml
resource_query_manifest:
ref: contracts/resource_query_manifest.yaml
event_bindings:
ref: contracts/event_bindings.yaml
artifact_policies:
ref: contracts/artifact_policies.yaml
Contract sections:
| Section | Purpose |
|---|---|
connection_types | Connection definitions and auth model, including API key, OAuth2, static secret, or none. |
dataset_connectors | Dataset connector declarations and runtime kind. |
operation_manifest | Builder operations such as sources, actions, bulk actions, reports, and human channels. |
step_ui_manifest | Workflow step form fields for operations. |
resource_query_manifest | Resource pickers and lookup queries used by builders. |
event_bindings | Trigger bindings and event-to-operation relationships. |
artifact_policies | Artifact inputs and outputs for operations. |
Operation kinds currently include source, action, bulk_action, report,
and human_channel.
Value types include text, number, integer, boolean, json, array,
object, artifact_handle, dataset_handle, and connection_ref.
Step UI widgets include common form widgets plus builder-specific controls such
as resource_picker, artifact_picker, dataset_picker, schema_mapper,
mapping_table, and batch_policy.
The Contract v2 validator checks unique ids, valid operation kinds, connection references, batch settings, OAuth2 requirements, resource query references, step UI field references, event bindings, artifact policies, and optional locale keys.
Register Workflows, Agents, Skills, And Tools
Use extension-descriptor.yaml artifacts to publish bundled Flow Steward
objects together with the extension:
artifacts:
workflows:
- workflow_id: acme_triage_workflow
ref: artifacts/workflows/acme_triage_workflow.yaml
publish: true
agents:
- agent_id: acme_triage_agent
ref: artifacts/agents/acme_triage_agent.yaml
publish: true
skills:
- skill_id: acme_triage_skill
ref: artifacts/skills/acme_triage_skill.yaml
publish: true
tools:
- tool_id: acme_lookup_tool
ref: artifacts/tools/acme_lookup_tool.yaml
publish: true
Supported artifact sections are workflows, agents, skills, tools,
datasets, saved_queries, data_contracts, guardrails and extensions. Each artifact entry needs an id and a
ref or snapshot. Set publish: false for internal artifacts that should be
packaged but not listed.
Validate every published artifact against the target registry schema. The Acme CRM Sync example has no distribution descriptor; the Example Showcase publishes 14 workflows, 5 agents, 5 skills, 3 tools, a dataset and a saved query this way.
Package target and host grants
runtime_target is package metadata in .fs-package.yaml, not an allowed
extension.yaml field. Native bundles use linux-amd64 or linux-arm64 and
need the matching Linux executable; inspect the bundler's --runtime-target
option. Python wheel platform tags describe dependency compatibility separately.
All extensions share the same trust model. Vendor/publisher names and catalog
verification do not grant privileges. Declare only supported runtime_grants
for your extension's own namespace; use the current grant validator and SDK.
There is no privileged system-extension bootstrap step.
Validation
The extension compiler and validator perform several checks before an extension can be enabled:
- Locate
extension.yamlorextension.yml, and checkicon.png. - Reject forbidden JSON contract files.
- Normalize manifest identity, entrypoint, healthcheck, features, scopes, and refs.
- Resolve schema refs inside the bundle.
- Resolve UI manifest pages and components.
- Resolve query and action manifests.
- Detect duplicate page ids, component ids, action ids, and query ids.
- Validate kind and feature combinations.
- Validate callbacks, dependencies, workflow triggers, and event selectors.
- Check
runtime.contract_version,egress, bundled wheels againstpython_requirements, translations and the runtime code boundary. - Build the compiled manifest preview.
Compiler validation issues include severity, code, message, file path, field path, and hint. These compiler codes are separate from public CLI envelope codes. Use JSON output to inspect the public command result:
./flow-steward extensions validate com.acme.crm-sync --json
The public CLI maps failed extension validation or tests to exit 3 and envelope
code validation_failed; it does not preserve each compiler issue as the
envelope's .error.code. Internal tooling exit values are not public process
statuses. Successful commands exit 0. An unattended create/bundle reaching
review without confirmation exits 10 with cancelled. Record actual launcher
results and prerequisite failures, not inferred successes.
Marketplace validation checks manifest vendor/listing/scopes/machine-auth/account-scope metadata, not the distribution descriptor schema. Ordinary validation also checks certain descriptor-referenced artifact modules when a descriptor is present. Use --marketplace for the additional manifest checks:
./flow-steward extensions validate com.acme.crm-sync --marketplace
vendor.name is required even in ordinary validation. Marketplace validation
additionally requires a listing category/summary, nonempty required_scopes,
machine_auth.service_account_key, nonempty machine_auth.required_scopes,
account_scope.scope_type and account_scope.description. Listing summary can
be normalized from the manifest description; an absent listing still lacks the
required category. Marketplace rejections are textual diagnostics, not invented
compiler issue codes.
Validate distribution descriptors separately through the descriptor API described
in Developer Extensions; --marketplace is not a
descriptor-validation command.
Tests And Scripts
Use both scripts and tests. They serve different purposes.
| File | Purpose |
|---|---|
scripts/smoke_check.py | Fast executable proof that the bundle can run. It should check health plus representative queries/actions. It does not need pytest and can be run directly in local CI or packaging jobs. |
tests/test_*.py | Regression tests. Use pytest assertions for compile output, descriptor validation, Contract v2 checks, runtime behavior, integration safety, OAuth secret injection, ingress payloads, and action response shapes. |
The extensions test command runs validation, then any smoke script and local pytest targets. Default testing requires requirements-dev.txt and fails if no pytest targets exist unless --allow-no-pytest explicitly allows their absence:
./flow-steward extensions test com.acme.crm-sync
The isolated test environment inherits the tooling interpreter's site packages and uses offline pip with dev-wheels/ and wheels/. The generated scaffold has a comment-only requirements-dev.txt and one pytest file; neither wheel directory is required when the tooling interpreter already has the test packages. Missing requirements or unavailable development dependencies fail the command. --allow-no-pytest and --skip-pytest do not prove regression coverage.
Use scripts/smoke_check.py to answer "does this bundle basically work right
now?" Use pytest to answer "will this behavior stay correct as the extension and
platform evolve?"
Python Dependencies And Wheel Distribution
An extension never installs packages into the Flow Steward Python environment
and never runs pip install at run time. Every third-party library it imports
ships inside the archive as a wheel, and extension.yaml pins it with its
hashes under python_requirements:
python_requirements:
- name: cffi
version: "2.1.1"
hashes:
- sha256:<hash of the x86_64 wheel>
- sha256:<hash of the aarch64 wheel>
- name: pycparser
version: "3.11"
hashes:
- sha256:<hash of the py3-none-any wheel>
Each entry is:
- a PEP 503-normalized lowercase package name;
- an exact PEP 440 version, not a range;
- a non-empty list of
sha256:hashes, one per wheel file of that version; - part of the full transitive closure: every dependency of every wheel is listed too.
Wheels only: source distributions are not accepted, and nothing is built during installation.
Target platforms
Flow Steward runs extensions on Linux in its own container: CPython 3.12, Debian with glibc 2.36, on x86_64 (amd64) or aarch64 (arm64) servers. Apple Silicon Macs and ARM cloud servers run the arm64 image. A distributable extension must include wheels for both architectures:
| Server | Wheel platform tag | Example wheel |
|---|---|---|
linux/amd64 | manylinux…_x86_64 | cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl |
linux/arm64 | manylinux…_aarch64 | cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl |
| both | any | pycparser-3.11-py3-none-any.whl |
A pure-Python py3-none-any wheel covers both. A compiled package needs one
wheel per architecture, built for CPython 3.12 (cp312) or the stable ABI
(abi3). macOS (macosx_*), Windows (win_*) and musllinux wheels do not
install in the Flow Steward container.
Step by step
Run these in the Development tool runtime (./flow-steward …). It runs Python
3.12, so the downloaded wheels match the server's Python.
-
Put the direct dependencies, pinned, in
requirements.innext toextension.yaml:paramiko==5.0.0cryptography==50.0.2 -
Lock them.
lock-depsdownloads binary wheels formanylinux2014_x86_64andmanylinux2014_aarch64, resolves the full closure for each, and writes every name, version and hash intopython_requirements:./flow-steward extensions lock-deps com.acme.crm-sync --root extensions --confirmIt fails, naming the platform, when a package has no binary wheel for one of them.
-
Download the wheels into
wheels/and build the archive:./flow-steward extensions bundle com.acme.crm-sync --root extensions --with-wheels --confirmFor each platform,
--with-wheelsdownloads every locked requirement with--require-hashes, so a platform without a wheel whose hash is in the lock stops the bundle. The wheels stay in<extension>/wheels/; commit them if you build archives elsewhere, as the published Flow Steward extensions do. -
Validate the bundle with its wheels and check the archive size (below):
./flow-steward extensions validate com.acme.crm-sync --root extensions
Some packages publish only newer manylinux_2_28 wheels (for example recent
numpy, pillow or onnxruntime). The manylinux2014 default does not find
them; name a newer baseline for both steps. The runtime's glibc 2.36 accepts
anything up to manylinux_2_36:
./flow-steward extensions lock-deps com.acme.crm-sync --root extensions \
--platforms manylinux_2_28_x86_64,manylinux_2_28_aarch64 --confirm
./flow-steward extensions bundle com.acme.crm-sync --root extensions --with-wheels \
--platforms manylinux_2_28_x86_64,manylinux_2_28_aarch64 --confirm
--platforms active resolves only for the tool runtime's own architecture. Use
it for local experiments, never for an archive you hand to someone else.
Vendoring wheels without the Flow Steward CLI
In a standalone extension repository you can fetch the same wheels with pip. Use Python 3.12 and run pip once per architecture:
pip download --only-binary=:all: --dest wheels \
--platform manylinux2014_x86_64 --implementation cp --python-version 3.12 \
--abi cp312 --abi abi3 --abi none -r requirements.in
pip download --only-binary=:all: --dest wheels \
--platform manylinux2014_aarch64 --implementation cp --python-version 3.12 \
--abi cp312 --abi abi3 --abi none -r requirements.in
These are the options lock-deps passes to pip. Then add one
python_requirements entry per package and version, listing the SHA-256 of
every wheel of that version (sha256sum wheels/*.whl, or shasum -a 256 on
macOS). Run extensions validate on the result: it reports any wheel that is
not declared, a hash that is not listed, and a dependency missing from the
closure.
What the tooling checks, and what it does not
| Check | Where | Both architectures? |
|---|---|---|
Every file in wheels/ is a .whl, declared in python_requirements with its hash; wheel metadata names no undeclared dependency | extensions validate, install | No: it checks the files that are there. |
| Every requirement has at least one bundled wheel with a listed hash | extensions validate, install | No: one wheel of any architecture satisfies it. |
| Each target platform resolves with binary wheels | extensions lock-deps | Yes, for the platforms it is given (both by default). |
| Each locked requirement downloads for each target platform with a listed hash | extensions bundle --with-wheels | Yes, for the platforms it is given (both by default). |
| A wheel compatible with this server's Python and architecture exists | Install and update | Only the server's own architecture. |
So the guarantee comes from building with lock-deps and bundle --with-wheels
using both platforms. extensions validate passes a bundle whose wheels/
holds only x86_64 wheels; that archive installs on amd64 and fails on an arm64
server with No compatible wheel for python requirement '<name>==<version>'.
Before publishing, check that wheels/ holds an _x86_64 and an _aarch64
wheel (or one any wheel) for every requirement.
Archive size and per-architecture archives
The packager and the installer refuse an archive larger than 50 MiB zipped
or 150 MiB unpacked. The installed dependencies may take up to 500 MiB
(FS_EXTENSION_DEPS_SITE_MAX_BYTES). When wheels for both architectures do not
fit in one archive, build one archive per architecture:
./flow-steward extensions bundle com.acme.crm-sync --root extensions --runtime-target linux-amd64 --confirm
./flow-steward extensions bundle com.acme.crm-sync --root extensions --runtime-target linux-arm64 --confirm
Each archive, named com.acme.crm-sync-<version>-linux-amd64.zip or
…-linux-arm64.zip, holds only the wheels for its architecture and a
.fs-package.yaml naming the target. A server refuses an archive built for the
other architecture. Run bundle --with-wheels first so wheels/ holds both
sets. Extensions with native executables (bin/linux-amd64/,
bin/linux-arm64/) always need these two archives.
At install time
Install and update pick, for each requirement, the bundled wheel that best matches the server, and unpack them into the extension folder:
.fs-python/<extension-version>/site/
The Flow Steward Python environment is not modified. Each invocation gets
PYTHONPATH with that site directory first and the public SDK second.
| Setting | Meaning |
|---|---|
FS_EXTENSION_DEPS_FETCH=off | Default. Every requirement must have a bundled wheel with a listed hash. |
FS_EXTENSION_DEPS_FETCH=allowlist | Install may download a missing wheel from PyPI when its hash is listed. For controlled environments only; a published archive should not rely on it. |
FS_EXTENSION_DEPS_SITE_MAX_BYTES | Caps the unpacked dependency directory (default 500 MiB). |
Bundle And Install
Run extensions validate and extensions test first and stop on any failure:
bundle only packages files and can produce an archive whose manifest is
invalid. Then build the archive:
./flow-steward extensions bundle com.acme.crm-sync --root extensions --with-wheels --confirm
Use bare bundle (without --with-wheels) only when the extension has no
python_requirements. The command prints the archive path and its SHA-256, for
example dist/extensions/com.acme.crm-sync-0.1.0.zip. The archive's top folder
is the canonical folder name (com_acme_crm_sync/). It never contains
dev-wheels/, .fs-python/ or cache folders, and the bundle refuses to run
without a valid icon.png. --extension-version 0.2.0 writes that version into
the packaged manifest and descriptor.
Install the archive on Flow Steward as an administrator:
- In the UI: open Extensions, choose Install extension from file
and pick the
.zip, or drop the.zipon the extensions list. Review the install result, configure the extension, then enable it. - Through the API:
POST /api/extensions/installwithmultipart/form-dataand the archive in thefilefield. - From the CLI on a Development or Scalable installation:
./flow-steward extensions install --archive dist/extensions/com.acme.crm-sync-0.1.0.zip, thenextensions enable,extensions health.
On Compact, install through the UI or the API. The application hands the archive to its installer service, which owns the extensions folder; copying a folder into the container is not an install.
After installation, check the extension's preview and Health before enabling it. Catalog extensions install from Extensions › Marketplace instead; see Workflow and Extension Catalogs.
Updating An Extension
To update an installed extension, install an archive with a higher
version through the same paths: Install extension from file,
POST /api/extensions/install or extensions install --archive. Flow Steward
recognises the installed extension id and updates it in place: it stages the
new release, runs its data migrations, and only then switches over; a failure
leaves the previous release active. A disabled extension stays disabled.
Installing the same version again, or a lower one, is refused.
Before you release an update:
- Make the smallest compatible manifest and runtime changes.
- Add or update schemas before wiring new UI fields to actions.
- Increment
lifecycle.data_schema_versionwhen persisted extension data changes and declare the ordered migration from the prior version. Leave it unchanged for code/UI-only updates. - Keep operation ids stable. Renamed action, query, trigger and operation ids break workflows that use them.
- Bump the extension version.
- Update descriptor artifacts and compatibility metadata.
- Re-run
lock-depsandbundle --with-wheelsif dependencies changed. - Run validation, smoke checks and pytest.
- Bundle the extension and record the archive digest.
- Install it over the previous version in a test environment and verify migrations, health, callbacks, queries, actions, workflow triggers and artifact projections.
Authoring Checklist
- The folder name matches the canonical
extension_id. icon.pngsits next toextension.yaml, andruntime.contract_versionisextension_host_v2.egresslists every host the extension connects to, or is{}.- All authoring contracts are YAML, not JSON.
extension.yamldeclares the mandatory lifecycle and the correct kind, features, scopes, entrypoint, and healthcheck.- Every UI page, component, query, and action id is unique.
- Every UI reference points to a declared action or query.
- Schemas describe data, while UI schemas describe controls.
- Secret values are stored only in secrets and are never echoed back.
- Runtime code imports platform helpers from
flowsteward_extension_sdk, not privatecore.*modules. - Runtime code does not mutate
sys.path. - Third-party subprocess libraries are locked in
python_requirementsand bundled as wheels. - Live external requests require HTTPS and an allowlist.
- Event delivery is authored through
event_subscriptions; compiledsupported_eventsis validator output, not an authoring field. - Callback ids and workflow trigger ids are stable.
- Contract v2 operation ids, resource query ids, event bindings, and artifact policies validate together.
- Descriptor artifacts have ids plus refs or snapshots.
- Smoke scripts and pytest cover representative runtime behavior.
validate,test, andbundlepass before publishing.bundle --with-wheelsis used whenpython_requirementsis present, andwheels/holds an_x86_64and an_aarch64wheel (or oneanywheel) for every requirement.extensions validatedoes not check this for you.- The archive is under 50 MiB zipped and 150 MiB unpacked, or split per
architecture with
--runtime-target.