Language Reference

Complete DSL syntax specification.

Table of Contents

  1. Authoring Shorthands
  2. Root Properties
  3. Metadata
  4. Triggers (on)
  5. Concurrency
  6. Defaults
  7. Permissions
  8. Variables
  9. Jobs
  10. 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.

ShorthandExpands toNotes
needs: job1needs: [job1]Single-dependency form
uses: xema/agentResolved to the action's current version at compile timeThe 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.

yaml
1apiVersion: xema.dev/workflow/v1alpha1

kind

Type: string
Required: Yes
Value: Workflow

Specifies this is a workflow definition.

yaml
1kind: Workflow

metadata

Type: object
Required: Yes

Workflow metadata.

yaml
1metadata:
2  name: my-workflow              # Unique name
3  version: 1.0.0                 # Semantic versioning
4  description: Optional description

on

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

yaml
1metadata:
2  name: workflow-name                    # Required: unique identifier
3  version: 1.0.0                         # Required: semantic version
4  description: Human-readable description # Optional

name

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:

yaml
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: true

Inputs Schema

typescript
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:

yaml
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: monthly

Cron Format: minute hour day-of-month month day-of-week

Examples:

  • 0 9 * * MON — Monday 9 AM
  • 0 0 * * * — Daily midnight
  • 0 0 1 * * — First day of month
  • */15 * * * * — Every 15 minutes

webhook

External event trigger:

yaml
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: high

Supported Events:

  • scm.push — Code push
  • scm.pull_request — Pull request
  • tracker.issue — Issue event
  • doc.page — Document event

workflow_call

Called by other workflows:

yaml
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:

yaml
1concurrency:
2  group: requirements-${{ inputs.project_id }}
3  mode: queue

group

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 runs
  • queue — Sequential execution
  • cancel_in_progress — Cancel old, start new
  • skip — Skip if one running

Defaults

Default settings applied to all jobs:

yaml
1defaults:
2  retry:
3    maxAttempts: 3
4    backoffCoefficient: 2.0
5    initialInterval: 1s
6    maxInterval: 60s
7  timeout: "2h"

retry

typescript
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:

yaml
1permissions:
2  repos: read                      # SCM access
3  kb: limited                      # Knowledge base
4  backlog: write                   # Issue tracking
5  artifacts: read                  # Artifact store
6  integrations: read               # Integrations

Available Scopes

ScopeLevelsPurpose
reposnone, read, limited, writeSCM repository access
kbnone, read, limited, writeKnowledge base access
backlognone, read, limited, writeIssue/backlog access
artifactsnone, read, limited, writeArtifact store access
integrationsnone, read, limited, writeExternal integrations

Variables

Static variables:

yaml
1vars:
2  organization: "Acme Corp"
3  max_retries: 3
4  review_template: "enterprise"

Access via: ${{ vars.key }}


Jobs

yaml
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: 5

Job 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.

yaml
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.

yaml
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):

FunctionTrue 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:

yaml
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 cancelled

Failure 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 their if: with cancelled() true: only those that name always(), cancelled() or failure() run, and they are not cut short by the cancel until the cleanup budget expires (their own timeout bounds them). Every other job is recorded skipped. 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:

yaml
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
yaml
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: 23

Access via: ${{ matrix.os }}, ${{ matrix.version }}

Dynamic Matrix
yaml
1strategy:
2  dynamic:
3    from: ${{ needs.previous.outputs.items }}
4    as: item
5    maxEntries: 100                  # Prevent runaway expansion

Access 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.

yaml
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.

yaml
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.

yaml
1timeout: "30m"

retry

Type: object

Job-specific retry policy. Overrides default.

yaml
1retry:
2  maxAttempts: 5
3  backoffCoefficient: 1.5

Expressions

Dynamic values in templates:

Syntax

${{ <expression> }}

Literals

yaml
1${{ 'string' }}
2${{ 42 }}
3${{ 3.14 }}
4${{ true }}
5${{ false }}
6${{ null }}

Variable References

yaml
1${{ inputs.project_name }}
2${{ vars.organization }}
3${{ needs.job1.outputs.summary }}
4${{ matrix.os }}
5${{ dynamic.service }}
6${{ trigger.provider }}

Operators

Comparison

yaml
1==, !=, <, >, <=, >=

Logical

yaml
1&&, ||, !

String

yaml
1+              # Concatenation

Functions

Conditionals

yaml
1success()      # Run not cancelled and every upstream job succeeded
2failure()      # Some upstream job failed
3cancelled()    # The run was cancelled
4always()       # Always true

String

yaml
1contains(str, substr)      # Check substring
2startsWith(str, prefix)    # Check prefix
3endsWith(str, suffix)      # Check suffix

Array

yaml
1length(arr)    # Array length
2first(arr)     # First element
3last(arr)      # Last element

Type Conversion

yaml
1toNumber(str)  # Convert to number
2toString(val)  # Convert to string
3toJSON(val)    # Convert to JSON

Access Patterns

Object Property

yaml
1${{ obj.property }}
2${{ obj['property-with-hyphen'] }}

Array Element

yaml
1${{ arr[0] }}           # First element
2${{ arr[-1] }}          # Last element
3${{ arr[*] }}           # All elements (for matrix)

Context

ContextAvailable InType
inputsAll jobsInput parameters
varsAll jobsWorkflow variables
needsDependent jobsPrevious job outputs
matrixMatrix jobsCurrent matrix value
dynamicDynamic jobsCurrent dynamic value
triggerAll jobsWebhook trigger data
resultIn outputsCurrent job result

Mounts (Advanced)

Optional: Mount external resources:

yaml
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-saas

Version: 1.0 — April 2026