Compositions
A composition (also called a virtual workflow) is a workflow you assemble from catalog workflows — saved, versioned, and runnable like any other type, without writing or deploying code.
You describe what runs, in which order, and where each input comes from. Orkestia validates the definition against the live catalog, compiles it, and registers it as:
virtual.<composition-uuid>@<version>From that point you can invoke it (API / MCP / SDK), share it with your app's end-users, and they use it with their Sign in with Orkestia JWT.
Not to be confused with the
composition.*workflows in the catalog — those manage compositions (composition.save,composition.validate,composition.activate, …). The composition itself is data: a JSON definition, not Python.
How-to for app builders: docs.orkestia.dev — Compositions. Catalog ops: Composition domain.
The definition format
A composition is a JSON document with ordered layers. Layers run in sequence. Steps inside one layer can run together. Each step names a catalog workflow_type and an input_mapping object — keys are the step's input parameter names.
The live serializer dialect (composition.validate / composition.save) is:
input_mapping: { <param>: { source: "input"|"step"|"static", field_name?, step?, value? } }{
"name": "provision-and-notify",
"description": "Create a bucket, then tell the team",
"layers": [
{
"name": "provision",
"steps": [
{
"name": "create-bucket",
"workflow_type": "aws.s3.create_bucket",
"input_mapping": {
"bucket_name": { "source": "input", "field_name": "bucket_name" },
"region": { "source": "static", "value": "us-east-1" }
}
}
]
},
{
"name": "notify",
"steps": [
{
"name": "announce",
"workflow_type": "slack.message.send",
"input_mapping": {
"connection_uuid": { "source": "input", "field_name": "slack_connection_uuid" },
"channel": { "source": "static", "value": "#deploys" },
"text": { "source": "step", "step": "create-bucket", "field_name": "bucket_name" }
}
}
]
}
]
}Do not use the legacy input_mappings array with a target field. composition.validate accepts input_mapping and only warns on leftover per-step inputs.
Input mapping sources
source | Meaning | Extra fields |
|---|---|---|
input | Read from the composition's own top-level input | field_name (required) |
step | Read from a previous step's output | step (step name) + field_name |
static | A hardcoded value baked into the definition | value |
step mappings may only read steps in earlier layers. static values are invisible to whoever invokes the composition — that is how you hide connections, table names, and tenant columns when you share the composition with an app user.
Validate, save, invoke
Do not pass organization_uuid. The server fills it from your token.
composition.validate → composition.save → start virtual.<uuid>@<version>| Workflow | What it does |
|---|---|
composition.validate | Three static phases, no persist. Input: { definition } |
composition.save | Validate + compile + persist. Input: { name, definition }. Returns composition_uuid, workflow_type (virtual.<uuid>@N), version |
composition.version | Append a new immutable version on the same lineage |
composition.activate | Re-validate against the live registry and set active |
composition.plan | Read-only diff of desired definitions vs saved state |
composition.archive / composition.rename / composition.delete-archived | Lifecycle |
Invoke as an org member (API)
# 1. Save
curl -X POST https://workflow-api.orkestia.dev/api/workflows/start \
-H "Authorization: Bearer $ORKESTIA_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"workflow_type": "composition.save",
"initial_data": {
"name": "provision-and-notify",
"definition": { }
}
}'
# 2. Run the compiled type from the save output
curl -X POST https://workflow-api.orkestia.dev/api/workflows/start \
-H "Authorization: Bearer $ORKESTIA_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"workflow_type": "virtual.6f2c9a41-0000-0000-0000-000000000000@1",
"initial_data": { "bucket_name": "my-new-bucket" }
}'Invoke over MCP
whoami()
start_workflow("composition.save", { "name": "…", "definition": { … } })
watch_workflow(workflow_id)
# from terminal output: composition_uuid, workflow_type, version
start_workflow("virtual.<composition_uuid>@1", { /* free inputs */ })MCP clients: MCP client setup.
Invoke with the Node SDK
import { LtIntegWorkflowsClient } from "@ltinteg/workflows-sdk"
const client = new LtIntegWorkflowsClient({
baseUrl: "https://workflow-api.orkestia.dev",
token: process.env.ORKESTIA_TOKEN,
})
const saved = await client.start("composition.save", {
name: "provision-and-notify",
definition: { /* JSON above */ },
})
const out = await saved.wait()
// out.state_data.workflow_type === "virtual.<uuid>@1"
const run = await client.start(out.state_data.workflow_type, {
bucket_name: "my-new-bucket",
})Every save creates a new immutable version — virtual.<uuid>@1, @2, …. The uuid is the lineage id and stays stable. Runs always pin an explicit version.
Share with an app user
End-users of your app cannot start raw catalog workflows. They can start only virtual types you expose.
- Provision the app (
identity.app.provision) and wire@orkestia/auth— see App Enablement. - Author the composition so every step is
end_user_eligible. Lock secrets asstaticmappings. Leave only free inputs assource: "input". - Expose that exact version:
identity.app.expose-virtual-workflow({
identity_app_uuid: "<from provision>",
composition_uuid: "<from composition.save>",
version: 1
})Pass only those three fields. Do not pass organization_uuid or actor.
- The signed-in user starts
virtual.<uuid>@1with their JWT. Orkestia injects the end-user principal immutably.
import { createOrkestiaAuth } from "@orkestia/auth"
import { LtIntegWorkflowsClient } from "@ltinteg/workflows-sdk"
const auth = createOrkestiaAuth({ clientKey: "orkestia_…" })
const session = auth.getSession()
const client = new LtIntegWorkflowsClient({
baseUrl: "https://workflow-api.orkestia.dev",
token: session.token,
})
const run = await client.start("virtual.<composition_uuid>@1", {
/* free inputs only */
})Revoke with identity.app.unexpose-virtual-workflow (identity_app_uuid, composition_uuid, optional version).
By default every authorized end-user of the app can call an exposed composition. To restrict it, grant vw:<composition-uuid>:invoke to end-user access groups. Grants use the version-agnostic uuid, so they survive re-publishes.
If expose fails, read ineligible_steps — every step must be end_user_eligible in the live catalog.
Enable a user to use it
Give your app user this path (or paste the agent prompt on docs):
- They sign in with Orkestia (
@orkestia/auth). - Your UI calls
virtual.<uuid>@<version>with their session token. - They supply only free inputs. They never see the connection, the table, or another user's rows.
App user → Sign in with Orkestia → JWT
→ start virtual.<uuid>@N (Bearer JWT)
→ engine injects end-user → only their dataWays to author
You rarely write the JSON by hand:
- Visual builder — Orkestia console DAG builder.
- DGI — describe the outcome in chat; review and save.
- TypeScript SDK —
defineComposition/.vw.ts. - DevKit —
ltinteg-devkit vw validate|plan|import. - API / MCP —
composition.savewith the JSON on this page.
Composition patterns
- Chaining — one step per layer; each reads the previous step (
source: "step"). - Fan-out — several steps in one layer, same input, different
static/inputvalues. - Fan-in — a later layer with one step reading several earlier steps.
Prefer several focused compositions over one sprawling DAG — each stays independently versioned, testable with composition.plan, and separately exposable to end-user groups.
