Orkestia
Blog
Guides

Creating & Exposing Virtual Workflows

Compose existing workflows into a validated, versioned virtual workflow and expose it to your app's end-users through App Enablement

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 composition substrate is the same one DGI and visual builders use to author workflow DAGs programmatically — see Building with DGI. Authoring by hand (this guide) and authoring by AI converge on the identical validated representation.

The mental model

A composition is built from three pieces. Layers run in order; steps within a layer can run together.

PieceRole
LayerA stage of the composition. Layers execute sequentially; steps inside one layer are concurrent.
StepOne existing workflow type to invoke (e.g. aws.s3.create_bucket), placed in a layer.
Input mappingWhere 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.bucket is threaded from s1's output ("source": "step") — that data dependency is what places s2 in a later layer.
  • tagging uses "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_name uses "source": "input" — a free input the caller supplies at run time.
Workflow type names like 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).

PhaseChecksExample failure
StructureLayers acyclic, step IDs unique, mappings well-formedduplicate step id s1
ReferencesEvery {provider}.{service}.{operation} actually exists in the catalogaws.s3.put_bucket_taging — unknown type
CompatibilityEvery InputMapping resolves to a compatible source field/typestep.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'?"
}
The catalog is a snapshot at compile time: validation runs against the workflow types installed in the process that compiles. A type added after compile won't be visible to that compile; a type removed after compile will still be in the emitted config and fail at engine dispatch. Pin library versions consistently across authoring and runtime environments. (beta)

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

Every step in an exposed composition must be 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.

Next

App Enablement

Wire Sign in with Orkestia and end-user data for your app.

Building with DGI

Let an AI brain design the composition for you, then compile it deterministically.

Hybrid execution model

How AI design compiles down to efficient deterministic runs.

Workflow types registry

The capabilities you can chain — and which are end-user eligible.