End-user authoring
Your app's end users can build their own automations. Each one is a composition: a small workflow made of steps you allow. A person builds it, checks it, saves drafts, publishes it, runs it and puts it on a schedule, all inside their workspace.
How it works
- A workspace owns it. A composition belongs to the workspace that was active when it was saved. Other workspaces cannot see it, run it or change it. A composition of another workspace answers
NOT_FOUND, the same answer as one that does not exist. - Steps come from your catalog. You choose which workflow types your end users may use as steps. A step outside that catalog is refused.
- It runs as the person. A run of an end user's composition has that person's authority in that workspace, never more. The organization, app, person and workspace always come from the session, never from the request.
- Roles decide who does what. By default, members can build drafts and run compositions. Admins and owners can also publish and archive. You choose which groups get these rights.
The platform calls this end-user composition authoring. You turn it on per app.
Availability
Two switches must both be on:
- Your organization is on the authoring allowlist. Authoring is live for customer organizations on an allowlist. Ask Orkestia to add your organization. Until then,
identity.app.enable-authoringfails withAUTHORING_NOT_ENABLEDand nothing is written. - Your app turns it on with
identity.app.enable-authoring.
You need an app with Sign in with Orkestia end users and workspaces.
Turn it on
Three admin workflows control authoring for an app. A person who is an admin or owner of your organization must run them. Agent tokens and end users are refused. Each takes identity_app_uuid, plus actor and organization_uuid like every identity.app.* admin workflow.
1. Choose the steps
identity.app.set-authoring-catalog sets the authoring catalog: the workflow types your end users may use as steps.
| Input | Meaning |
|---|---|
workflow_types | A list of library workflow types. Required. With mode: "replace", [] empties the catalog. |
mode | replace (default), add or remove |
{
"identity_app_uuid": "<identity_app_uuid>",
"workflow_types": ["<step-type-a>", "<step-type-b>"],
"mode": "replace"
}
- Every type that
replaceoraddputs in the catalog must be marked end-user eligible by the platform. An app override fromidentity.app.set-workflow-eligibilitydoes not count, because the end user controls every input of the step. Otherwise the run fails withSTEP_NOT_AUTHORABLE,failure_details.workflow_typeslists the types, and nothing is written. removeis never checked, so you can always take a type out.- Compositions (
virtual.*) and composition references cannot be catalog entries. - You can set the catalog before or after you enable authoring.
It returns authoring_catalog (after the change), added, removed and changed.
2. Enable authoring
identity.app.enable-authoring turns authoring on.
| Input | Meaning |
|---|---|
groups | Access group slugs to grant authoring to. Default ["owner", "admin", "member"]. |
limits | Overrides of the limits, key by key. Positive integers. An unknown key is refused. |
{
"identity_app_uuid": "<identity_app_uuid>",
"groups": ["owner", "admin", "member"],
"limits": { "live_lineages_per_workspace": 10 }
}
It does three things:
- Turns authoring on for the app.
- Grants the capabilities to the chosen groups.
ownerandadminget read, run, draft and publish. Every other chosen group gets read, run and draft. Grants are only added, never removed, so grants you made yourself stay. - Publishes and exposes the app's entry points: one small composition per action, named
composer-<key>-<app12>.<app12>is the first 12 hex characters of the app's UUID, without dashes. See Entry points.
It returns authoring_enabled, was_enabled, groups, capabilities_added ({group: [capabilities]}), authoring_limits (the limits now in effect), entrypoints, published and exposed. entrypoints maps each key to {workflow_type, composition_uuid, version, name, wraps, revision, exposed}. Keep the workflow_type values: your app starts them.
- The run can take a few minutes, because it publishes the entry points before it finishes.
- It is safe to run again. A second run only adds what is missing: a capability, an entry point, or an entry point whose definition changed in a platform update. If it fails with
AUTHORING_ENTRYPOINT_FAILED, run it again and it continues where it stopped. limitsreplaces the app's earlier overrides. A key you leave out goes back to its default. Leavelimitsout to keep the current overrides.
3. Give people a workspace
Authoring works only in an active workspace of the app. Add people to a workspace with a role, as described in Workspaces. Their role in that workspace decides what they can do there.
Who can do what
Capabilities are resolved per workspace, from the person's role in their active workspace.
| Capability | Lets the person |
|---|---|
compositions:read | List and read the workspace's compositions |
compositions:run | Start a published composition of the workspace |
compositions:draft | Check compositions, see the step catalog, and save drafts of new compositions and of compositions they started |
compositions:publish | Everything compositions:draft allows, plus publish, archive, change compositions other people started, and see everyone's drafts |
With the default grant:
| Member | Admin or owner | |
|---|---|---|
| List, read and run published compositions | Yes | Yes |
| Check, see the step catalog, and save drafts of their own compositions | Yes | Yes |
| See other people's drafts | No | Yes |
| Change a composition someone else started | No | Yes |
| Publish and archive | No | Yes |
Two more rules:
- Authors keep access to their own work. A person can always read and run a composition they wrote, even without
compositions:readorcompositions:run, while they are a member of the workspace. - Drafts stay private to their author and to publishers. Without
compositions:publish, a person sees the published compositions up to their live version, plus everything they started or wrote. Anything else answersNOT_FOUND.
So a member can prepare a composition, and an admin reviews and publishes it.
Entry points
An end user's browser can start only compositions your app exposes, never a library workflow. So identity.app.enable-authoring publishes one entry point per action. Each one is a one-step composition that wraps a composition.app.* verb (or a workspace or schedule verb) and passes the caller's inputs through unchanged.
Start an entry point with the end user's token, using the workflow_type from entrypoints. The verbs read the app, the workspace and the person from the session. You never send them.
| Key | Wraps | Inputs | Needs |
|---|---|---|---|
list | composition.app.list | status, limit, cursor | compositions:read (or own compositions only) |
get | composition.app.get | composition_uuid, or lineage_name / slug with optional version | compositions:read (or the author) |
catalog | composition.app.catalog | none | compositions:draft or compositions:publish |
validate | composition.app.validate | definition (required), include_dry_run, summary_only | compositions:draft or compositions:publish |
save-draft | composition.app.save-draft | definition (required), slug, lineage_name, title, expected_version | compositions:draft or compositions:publish |
publish | composition.app.publish | definition, expected_version, idempotency_key (all required), slug, lineage_name, title | compositions:publish |
archive | composition.app.archive | composition_uuid, or lineage_name / slug with version | compositions:publish |
publish-status | composition.app.publish-status | idempotency_key (required) | The person who published |
workspaces | identity.end-user.organization.query | none. Returns items, total | — |
workspace-switch | identity.end-user.organization.switch | workspace_uuid (required). Returns active_app_organization_uuid, app_organization_ref, membership_status | A member of that workspace |
schedule-list, schedule-runs, schedule-create, schedule-update, schedule-pause, schedule-resume, schedule-delete | schedule.app.list, .runs, .create, .update, .pause, .resume, .delete | The verb's inputs. See Schedules | Changes only the person's own schedules |
Read each verb's fields from the entry point run's output. They can sit under the step's name, which is the key with - written as _ (for example save_draft).
A definition is a composition definition in the same format composition.save takes. See Compositions.
Names, titles and versions
- A composition is one lineage of numbered versions. Name it with a
slug: lowercasea-z0-9words joined by single hyphens, at most 64 characters. Without a slug, one is made fromtitle. - The stored name is
ws-<workspace id>-<slug>.listreturns it aslineage_name. You can pass it back instead of the slug. titleis the display name, at most 200 characters. A new composition needs one. Later saves keep it when you leave it out.expected_versionis the newest version your editor holds, of any status. Send0for a new composition. If someone saved a newer version meanwhile, the write is refused withVERSION_CONFLICTandhead_version. It is optional onsave-draftand required onpublish.
Check
validate writes nothing. It runs the platform's validation and the authoring rules, and tells you whether publish would accept the definition.
| Output | Meaning |
|---|---|
passed, errors, warnings | The platform's validation |
authoring | The authoring rules: {passed, findings, codes, step_count, step_types} |
findings | Each problem: {code, message, ...}, with step_name, path, workflow_type or field when known |
publishable | passed and authoring.passed. valid is the same value. |
pinned_children | Nested compositions of this workspace that a publish would pin to their live version |
The authoring rules, with the code each finding carries:
- Every step type, at every depth, is end-user eligible, and every top-level step is in the app's catalog (
STEP_NOT_AUTHORABLE). - No step sets
organization_uuid,user_uuid,end_user_uuid,identity_app_uuidorapp_organization_uuidas a fixed value. Onlyorganization_uuidmay come from the run's input (UNSCOPED_INPUT). - A nested composition is pinned to one exact version, belongs to this workspace or the organization, and is exposed to this workspace (
NESTED_NOT_IN_WORKSPACE). A nested composition of this workspace referenced as@latestis pinned for you to its live version. - The step count is within
steps_per_composition(QUOTA_EXCEEDED).
catalog lists the allowed steps in catalog order: workflow_type, name, description, authorable, and schema with input_fields and output_fields. An input field with server_controlled: true is filled by the platform: don't ask the person for it. A type that is no longer eligible stays in the list with authorable: false and a reason (not_end_user_eligible or unknown_type). catalog also returns the app's limits.
Save a draft
save-draft saves a new version with status draft. It never publishes or exposes anything. It returns composition_uuid, lineage_name, slug, title, version, head_version, status (draft), workflow_type, validation_passed, validation_errors and validation_warnings.
Publish
publish saves a new version, checks it, makes it the live version and exposes it to this workspace only, at this version only.
idempotency_key: choose one per publish attempt (for example a UUID, at most 128 characters) and reuse it on every retry of that attempt. A retry of a finished key returns the stored outcome withreplayed: true. Nothing new is written. The same key used by another person, workspace or lineage is refused withIDEMPOTENCY_KEY_REUSED.- Nested compositions of this workspace referenced as
@latestare pinned to their live version. Those that were not yet exposed to the workspace are exposed with it.
The output is the publish request:
| Field | Meaning |
|---|---|
status | published, refused, failed, or accepted (still running) |
refusal_code | The code when refused; INTERNAL_ERROR or PUBLISH_ABANDONED when failed; PUBLISH_IN_PROGRESS when accepted; null when published |
refusal | {code, message, ...} with what the refusal names: head_version, findings, errors, limit and maximum, or capability |
receipt | When published: composition_uuid, lineage_uuid, lineage_name, title, version, workflow_type, orn_pinned, published_at, request_uuid, pinned_children |
replayed | true when this key was already used |
request_uuid, idempotency_key, lineage_name, expected_version, created_at, finished_at | The request itself |
After the request is accepted, a refusal does not fail the run. It is recorded and returned with status: "refused", so publish-status can report it later. A definition that fails validation is kept as a draft and refused with VALIDATION_FAILED. Refusals found before the request is recorded fail the run instead: no workspace, authoring off for the organization, a malformed input, or another workspace's lineage_name.
If a publish times out on your side, call publish-status with the same key before you retry. It returns the same shape. A request still accepted after 15 minutes is reported as failed with PUBLISH_ABANDONED. Publish again with a new key.
List
list returns one item per lineage, newest change first, up to limit (default 50, at most 200). Pass next_cursor back as cursor for the next page. Filter with status: live, draft or retired.
Each item has lineage_name, slug, title, status, head and live (or null), step_count, updated_at, author ({mine, display_name}), exposed and availability:
availability | Meaning |
|---|---|
available | Published and served to this workspace. People can run it. |
unexposed | Published, but not served to this workspace, for example after authoring was turned off with unexpose. Publish it again to expose it. |
not_published | No live version. |
list also returns view (workspace, published or own, depending on what the person may see) and capabilities: {read, run, draft, publish} for the person in this workspace. Use capabilities to show or hide actions. author.display_name is the part of the person's email before the @. No member UUID is returned.
get returns one version with its definition, validation, head_version, live_version, exposed, author, lineage_author and capabilities.
Archive
archive retires one version: it can no longer be started, and its exposure to the workspace is removed. Schedules that fire it stop at their next firing. It returns composition_uuid, lineage_name, version, status (retired), previous_status and unexposed.
Run a published composition
A published version is a normal workflow type, virtual.<composition_uuid>@<version>. Find it in the publish receipt.workflow_type, or in live.workflow_type from list. Start it with the end user's token:
const run = await client.start("virtual.<composition_uuid>@<version>", {
// the composition's own inputs only
})
The start succeeds only when:
- the version is exposed to the person's active workspace, otherwise
403withNOT_EXPOSED_TO_END_USERS; - the person holds
compositions:runin that workspace or wrote the composition, otherwise403withCAPABILITY_REQUIREDandcapability: "compositions:run"; - a session bound to a workspace is still in that workspace, otherwise
403withWORKSPACE_MISMATCH(see Sessions bound to a workspace).
Don't send app_organization_uuid in the input. The active workspace comes from the session, and a start whose app_organization_uuid differs from it is refused with PRINCIPAL_OVERRIDE_REJECTED.
Schedules
End users schedule published compositions with the schedule.app.* workflows, through the app's composer-schedule-<verb>-<app12> entry points. A schedule runs as the person who created it, in their workspace.
| Workflow | What it does |
|---|---|
schedule.app.create | Create a schedule. Inputs: target_workflow_type, name, interval_minutes or schedule_expression with timezone, initial_data, active |
schedule.app.update | Change the name, cadence, target, inputs or active of your own schedule |
schedule.app.pause, schedule.app.resume, schedule.app.delete | Act on your own schedule |
schedule.app.list | The schedules you can see in this workspace |
schedule.app.runs | The recent runs (last 30 days) one schedule started: status and timing |
- Cadence: either
interval_minutes, orschedule_expression(five cron fields, for example0 8 * * 1-5) read intimezone, an IANA name such asAmerica/Sao_Paulo. Not more often than every 15 minutes. - Target: a composition your app exposes to the person's workspace, as
virtual.<uuid>@<version>or a composition reference. A raw workflow type is refused withschedule_target_not_exposed. - New schedules start paused unless you send
active: true. - Limits: by default a person may hold 10 schedules in an app and an app 50, paused ones included (
schedule_quota_exceeded). A cadence that is too short is refused withschedule_interval_too_short. - Ownership: people change only the schedules they created. Anything else answers
schedule_not_found.
The schedule.app.* workflows are library workflows, so the browser starts them through the schedule-* entry points. An app that turned authoring on before those entry points existed gets them by running identity.app.enable-authoring again; nothing else changes.
Skipped firings
Before each firing, the platform checks again that the creator is still an active member of the workspace and that the composition is still exposed to it. If not, it skips the firing and starts nothing. So when a person leaves the workspace, their schedules stop running.
schedule.app.list shows it on each schedule:
| Field | Meaning |
|---|---|
last_fire_skipped | true when the last occurrence started nothing |
last_error_code | Why, for example creator_not_in_workspace, workspace_inactive or target_not_exposed_to_workspace. Other values describe failed runs, such as run_failed or start_rejected. |
last_error_reason | A fixed sentence for the code, safe to show to the person |
last_run_at | When the last occurrence came due, run or skipped |
run_count | Occurrences that started a run. Skips are not counted. |
Each item also has schedule_uuid, name, target, interval_seconds, schedule_expression, timezone, active, next_run_at, last_status, consecutive_failures, paused_reason, locked_by_admin and mine. initial_data appears only on the person's own schedules.
schedule.app.runs lists only runs, so it adds last_skip: {at, code, reason} for the latest skip, or null. Only the latest skip is kept.
A person who holds schedules:manage in the workspace can also pause, resume, edit the timing of, archive, reassign or delete other people's schedules with schedule.app.admin.*. An admin pause locks the schedule, so its creator cannot resume it until an admin unlocks it. identity.app.enable-authoring does not grant schedules:manage.
Refusal codes
A refused verb fails its run. failure_reason starts with the code, for example VERSION_CONFLICT: the composition changed: .... On the verb's own run, failure_details holds {code, ...} with what the refusal names. When you call the verb through an entry point, failure_details may be missing from the entry point's run, so read the code from the start of failure_reason. publish is the exception: once a request is recorded, it returns the code in refusal_code instead of failing.
| Code | Meaning | What your app should do |
|---|---|---|
AUTHORING_NOT_ENABLED | The organization is not on the allowlist, or the app has not enabled authoring | Hide the authoring screens. Ask an admin to run identity.app.enable-authoring. |
WORKSPACE_REQUIRED | The session has no active workspace | Let the person pick a workspace (workspace-switch) |
WORKSPACE_MISMATCH | The active workspace is not an active workspace of this app. At start, a session bound to another workspace (see reason). | Switch workspace, or refresh the session for the active one |
CAPABILITY_REQUIRED | The person lacks the capability named in capability in this workspace | Show the screen read-only. A workspace admin can publish for them. |
NOT_FOUND | No such composition or publish in this workspace, or one the person may not see | Refresh the list |
VERSION_CONFLICT | Someone saved a newer version; head_version names it | Reload with get, let the person review, then retry with the new expected_version |
PUBLISH_IN_PROGRESS | A publish with this key is still running | Poll publish-status with the same key |
VALIDATION_FAILED | The definition does not validate; on publish it was kept as a draft. errors lists why. | Show the errors and let the person fix them |
STEP_NOT_AUTHORABLE | A step is not in the app's catalog or not end-user eligible | Remove the step. An admin can add an eligible type to the catalog. |
UNSCOPED_INPUT | A step sets or takes a platform-controlled field | Remove that mapping |
NESTED_NOT_IN_WORKSPACE | A nested composition is not pinned to one version, not in this workspace, or not exposed to it | Publish the nested composition first, then retry |
QUOTA_EXCEEDED | A limit is reached; limit and maximum name it | Tell the person. Archive old work, or raise the limit. |
INVALID_NAME | The slug, lineage_name or title is malformed, or a new composition has no title | Ask for a valid name |
RESERVED_NAME | The ws- name prefix is reserved for workspace compositions | Use another name |
IDEMPOTENCY_KEY_REUSED | The key was used for another publish | Publish with a new key |
PUBLISH_ABANDONED | A publish never finished | Publish again with a new key |
INTERNAL_ERROR | The publish failed for a reason that is not a refusal. If exposing failed, the version was kept as a draft. | Publish again with a new key |
AUTHORING_ENTRYPOINT_FAILED | identity.app.enable-authoring could not publish an entry point | Run it again |
Limits
| Limit | Default |
|---|---|
live_lineages_per_workspace: published compositions in one workspace | 25 |
versions_per_lineage: versions of one composition | 200 |
steps_per_composition: steps in one composition | 40 |
publishes_per_person_per_day: successful publishes per person in 24 hours | 50 |
drafts_per_person: drafts one person keeps in a workspace | 100 |
Change them with limits on identity.app.enable-authoring. A limit that is reached refuses with QUOTA_EXCEEDED. Schedules have their own limits.
The ready-made editor
@orkestia/composer is an open-source (MIT) React editor for compositions: the list, each composition as steps or a diagram, checks, runs and schedules. Its appBackend uses the entry points above.
It is not on npm. Install the release tarball from GitHub, and pin the version you tested:
pnpm add https://github.com/orkestia/orkestia-composer/releases/download/v0.1.2/orkestia-composer-0.1.2.tgz
react and react-dom 19.2 or later are peer dependencies.
import { appBackend, CompositionEditor } from '@orkestia/composer'
import '@xyflow/react/dist/style.css'
import '@orkestia/composer/style.css'
// endUserClient runs workflows with the end user's token.
const backend = appBackend(endUserClient)
export function Automations() {
return <CompositionEditor backend={backend} host={{ scopeKey: `${appId}:${workspaceId}` }} />
}
appBackend finds the composer-<key>-<app12> entry points among your organization's compositions. If more than one app in your organization has authoring on, pass appBackend(endUserClient, { app12 }), where app12 is the first 12 hex characters of your identity app's UUID, or pass entryPoints with the workflow_type values identity.app.enable-authoring returned. The step picker offers only your app's authorable steps, and the editor offers only what the person's capabilities in the workspace allow. See the repository README for the client interface, theming, languages and schedules.
Not in this phase
- Event triggers: an end user's composition starts by hand or on a schedule, not on an event.
- Step-by-step run logs for end users.
schedule.app.runsgives status and timing only. - A calendar source by date range.
- Checks at publish time that a referenced vault credential is usable.
Turn it off
identity.app.disable-authoring turns authoring off for the app. The composition.app.* verbs then refuse with AUTHORING_NOT_ENABLED. It works even if your organization has left the allowlist, so you can always roll back.
| Input | Meaning |
|---|---|
unexpose | Default false. With true, also remove every workspace exposure. Those compositions can no longer be started, and schedules that fire them stop at their next firing. |
{ "identity_app_uuid": "<identity_app_uuid>", "unexpose": true }
It returns authoring_enabled (false), changed, unexpose and unexposed ([{composition_uuid, version, app_organization_uuid}]). It is safe to run again.
Nothing is deleted. Compositions, the catalog, the limits, the capability grants and the app-wide entry points stay. Running identity.app.enable-authoring again turns authoring back on as it was. Workspace exposures removed with unexpose do not come back: those compositions show availability: "unexposed" until someone publishes them again.
Workspaces
A workspace is a tenant inside your app — a farm, a company, an office — that members join with a role, so data can belong to a group instead of one person.
Operate
Watch an app in production — a public readiness probe for sign-in, continuation grants for work that outlives a session, and where logs, hosting usage and troubleshooting live.
