# Reference syntax

> The formal grammar for $. references, string interpolation, and when expressions — with the run-state vocabulary they evaluate against.

References are how a task reads a parameter or an upstream task's output. Conditions are how a task decides whether to run at all. Both are given here as complete grammars, because a syntax without a grammar cannot be parsed reliably by a person writing a regex or by a model generating a document.

Both grammars are in **ISO/IEC 14977 EBNF**. Terminals are quoted; `{ }` is zero-or-more; `[ ]` is optional.

## Shared lexical rules

```
identifier  = ( letter | digit ) { letter | digit | "_" | "-" } ;
letter      = "A" … "Z" | "a" … "z" ;
digit       = "0" … "9" ;
ws          = { " " | "\t" } ;
```

Identifiers match the `id` patterns used elsewhere in the specification, so any task, parameter, or asset id is a legal path segment without escaping.

## References

A reference addresses a value in the run state. There are two syntactic positions, and the distinction matters because they return different things.

```
value-reference = "$." path ;
interpolation   = "${" path "}" ;

path            = params-path | task-path | workflow-path | task-self-path ;

params-path     = "params." identifier ;
task-path       = "tasks." identifier "." task-field ;
task-field      = "outputs." identifier
                | "status" ;
workflow-path   = "workflow." ( "id" | "version" ) ;
task-self-path  = "task." ( "id" | "attempt" ) ;
```

### Value references

When a JSON string consists **entirely** of a value reference, the whole string is replaced by the referenced value, **preserving its type**.

```json
{
  "inputs": {
    "input_key":   "$.tasks.ingest.outputs.output_key",
    "add_captions": "$.params.add_captions"
  }
}
```

`add_captions` resolves to a boolean, not the string `"true"`. This is why the whole-string form exists: it is the only way to pass a non-string parameter through.

### Interpolation

When a reference appears **inside** a larger string, it uses the brace form and is substituted as text.

```json
{
  "outputs": {
    "output_key": "jobs/${workflow.id}/${task.id}/output.mp4"
  }
}
```

Interpolated values are stringified. A boolean becomes `true`, a number its decimal form, and an array or object is a validation error — you cannot interpolate a structure into a path.

### Escaping

A literal dollar sign is written `$$`.

```json
{ "inputs": { "label": "Cost: $$40 per hour" } }
```

`$$` is the only escape. There is no backslash escaping, because JSON already owns the backslash.

### What is deliberately absent

Stated so you do not go looking:

