Language Reference
Complete DSL syntax specification.
Table of Contents
- Authoring Shorthands
- Root Properties
- Metadata
- Triggers (on)
- Concurrency
- Defaults
- Permissions
- Variables
- Jobs
- Expressions
Authoring Shorthands
The DSL accepts a small set of shorthand forms so common cases stay terse. Every shorthand normalizes to the canonical form at compile time — the compiled run snapshot is the verbose form, so replays and debugging never depend on the shorthand. Authors write less; the engine sees more.
| Shorthand | Expands to | Notes |
|---|---|---|
needs: job1 | needs: [job1] | Single-dependency form |
uses: xema/agent | Resolved to the action's current version at compile time | The compiled run pins the concrete version — replays are deterministic |
Job with no with: | Treated as with: {} | Validation still rejects the job if the action's inputs schema has required fields |
Job with no outputs: | Downstream consumers see the activity's raw output envelope under needs.<job>.outputs.* | Add an explicit outputs: block when you need to rename or project |
Ranges in uses: (e.g. @1.0, ^1.0.0) are not supported — a workflow
that compiles today but resolves to a different version tomorrow would break
replay safety. Use either an exact @<semver> or omit @<ver> entirely.
Root Properties
apiVersion
Type: string
Required: Yes
Value: xema.dev/workflow/v1alpha1
Specifies the DSL version.
1apiVersion: xema.dev/workflow/v1alpha1kind
Type: string
Required: Yes
Value: Workflow
Specifies this is a workflow definition.
1kind: Workflowmetadata
Type: object
Required: Yes
Workflow metadata.
1metadata:
2 name: my-workflow # Unique name
3 version: 1.0.0 # Semantic versioning
4 description: Optional descriptionon
Type: object
Required: Yes
Trigger definitions. At least one trigger required.
concurrency
Type: object
Required: No
Concurrency control.
defaults
Type: object
Required: No
Default settings for all jobs.
permissions
Type: object
Required: No
Resource access declarations.
vars
Type: object
Required: No
Static variables.
jobs
Type: object
Required: Yes
Workflow jobs (at least one required).
Metadata
1metadata:
2 name: workflow-name # Required: unique identifier
3 version: 1.0.0 # Required: semantic version
4 description: Human-readable description # Optionalname
Type: string (alphanumeric, hyphens)
Required: Yes
Pattern: ^[a-z0-9-]+$
Unique workflow identifier within project.
version
Type: string
Required: Yes
Pattern: ^\d+\.\d+\.\d+(-[a-z0-9]+)?$
Semantic version (MAJOR.MINOR.PATCH).
description
Type: string
Required: No
Human-readable description.
Triggers (on)
At least one trigger required.
workflow_dispatch
Manual workflow trigger:
1on:
2 workflow_dispatch:
3 inputs:
4 project_name:
5 type: string
6 required: true
7 description: Project name
8 budget:
9 type: number
10 required: false
11 defaultValue: 100000
12 include_testing:
13 type: boolean
14 required: false
15 defaultValue: trueInputs Schema
1interface InputDescriptor {
2 type: 'string' | 'number' | 'integer' | 'boolean' | 'object' | 'array'
3 required: boolean
4 description?: string
5 defaultValue?: unknown
6 enum?: string[] // For closed sets
7 format?: 'multiline' // For string type
8 itemType?: string // For array type
9}schedule
Cron-based trigger:
1on:
2 schedule:
3 - cron: "0 9 * * MON" # Monday 9 AM
4 inputs:
5 report_type: weekly
6 - cron: "0 0 1 * *" # 1st of month
7 inputs:
8 report_type: monthlyCron Format: minute hour day-of-month month day-of-week
Examples:
0 9 * * MON— Monday 9 AM0 0 * * *— Daily midnight0 0 1 * *— First day of month*/15 * * * *— Every 15 minutes
webhook
External event trigger:
1on:
2 webhook:
3 - event: scm.push
4 filters:
5 projectId: proj_123
6 branch: main
7 - event: tracker.issue
8 filters:
9 type: feature
10 priority: highSupported Events:
scm.push— Code pushscm.pull_request— Pull requesttracker.issue— Issue eventdoc.page— Document event
workflow_call
Called by other workflows:
1on:
2 workflow_call:
3 inputs:
4 scope:
5 type: string
6 required: true
7 outputs:
8 summary:
9 value: ${{ needs.final.outputs.summary }}Concurrency
Control concurrent executions:
1concurrency:
2 group: requirements-${{ inputs.project_id }}
3 mode: queuegroup
Type: string
Required: Yes
Concurrency group name. Supports expressions.
mode
Type: string (enum)
Required: Yes
Values: allow | queue | cancel_in_progress | skip
allow— Multiple concurrent runsqueue— Sequential executioncancel_in_progress— Cancel old, start newskip— Skip if one running
Defaults
Default settings applied to all jobs:
1defaults:
2 retry:
3 maxAttempts: 3
4 backoffCoefficient: 2.0
5 initialInterval: 1s
6 maxInterval: 60s
7 timeout: "2h"retry
1interface RetryPolicy {
2 maxAttempts: number // Max attempts (1-10)
3 backoffCoefficient: number // Multiplier per retry (1.0-10.0)
4 initialInterval: string // Starting wait (e.g., "1s")
5 maxInterval: string // Max wait (e.g., "60s")
6}timeout
Type: string (duration)
Format: "1s" | "1m" | "1h" | "1d"
Workflow-level timeout.
Permissions
Resource access declarations:
1permissions:
2 repos: read # SCM access
3 kb: limited # Knowledge base
4 backlog: write # Issue tracking
5 artifacts: read # Artifact store
6 integrations: read # IntegrationsAvailable Scopes
| Scope | Levels | Purpose |
|---|---|---|
repos | none, read, limited, write | SCM repository access |
kb | none, read, limited, write | Knowledge base access |
backlog | none, read, limited, write | Issue/backlog access |
artifacts | none, read, limited, write | Artifact store access |
integrations | none, read, limited, write | External integrations |
Variables
Static variables:
1vars:
2 organization: "Acme Corp"
3 max_retries: 3
4 review_template: "enterprise"Access via: ${{ vars.key }}
Jobs
1jobs:
2 job-key:
3 title: Optional title
4 needs: [job1, job2] # Dependencies
5 if: ${{ condition }} # Conditional
6 strategy: # Matrix/dynamic
7 matrix:
8 os: [ubuntu, windows]
9 uses: namespace/action@version # Action ref
10 with: # Inputs
11 param: value
12 outputs: # Extract outputs
13 key: ${{ result.field }}
14 timeout: "1h" # Job timeout
15 retry: # Job retry policy
16 maxAttempts: 5Job Properties
id / name
Type: string
The job identifier (from the key).
title
Type: string
Optional human-readable title.
needs
Type: string[] (or string — single-dep shorthand)
Job dependencies. Jobs listed here must complete first.
1needs: [job1, job2] # canonical array form
2needs: job1 # shorthand — exactly equivalent to needs: [job1]if
Type: string (expression)
Conditional execution. A job runs only when its if: is true.
1if: ${{ inputs.deploy == true }}
2if: ${{ contains(needs.previous.outputs.errors, 'critical') }}
3if: ${{ always() }}Status functions read how the jobs this job depends on ended. They take no
arguments and are valid only in a job if: (the compiler refuses them in
with:, outputs:, strategy and concurrency):
| Function | True when |
|---|---|
success() | The run was not cancelled, every job in needs: succeeded, and no job upstream of it (transitively) failed or was cancelled. A direct need that was skipped makes it false. |
failure() | Some job upstream of this one (directly or transitively) failed. A matrix job counts as failed when any of its entries failed. |
cancelled() | A cancel was requested for the run. |
always() | Always. |
Default: a job with no if: behaves as if: ${{ success() }}. An if:
that names no status function is combined with it: if: ${{ expr }} compiles
to success() && (expr). So a job whose upstream failed, was cancelled or was
skipped does not run, and writing if: ${{ true }} does not override that.
To run despite an upstream outcome, say so with a status function:
1cleanup:
2 needs: [build, test]
3 if: ${{ always() }} # runs after success, failure, skip or cancel
4notify:
5 needs: [build]
6 if: ${{ failure() }} # runs only when something upstream failed
7join:
8 needs: [fast-path, slow-path] # one of them is skipped by its own `if:`
9 if: ${{ !failure() && !cancelled() }} # run unless something failed or the run was cancelledFailure and cancel semantics
- A failed job does not stop the run at once. Branches that do not depend on it
keep running, and each dependant evaluates its own
if:. When every job has settled, a run with any failed job ends failed, even if a cleanup job (always()/failure()) succeeded. - On cancel, jobs in flight are cancelled and recorded
cancelled. Jobs not yet started evaluate theirif:withcancelled()true: only those that namealways(),cancelled()orfailure()run, and they are not cut short by the cancel until the cleanup budget expires (their owntimeoutbounds them). Every other job is recordedskipped. A forced termination of the execution runs nothing.
Each entry of needs also exposes needs.<job>.outcome, one of succeeded,
failed, cancelled or skipped, for finer conditions:
1if: ${{ needs.build.outcome == 'failed' }}A bare needs.<job> (for example if: ${{ needs.draft }}) is rejected at
compile time. Use success(), or compare needs.<job>.outcome.
strategy
Parallelization strategy:
Static Matrix
1strategy:
2 matrix:
3 os: [ubuntu, windows, macos]
4 version: [20, 21, 22]
5 architecture: [x86_64, arm64]
6 maxParallel: 6 # Optional: limit concurrency
7 include: # Optional: add extra combinations
8 - os: ubuntu
9 version: 23Access via: ${{ matrix.os }}, ${{ matrix.version }}
Dynamic Matrix
1strategy:
2 dynamic:
3 from: ${{ needs.previous.outputs.items }}
4 as: item
5 maxEntries: 100 # Prevent runaway expansionAccess via: ${{ dynamic.item }}
uses
Type: string
Action reference in format: namespace/action[@version]
Examples:
xema/[email protected]— pinned to the current exact version (recommended for production)xema/agent— pins to the action's current version at compile time and freezes it into the run. The compiled run carries a concrete version, so replays remain deterministic; only a new compilation would pick up a newer current version.xema/[email protected]xema/[email protected]software-dev/[email protected]
Ranges (e.g. @1.0, ^1.0.0) are intentionally not supported — they would let the same workflow compile to different versions on different days, breaking replay safety. Use exact versions or omit @<ver> entirely.
with
Type: object
Action inputs. Supports expressions.
1with:
2 task: "Analyze requirements"
3 org: ${{ vars.organization }}
4 scope: ${{ inputs.project_scope }}
5 previous_result: ${{ needs.analyze.outputs.summary }}outputs
Type: object
Extract outputs from job result.
1outputs:
2 summary: ${{ result.summary }}
3 risks: ${{ result.risks }}
4 artifact_id: ${{ result.artifact_id }}Access downstream: ${{ needs.job_key.outputs.summary }}
timeout
Type: string (duration)
Job-specific timeout. Overrides default.
1timeout: "30m"retry
Type: object
Job-specific retry policy. Overrides default.
1retry:
2 maxAttempts: 5
3 backoffCoefficient: 1.5Expressions
Dynamic values in templates:
Syntax
${{ <expression> }}
Literals
1${{ 'string' }}
2${{ 42 }}
3${{ 3.14 }}
4${{ true }}
5${{ false }}
6${{ null }}Variable References
1${{ inputs.project_name }}
2${{ vars.organization }}
3${{ needs.job1.outputs.summary }}
4${{ matrix.os }}
5${{ dynamic.service }}
6${{ trigger.provider }}Operators
Comparison
1==, !=, <, >, <=, >=Logical
1&&, ||, !String
1+ # ConcatenationFunctions
Conditionals
1success() # Run not cancelled and every upstream job succeeded
2failure() # Some upstream job failed
3cancelled() # The run was cancelled
4always() # Always trueString
1contains(str, substr) # Check substring
2startsWith(str, prefix) # Check prefix
3endsWith(str, suffix) # Check suffixArray
1length(arr) # Array length
2first(arr) # First element
3last(arr) # Last elementType Conversion
1toNumber(str) # Convert to number
2toString(val) # Convert to string
3toJSON(val) # Convert to JSONAccess Patterns
Object Property
1${{ obj.property }}
2${{ obj['property-with-hyphen'] }}Array Element
1${{ arr[0] }} # First element
2${{ arr[-1] }} # Last element
3${{ arr[*] }} # All elements (for matrix)Context
| Context | Available In | Type |
|---|---|---|
inputs | All jobs | Input parameters |
vars | All jobs | Workflow variables |
needs | Dependent jobs | Previous job outputs |
matrix | Matrix jobs | Current matrix value |
dynamic | Dynamic jobs | Current dynamic value |
trigger | All jobs | Webhook trigger data |
result | In outputs | Current job result |
Mounts (Advanced)
Optional: Mount external resources:
1mounts:
2 - path: /artifacts
3 source:
4 kind: artifact-store-collection
5 collectionId: coll_123
6 - path: /code
7 source:
8 kind: scm-repo
9 repoRef: github.com/acme/myrepo
10 ref: main
11 - path: /specs
12 source:
13 kind: deliverable-specs
14 contractKey: enterprise-saasVersion: 1.0 — April 2026