Compositions — use, invoke, share
A composition (a virtual workflow) is your business logic as structure, not code: layers of existing catalog workflows, with every input wired from input, step, or static. After composition.save it is a normal type:
virtual.<composition_uuid>@<version>
That is the only kind of workflow you can share with an app end-user. Raw catalog types are not startable with an end-user JWT.
Contract on reference.orkestia.dev/composition. Authoring detail: Creating & exposing virtual workflows. To put a closed-set gate in front of later steps, use Typed decisions with TypeSafe as the first layer.
What you do
- Author — JSON, DGI, the console DAG builder, or
ltinteg-devkit vw. - Validate / save —
composition.validatethencomposition.save(org-member token or MCP). - Invoke — start
virtual.<uuid>@Nas yourself to prove it. - Share —
identity.app.expose-virtual-workflowso your app's users can start that version. - Enable the user — they sign in with
@orkestia/authand your UI starts the virtual with their JWT.
Do not pass organization_uuid or actor. The server fills those from the token.
1. Author and save
input_mapping is an object. Keys are the step's parameter names. Live dialect:
{
"name": "list-my-orders",
"layers": [
{
"name": "query",
"steps": [
{
"name": "rows",
"workflow_type": "data.appdata.record.query",
"input_mapping": {
"table": { "source": "static", "value": "orders" },
"page_size": { "source": "input", "field_name": "page_size" }
}
}
]
}
]
}
source | Who sets it | Use for |
|---|---|---|
static | You, at authoring time | Table, connection, tenant column — the user must not see or change this |
input | The caller at invoke time | Free inputs only (a filter, a page size) |
step | A previous step's output | Chain layers |
// MCP or org-member SDK
composition.validate({ definition })
composition.save({ name: "list-my-orders", definition })
// → { composition_uuid, workflow_type: "virtual.<uuid>@1", version: 1 }
composition.version appends @2 on the same lineage. composition.activate re-validates and sets active. Browse ops on reference — Composition domain.
Every step you will expose must be end_user_eligible in the catalog. Expose fails with ineligible_steps otherwise.
2. Invoke (you)
Org member, API token, or MCP — same type name:
import { LtIntegWorkflowsClient } from "@ltinteg/workflows-sdk"
const client = new LtIntegWorkflowsClient({
baseUrl: "https://workflow-api.orkestia.dev",
token: process.env.ORKESTIA_TOKEN,
})
const run = await client.start("virtual.<composition_uuid>@1", {
page_size: 20,
})
const { state_data } = await run.wait()
REST: POST https://workflow-api.orkestia.dev/api/workflows/start with Authorization: Bearer … and { "workflow_type": "virtual.<uuid>@1", "initial_data": { … } }.
MCP: start_workflow("virtual.<uuid>@1", { … }) after whoami().
3. Share with an app user
Provision the app first (Sign in with Orkestia / agent prompt). Then expose that version:
identity.app.expose-virtual-workflow({
identity_app_uuid: "…", // from identity.app.provision
composition_uuid: "…", // from composition.save
version: 1,
})
Revoke with identity.app.unexpose-virtual-workflow (same ids; version optional).
An end-user token can start only virtuals you exposed — never data.appdata.record.query by catalog name.
4. Enable the user to use it
In their browser: Sign in, then start the virtual with their JWT. Never put an org-member token in the page.
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", {
page_size: 20, // free inputs only
})
Orkestia injects the end-user principal immutably. Ownership / workspace on App Data is forced from that identity.
App user → @orkestia/auth → JWT
→ start virtual.<uuid>@N
→ only their rows
Paste this into your agent
Copy into Cursor, Claude, or any client connected to https://mcp.orkestia.dev/mcp:
Enable this app's users to run an Orkestia composition.
You are connected to the Orkestia MCP (https://mcp.orkestia.dev/mcp).
If you are not, stop: https://docs.orkestia.dev/reference/mcp-integration
1. Call whoami() first. Do not ask me for organization_uuid.
2. If I do not already have an identity app, follow
https://docs.orkestia.dev/app-enablement#paste-this-into-your-agent
3. Ask me what the composition should do, and the free inputs vs static secrets.
4. Author a definition using input_mapping objects (not input_mappings arrays):
{ "<param>": { "source": "input"|"step"|"static", "field_name"?, "step"?, "value"? } }
Every step I will expose must be end_user_eligible.
5. start_workflow("composition.validate", { definition }). Fix errors.
6. start_workflow("composition.save", { name, definition }).
Take composition_uuid, workflow_type, version from the terminal output.
7. Invoke it once as me: start_workflow("virtual.<uuid>@<version>", free inputs).
8. Share with app users:
start_workflow("identity.app.expose-virtual-workflow", {
identity_app_uuid, composition_uuid, version
})
9. Wire the frontend: createOrkestiaAuth({ clientKey }) + start the virtual
with session.token. Never put an org token in the browser.
10. Tell me composition_uuid, version, workflow_type, and the files you changed.
static mappings are how you share a capability without sharing a credential. The user never sees the connection or the table name.