Creating & Exposing Virtual Workflows
A virtual workflow (a composition) is your business logic expressed as structure rather than code: you declare which existing workflows run, in what order, with which inputs, and the platform validates it against the live catalog and compiles it into a regular engine DAG. The result runs, is watched, and is retried exactly like any hand-authored workflow — and, crucially, it is the only kind of workflow you can safely expose to your app's end-users.
This guide walks the full lifecycle: compose → validate → compile → version → expose. To use, invoke, and share a composition with an app user, start at Compositions. For the conceptual model see Compositions and the hybrid execution model.
The mental model
A composition is built from three pieces. Layers run in order; steps within a layer can run together.
| Piece | Role |
|---|---|
| Layer | A stage of the composition. Layers execute sequentially; steps inside one layer are concurrent. |
| Step | One existing workflow type to invoke (e.g. aws.s3.create_bucket), placed in a layer. |
| Input mapping | Where each step argument's value comes from — explicitly, never by implicit scope. |
Every step input declares its source. There is no global scope and no runtime string templating — just three sources:
input
Take the value from the composition's own input payload.
step
Take the value from a prior step's output field.
static
Use a fixed literal you set at authoring time. The caller can neither see nor change it.
Because every mapping is explicit, the platform can type-check the entire composition before it ever runs — confirming each referenced workflow type exists and that each mapping resolves to a compatible field, with precise errors (which step, which reason) instead of a runtime surprise.
Step 1 — Compose
Author the composition as layers of steps with input mappings. The wire format is plain JSON — the same structure AI brains, visual builders, and API clients all produce:
{
"name": "onboard_customer_bucket",
"layers": [
{
"name": "create",
"steps": [
{
"name": "s1",
"workflow_type": "aws.s3.create_bucket",
"input_mapping": {
"name": { "source": "input", "field_name": "bucket_name" }
}
}
]
},
{
"name": "tag",
"steps": [
{
"name": "s2",
"workflow_type": "aws.s3.put_bucket_tagging",
"input_mapping": {
"bucket": { "source": "step", "step": "s1", "field_name": "bucket_name" },
"tagging": { "source": "static", "value": { "TagSet": [{ "Key": "env", "Value": "demo" }] } }
}
}
]
}
]
}
Three things to notice:
s2.bucketis threaded froms1's output ("source": "step") — that data dependency is what placess2in a later layer.tagginguses"source": "static"— baked in at authoring time, invisible to whoever invokes the composition. This is exactly how you lock down sensitive parameters later (see Expose).bucket_nameuses"source": "input"— a free input the caller supplies at run time.
aws.s3.put_bucket_tagging follow the {provider}.{service}.{operation} convention. Browse the full installed catalog in the workflow types registry or the external catalog at reference.orkestia.dev — don't guess names; the validator will reject ones that don't exist.Step 2 — Validate
Authoring runs three validation phases against whatever workflows are currently installed in the catalog. Each phase short-circuits with a typed, structured error so the precise problem is obvious (and feedable straight back into an AI authoring loop).
| Phase | Checks | Example failure |
|---|---|---|
| Structure | Layers acyclic, step IDs unique, mappings well-formed | duplicate step id s1 |
| References | Every {provider}.{service}.{operation} actually exists in the catalog | aws.s3.put_bucket_taging — unknown type |
| Compatibility | Every InputMapping resolves to a compatible source field/type | step.s1.bucket_name field not produced by s1 |
A reference error looks like this — note it points at the exact step and suggests a fix:
{
"phase": "references",
"step_id": "s2",
"workflow_type": "aws.s3.put_bucket_taging",
"reason": "unknown workflow type; did you mean 'aws.s3.put_bucket_tagging'?"
}
Step 3 — Compile
Once validated, the composition compiles to a plain dict matching the engine's DAGWorkflow shape. The engine doesn't know — and doesn't care — that the config came from a composition; there's no runtime marker. It executes it as a normal DAG run with the same history, retries, and concurrency guarantees as any other workflow.
{
"id": "onboard_customer_bucket",
"layers": [
{ "steps": [ { "id": "s1", "workflow": "aws.s3.create_bucket",
"inputs": { "name": { "from": "input.bucket_name" } } } ] },
{ "steps": [ { "id": "s2", "workflow": "aws.s3.put_bucket_tagging",
"inputs": {
"bucket": { "from": "step.s1.bucket_name" },
"tagging": { "value": { "TagSet": [{ "Key": "env", "Value": "demo" }] } }
} } ] }
]
}
Compilation is deterministic: the same input composition against the same installed catalog produces byte-stable JSON — useful for diffing and for reviewing AI-authored plans before they ship.
Step 4 — Version
A composition is referenced by a uuid and a version (e.g. virtual.<uuid>@1). Versioning is what makes exposure safe to evolve: an exposed @1 keeps serving your live app while you author and validate @2. Treat each compiled version as immutable — change the logic, cut a new version, re-expose, then retire the old one once traffic has moved.
// run a specific version explicitly
{ workflow_type: 'virtual.<uuid>@1', initial_data: { /* free inputs */ } }
Step 5 — Expose to end-users
A composition is just another workflow type — which means it can be exposed to your app's signed-in users through App Enablement. This is the only way end-users run anything: an end-user token can never start a raw platform workflow, only a virtual (composed) one you've explicitly exposed.
Exposure is a single workflow call (an agent on the Orkestia MCP can run it unattended):
identity.app.expose-virtual-workflow({
identity_app_uuid: "…", // your App Enablement app
composition_uuid: "…", // the composition you authored
version: 1, // the exact version to expose
})
Your frontend then invokes it as the user, presenting their session JWT as a Bearer token. Orkestia injects the end-user principal server-side and immutably — the caller cannot set or override it:
const res = await fetch('https://workflow-api.orkestia.dev/api/workflows', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${session.token}`,
},
body: JSON.stringify({
workflow_type: 'virtual.<uuid>@1',
initial_data: { /* free inputs only — e.g. a filter or page size */ },
}),
})
const { state_data } = await res.json() // returns only this user's data
The user supplies only the free inputs you left as source="input". The sensitive parts — which table, which tenant column, the connection — were fixed as static mappings when you authored the composition, and the tenant filter is forced from the user's verified identity. The caller can neither see nor widen them.
Eligible steps
end_user_eligible. Not all platform workflows are safe to run under an end-user principal — exposure fails if any step is ineligible. A scoped data step (for example a structured, allow-listed read) is bound so its tenant filter is forced from the end-user's identity, with no way for the caller to widen it. Check eligibility per workflow in the workflow types registry; the eligibility set is still expanding. (beta)The end-to-end shape of an Orkestia-backed app:
Your frontend ──► Sign in with Orkestia ──► end-user JWT
│
└──► POST /api/workflows (virtual.<uuid>@1, Bearer JWT)
│
└──► Orkestia injects the end-user principal (immutable)
│
└──► composition runs, scoped to the user → only their data
What you get
No code, full engine guarantees
Declare structure, not implementation — and still get the engine's history, retries, and concurrency control.
Typed before it runs
Structure, reference, and compatibility checks catch errors at authoring time, with precise per-step diagnostics.
Safe to expose
Static mappings hide sensitive parameters; the tenant boundary is enforced from an immutable, server-injected identity.
Versioned & deterministic
Pin a version to your live app, author the next in parallel, and diff byte-stable compiled output.