- **No array indexing or filters.** No `[0]`, no `[*]`, no `[?(...)]`. If you need an element, name an output that holds it.
- **No arithmetic.** References read; they do not compute.
- **No nested references.** `$.params.${...}` is invalid.
- **No cross-document references.** A reference resolves within one document. Cross-document linkage is a [relationship](/relationships), not a reference.
- **No scatter/gather construct.** Fan-out over a collection is modelled as explicit tasks, which keeps per-item status, cost, and disclosure individually addressable — see [Implementation considerations](/implementation-considerations#fanning-work-out-over-a-collection).

> **This is not JSONPath.** The syntax borrows JSONPath's `$.` sigil and nothing else. Do not reach for a JSONPath library to evaluate it — the grammar above is the whole language, and it is small enough to parse directly.

## Run state vocabulary

Conditions frequently test a task's status, so the status values are part of the specification rather than left to an implementation.

| Status | Meaning |
|---|---|
| `pending` | Not yet eligible; dependencies incomplete |
| `running` | Started, not finished |
| `succeeded` | Completed successfully |
| `failed` | Completed unsuccessfully, after any retries |
| `skipped` | Not run, because its `when` evaluated false or a dependency was skipped |

`$.tasks.<id>.status` resolves to one of exactly these five strings.

Note the interaction: a task depending on a `skipped` task is itself `skipped`, not `pending`. Skip propagates. That is what stops an optional branch from blocking the graph.

## `when` expressions

`when` decides whether a task runs. If the expression evaluates false, the task is `skipped`.

```
expression   = or-expr ;
or-expr      = and-expr { ws "||" ws and-expr } ;
and-expr     = unary   { ws "&&" ws unary } ;
unary        = [ "!" ws ] primary ;
primary      = "(" ws expression ws ")"
             | function
             | comparison
             | operand ;

comparison   = operand ws comp-op ws operand ;
comp-op      = "==" | "!=" | "<=" | ">=" | "<" | ">" ;

function     = "exists" "(" ws value-reference ws ")" ;

operand      = value-reference | literal ;
literal      = string | number | boolean | "null" ;
string       = "'" { character - "'" } "'" ;
number       = [ "-" ] digit { digit } [ "." digit { digit } ] ;
boolean      = "true" | "false" ;
```

**Precedence**, tightest first: `!` → comparison operators → `&&` → `||`. Parentheses override.

### Semantics

- **Type discipline.** Comparisons are between like types. Comparing a string to a number is a validation error, not a coercion. `==` and `!=` work on all types; the ordering operators (`<`, `<=`, `>`, `>=`) apply only to numbers.
- **Strings are single-quoted.** Double quotes would need escaping inside JSON.
- **`exists()`** returns true when the reference resolves to a value that is present and not `null`. This is the intended answer to the optional-upstream problem: a task that consumes an output which may or may not have been produced.
- **An unresolvable reference is a validation error**, not a false. A typo should fail loudly at validation, not quietly skip a task at run time.

### Examples

```
$.params.add_captions == true
```

```
$.tasks.qc.status == 'succeeded' && $.params.territory != 'JP'
```

```
exists($.tasks.caption.outputs.captions_key)
```

```
!($.tasks.grade.status == 'failed') && $.params.rounds <= 3
```

The third example is the one worth internalising. A task that consumes an optional upstream output should gate on `exists()` rather than assuming the output is there — otherwise the document claims a dependency it cannot guarantee.

## A worked pair

A conditional caption step, and a publish step that tolerates its absence:

```json
{
  "params": {
    "add_captions": { "type": "boolean", "required": false, "default": false }
  },
  "tasks": [
    {
      "id": "caption",
      "type": "work",
      "executor": { "type": "agent" },
      "ai_role": "generative",
      "performed_by": "caption_agent",
      "depends_on": ["edit"],
      "when": "$.params.add_captions == true",
      "used": ["master_mp4"],
      "produced": ["captions_vtt"],
      "failure_mode": "SKIP_DEPENDENTS"
    },
    {
      "id": "publish",
      "type": "delivery",
      "executor": { "type": "saas_api" },
      "performed_by": "youtube_api",
      "depends_on": ["edit", "caption"],
      "when": "$.tasks.edit.status == 'succeeded'",
      "used": ["master_mp4"],
      "inputs": {
        "captions_key": "$.tasks.caption.outputs.captions_key",
        "include_captions": "exists($.tasks.caption.outputs.captions_key)"
      },
      "produced": ["publish_record"],
      "failure_mode": "HALT"
    }
  ]
}
```

`publish` depends on `caption` so ordering is defined, but gates on `edit` succeeding rather than on `caption` succeeding — and passes an `exists()` result so the executor knows whether captions are actually available. The alternative, listing `captions_vtt` in `used[]` unconditionally, would assert a lineage edge to an asset that may never exist.

## Implementer's note

The grammars above are small on purpose. A complete recursive-descent parser for both is a few hundred lines, and that is the intended cost — a language small enough to implement correctly beats a large one implemented three incompatible ways.

If you are generating documents rather than parsing them, the practical rules are:

- Whole-value reference for typed values; `${...}` only inside a larger string.
- Single-quoted strings in conditions.
- Gate on `exists()` whenever an upstream output is conditional.
- Keep conditions to one or two clauses. A condition that needs parentheses is usually a task that should be split.
