Source Authoring
YAML Source is the current standard for authoring Network Storage resources. Use YAML for endpoints, workflows, collections, tests, and shared libraries. Prefer...
# Source Authoring
YAML Source is the current standard for authoring Network Storage resources. Use YAML for endpoints, workflows, collections, tests, and shared libraries. Prefer `.yml` for new source filenames. `.yaml` is also accepted and treated the same.
Runtime API request and response bodies are still JSON. The YAML format only defines resources that Network Storage compiles and runs.
## File Extensions
Use these extensions for new source-authored files. `.yml` is preferred for new work; `.yaml` remains fully supported and equivalent.
| Resource | Preferred YAML Source | Also accepted | Legacy, deprecated |
|----------|-----------------------|---------------|--------------------|
| Endpoint | `endpoints/*.endpoint.yml` or `endpoints/*.yml` | `endpoints/*.endpoint.yaml` or `endpoints/*.yaml` | `endpoints/*.json` |
| Workflow | `workflows/*.workflow.yml` or `workflows/*.yml` | `workflows/*.workflow.yaml` or `workflows/*.yaml` | `workflows/*.json` |
| Collection | `collections/*.collection.yml` or `collections/*.yml` | `collections/*.collection.yaml` or `collections/*.yaml` | `collections/*.json` |
| Test | `tests/*.test.yml` or `tests/*.yml` | `tests/*.test.yaml` or `tests/*.yaml` | `tests/*.json` |
| Library | `libraries/*.library.yml` or `libraries/*.yml` | `libraries/*.library.yaml` or `libraries/*.yaml` | n/a |
## YAML Subset
Network Storage accepts a predictable YAML subset:
- top-level mappings
- nested mappings and arrays
- strings, numbers, booleans, and null
- comments
- quoted or unquoted scalar values
These YAML features are intentionally not supported:
| Feature | Reason | Use instead |
|---------|--------|-------------|
| Anchors and aliases (`&name`, `*name`) | They hide what is compiled | Duplicate the value or use a source library |
| Merge keys (`<<:`) | They make resource shape implicit | Write the merged fields explicitly |
| Custom tags (`!Tag`) | They are not portable to runtime JSON | Plain mappings, arrays, and scalars |
## Source Envelope
Every YAML Source file starts with an explicit envelope.
```yml
sourceVersion: "1"
kind: endpoint
```
| Field | Required | Values | Notes |
|-------|----------|--------|-------|
| `sourceVersion` | yes | `"1"` | Pinned source format version. `dslVersion` is also accepted by the low-level DSL and is normalized to `sourceVersion`. |
| `kind` | yes | `endpoint`, `workflow`, `collection`, `test`, `library` | `resourceKind`, plural forms, `endpoint-test`, and `shared-library` are accepted aliases where APIs pass them through. |
| `imports` | no | array of library ids | Available on endpoint and workflow sources for `call` steps. |
| `resource` | no | mapping | Optional wrapper. Fields inside `resource:` are merged with top-level fields. |
| `definition` | no | mapping | Optional wrapper for imported or exported definitions. Fields inside `definition:` are merged with top-level fields. |
| `<kind>` | no | mapping | Optional wrapper named after the resource kind, such as `endpoint:` or `workflow:`. |
| `notes` | no | string | Author-facing text. Preserved with the resource and ignored by runtime. |
## Accepted YAML Layouts
Prefer flat top-level YAML for new source files:
```yml
sourceVersion: "1"
kind: endpoint
slug: award-match-xp
method: POST
input: {}
steps: []
response: { status: 200, body: { ok: true } }
```
Legacy wrapper layouts still compile and load. The backend merges wrapper contents into the same canonical definition:
```yml
sourceVersion: "1"
kind: endpoint
definition:
slug: award-match-xp
method: POST
input: {}
steps: []
response:
status: 200
body:
ok: true
```
`resource:`, `definition:`, and kind-named wrappers such as `endpoint:` are accepted for compatibility. Backend source-upgrade flows currently accept the `flat` and `definition-wrapper` layouts, then write back flat canonical YAML when the upgrade is unambiguous and safe to auto-write.
## Endpoint Source
Endpoint YAML defines a callable server-side pipeline.
```yml
sourceVersion: "1"
kind: endpoint
name: Award Match XP
slug: award-match-xp
method: POST
enabled: true
skipSboxAuth: false
requiresSecretKey: false
requireReauth: false
input:
type: object
properties:
matchId:
type: string
xp:
type: number
required:
- matchId
- xp
steps:
- id: amount
type: transform
expression: "clamp({{input.xp}}, 0, 250)"
- id: save
type: write
collection: player_data
key: "{{playerKey}}"
ops:
- op: inc
path: xp
value: "{{amount}}"
source: match
reason: "Match {{input.matchId}}"
response:
status: 200
body:
ok: true
xpAwarded: "{{amount}}"
```
Endpoint responses can also use `echo` to include selected step outputs without repeating each field in `body`:
```yml
response:
status: 200
body:
ok: true
echo:
- "{{amount}}"
- "{{player.xp}}"
```
### Endpoint Fields
| Field | Required | Type | Default | Notes |
|-------|----------|------|---------|-------|
| `id` | no | string | resource id or `slug` | Internal id when present. |
| `name` | no | string | `slug` | Display name. |
| `slug` | no | string | `id` or `endpoint` | URL segment used by `/v3/endpoints/{projectId}/{slug}`. |
| `method` | no | `POST` or `GET` | `POST` | Method accepted by the endpoint route. |
| `description` | no | string | empty | User-facing description. |
| `enabled` | no | boolean | `true` | Disabled endpoints are not callable. |
| `skipSboxAuth` | no | boolean | `false` | Allows external calls without s&box token verification. |
| `requiresSecretKey` | no | boolean | `false` | Requires a verified Network Storage secret key on endpoint calls. Use for dedicated-server-only actions. |
| `requireReauth` | no | boolean | `false` | Requires a fresh auth flow when enforced by the route. |
| `authPolicy` | no | object | none | Advanced auth policy metadata. |
| `input` | no | schema object | empty object schema | Validates request JSON before steps run. |
| `steps` | no | array | `[]` | Ordered runtime pipeline. |
| `let` | no | object | none | Template aliases for repeated long paths. Example: `chum: player.activeChum` lets templates use `{{chum.expiresAtUnixSeconds}}`. |
| `response` | no | object | `200` with `{ ok: true }` | Templates are resolved after queued writes succeed. |
| `imports` | no | array | `[]` | Library ids used by `call` steps. |
| `notes` | no | string | none | Preserved metadata. |
| `exposure` | no | `public`, `internal` | `public` for endpoints | Internal endpoints are reusable flows and are not callable through the public endpoint route. |
### Endpoint Runtime Input
Endpoint calls receive JSON request input and auth metadata from the request. Steps can reference:
| Template | Value |
|----------|-------|
| `{{input.field}}` | Request body field after input validation. |
| `{{steamId}}` | Calling player's Steam ID, or the on-behalf-of Steam ID in proxy mode. |
| `{{playerKey}}` | Default record key. Uses `steamId` or `steamId_saveId` when the project uses player-save keys. |
| `{{_hasSecretKey}}` | `true` when the request includes a verified Network Storage secret key. |
| `{{_isDedicatedServer}}` | Alias of `_hasSecretKey` for dedicated-server workflows. |
| `{{values.group.key}}` | Game Value constants and tables for the project. |
| `{{now}}` | ISO timestamp for the execution. |
| `{{_unixMs}}`, `{{_unixS}}` | Unix time in milliseconds or seconds. |
| `{{_dateUTC}}`, `{{_timeUTC}}`, `{{_datetimeUTC}}` | UTC date/time strings from the same execution instant. |
| `{{_year}}`, `{{_month}}`, `{{_day}}`, `{{_hour}}`, `{{_minute}}`, `{{_dayOfWeek}}` | UTC date parts. |
| `{{stepId}}`, `{{stepId.field}}` | Output from a previous step. |
`steamId` and `playerKey` are reserved runtime identity fields. Nested `run` calls inherit them from the caller, and route params or returned values cannot overwrite them.
### Endpoint Runtime Output
Normal execution returns:
```yml
ok: true
status: 200
body:
ok: true
timing:
total: 12
steps:
# per-step timings appear here when present
storageDelta: 128
```
Failures return `ok: false`, an HTTP `status`, a JSON `body.error`, and `timing`. Expected game-logic rejections from `condition` or workflow failures include `conditionRejection: true`; author them with 4xx-style statuses, because 5xx statuses are reserved for internal server failures and are normalized away from server-error reporting. Dry-run tests include `_dryRun.steps`, `_dryRun.pendingWrites`, `_dryRun.pendingWebhooks`, `traceBytes`, `maxTraceBytes`, and `truncated`.
## Workflow Source
Workflows are reusable pipelines called from endpoint steps. They can validate, compute, read, write, delete, call other workflows, and return values.
```yml
sourceVersion: "1"
kind: workflow
id: validate_purchase
name: Validate Purchase
description: Shared purchase validation
params:
player:
type: object
item_id:
type: string
steps:
- id: item
type: lookup
source: values
table: shop_items
where:
field: item_id
op: "=="
value: "{{item_id}}"
- id: can_afford
type: condition
check:
field: "{{player.gold}}"
op: ">="
value: "{{item.cost}}"
onFail:
status: 403
errorCode: NOT_ENOUGH_GOLD
message: "Not enough gold."
returns:
item: "{{item}}"
cost: "{{item.cost}}"
```
### Workflow Fields
| Field | Required | Type | Default | Notes |
|-------|----------|------|---------|-------|
| `id` | no | string | resource id or generated id | Referenced by endpoint `workflow` steps. |
| `name` | no | string | `id` | Display name. |
| `description` | no | string | empty | User-facing description. |
| `params` | no | object | `{}` | Declares values the caller should pass. |
| `steps` | no | array | `[]` | Multi-step workflow body. |
| `returns` | no | object | all step outputs | Values mapped back to the caller under the workflow step id. |
| `condition` | no | condition | none | Legacy condition-only workflow mode. |
| `requires` | no | object | none | Legacy requirement metadata. |
| `onFail` | no | object | none | Fail action defaults for condition-only workflows. |
| `imports` | no | array | `[]` | Library ids used by `call` steps. |
| `notes` | no | string | none | Preserved metadata. |
### Calling A Workflow
```yml
steps:
- id: player
type: read
collection: player_data
key: "{{playerKey}}"
- id: purchase_check
type: workflow
workflow: validate_purchase
params:
player: "{{player}}"
item_id: "{{input.itemId}}"
```
The result is available as `{{purchase_check.item}}`, `{{purchase_check.cost}}`, and any other returned fields.
### Route Fields
Condition steps support explicit true/false route outcomes. `onTrue` and `onFalse` are accepted as source aliases, but saved/compiled output prefers `routes.true` and `routes.false`.
```yml
- id: gate
type: condition
check:
field: "{{input.mode}}"
op: ==
value: cached
routes:
true:
action: return
status: 200
body: { ok: true, cached: true }
false:
action: run
flow: rebuild_cache
as: rebuild
```
Route actions are `continue`, `reject`, `return`, `goto`, and `run`. `goto` targets a step id in the current flow. `run` targets a workflow or internal endpoint and executes in-process with the caller context; it does not make a public HTTP call or perform auth twice.
Backward `goto` edges and recursive `run` edges must be intentionally bounded. `route.maxVisits` and `route.maxTransitions` tell validation that a branch is expected to revisit earlier logic, but they are only metadata. Actual runtime safety comes from `retry.maxAttempts`, the default step-visit cap of 20, the route-transition cap of 1000, the nested-flow depth cap of 4, and the overall wall-time budget.
### Sleep and Retry
Use `sleep` only for short bounded polling or retries. The runtime rejects sleep durations above the configured maximum and still enforces the endpoint wall-time budget.
```yml
- id: wait
type: sleep
durationMs: 250
- id: poll_ready
type: condition
check: { field: "{{status.ready}}", op: ==, value: true }
retry:
maxAttempts: 5
sleepMs: 250
maxElapsedMs: 1500
routes:
true: { action: continue }
false: { action: goto, step: wait }
```
Execution limits for route-aware polling:
- `durationMs` / `retry.sleepMs`: **0-1000ms** each
- `retry.maxAttempts`: **1-10**
- default per-step visits without retry bounds: **20**
- nested reusable-flow depth: **4**
- total route transitions: **1000**
- total execution wall time: **30000ms**
Read-like steps inside polling loops use the normal per-execution read cache unless configured with `refresh: true` or `refreshCache: true`. Fresh reads still count against read limits.
## Collection Source
Collection YAML defines storage shape, constants, and tables.
```yml
sourceVersion: "1"
kind: collection
name: player_data
description: Player progression and inventory
collectionType: per-steamid
accessMode: endpoint
visibility: private
maxRecords: 1
allowRecordDelete: false
requireSaveVersion: true
rateLimits:
mode: none
rateLimitAction: reject
schema:
type: object
required:
- xp
properties:
xp:
type: number
min: 0
_ledger: true
inventory:
type: array
items:
type: object
constants:
- key: combat.xp_per_win
value: 100
tables:
- name: shop_items
columns:
- key: id
type: string
- key: cost
type: number
rows:
- id: potion
cost: 25
```
### Collection Fields
| Field | Required | Type | Default | Notes |
|-------|----------|------|---------|-------|
| `id` | no | string | resource id or `name` | Internal id when present. |
| `name` | no | string | `id` | Collection name used by endpoint steps. |
| `description` | no | string | empty | User-facing description. |
| `collectionType` | no | `per-steamid`, `global` | `per-steamid` | Per-player documents or shared global records. |
| `accessMode` | no | `endpoint` | `endpoint` | Collections are endpoint/query controlled; direct collection APIs require secret keys. Legacy `public` values are normalized to `endpoint`. |
| `visibility` | no | `private` | `private` | Collections are private implementation detail behind endpoints/queries. Legacy values are normalized to `private`. |
| `maxRecords` | no | number | `1` | Save-slot count for per-player collections. UI clamps to `1` through `50`. |
| `allowRecordDelete` | no | boolean | `false` | Allows explicit record deletion where exposed by the UI/API. |
| `requireSaveVersion` | no | boolean | `false` | Adds and increments save-version fields on endpoint writes. |
| `schema` | no | object | `{}` | Lightweight schema used on writes. |
| `rateLimits` | no | object | `{ mode: "none" }` | Collection-level save limits. |
| `rateLimitAction` | no | `reject`, `clamp` | `reject` | Action for collection save limits. |
| `webhookOnRateLimit` | no | boolean/string/object | none | Rate-limit notification metadata where configured. |
| `constants` | no | array | `[]` | Server-authored constants exposed under `values`. |
| `tables` | no | array | `[]` | Server-authored row tables exposed under `values`. |
| `notes` | no | string | none | Preserved metadata. |
### Schema Fields
The validator supports:
| Schema field | Applies to | Notes |
|--------------|------------|-------|
| `type` | all fields | `object`, `array`, `string`, `number`, `boolean`, `player`, `playerSave`, `datetime`. |
| `required` | object | Array of required child property names. |
| `properties` | object | Child field definitions. Optional for free-form object maps. |
| `additionalProperties` | object | Schema for dynamic object-map values, such as upgrade-id to level maps. |
| `items` | array | Item schema. |
| `min`, `max` | number | Numeric bounds. |
| `_ledger` | number | Enables immutable ledger audit entries for changes. |
| `_maxPerMin`, `_maxPerHour`, `_maxPerDay`, `_maxPerWeek`, `_maxPerYear` | number | Legacy rate-tracking metadata. |
| `_unique` | metadata | Preserved metadata. |
Extra stored fields are allowed during validation so older saved data can continue to exist after schema changes.
## Test Source
Tests define endpoint fixtures for validation and benchmark-friendly dry runs.
```yml
sourceVersion: "1"
kind: test
id: award_match_xp_basic
name: Award match XP basic
endpoint: award-match-xp
method: POST
input:
matchId: match_123
xp: 75
inputMode:
steamId: "76561198000000000"
mockData:
player_data:
xp: 10
expect:
outcome: pass
status: 200
body:
ok: true
skipWebhooks: true
tags:
- smoke
```
### Test Fields
| Field | Required | Type | Default | Notes |
|-------|----------|------|---------|-------|
| `id` | no | string | resource id or `name` | Stable test id. |
| `name` | no | string | `id` or `Source Test` | Display name. |
| `description` | no | string | empty | User-facing description. |
| `endpoint` | no | string | none | Endpoint slug or id under test. |
| `method` | no | string | `POST` | Request method. |
| `input` | no | object | `{}` | Request body used for the dry run. |
| `inputMode` | no | object | `{}` | Caller/auth fixture metadata. |
| `mockData` | no | object | `{}` | Collection mock data keyed by collection name. |
| `expect` | no | object | `{ outcome: "pass" }` | Expected outcome/status/body metadata. |
| `skipWebhooks` | no | boolean | false | Collect webhook payloads without sending them. |
| `tags` | no | array | `[]` | Test grouping metadata. |
## Library Source And Imports
Libraries provide reusable blocks for endpoint and workflow sources. A block body can contain the same step types as an endpoint/workflow.
```yml
sourceVersion: "1"
kind: library
id: economy
blocks:
can_afford:
requiredInputs:
- player
- cost
body:
- id: enough
type: return_on
condition:
field: "{{player.gold}}"
op: ">="
value: "{{cost}}"
status: 403
error: NOT_ENOUGH_GOLD
message: "Not enough gold."
```
Use it from an endpoint or workflow:
```yml
sourceVersion: "1"
kind: endpoint
slug: buy-item
imports:
- economy
steps:
- id: afford
type: call
block: economy.can_afford
inputs:
player: "{{player}}"
cost: "{{item.cost}}"
```
### Library Fields
| Field | Required | Type | Default | Notes |
|-------|----------|------|---------|-------|
| `id` | no | string | resource id | Import id used by `imports`. |
| `blocks` | no | object | `{}` | Block definitions keyed by block name. |
| `blocks.<name>.requiredInputs` | no | array | `[]` | Inputs that must be supplied by `call.inputs`. |
| `blocks.<name>.body` | no | array | `[]` | Source steps inlined at compile time. |
## Step Basics
Every step must have:
| Field | Required | Notes |
|-------|----------|-------|
| `id` | yes | Unique step id in the local context. Later steps reference it as `{{id}}` or `{{id.field}}`. |
| `type` | yes | One of the runtime or source-only step types. |
| `as` | no | Supported by read/lookup/filter/lookup_many/random_select result assignment. Defaults to `id`. |
| `notes` | no | Author-facing comment preserved through compilation where possible. |
Endpoint and workflow execution is ordered. Read-like steps run immediately. Write and delete steps are queued and committed after all steps pass, so validation can reject before storage changes are made.
## Runtime Step Types
### `read`
Loads one record from a collection.
```yml
- id: player
type: read
collection: player_data
key: "{{playerKey}}"
```
| Field | Required | Notes |
|-------|----------|-------|
| `collection` | yes | Collection name or id. |
| `key` | yes | Record key template. `{{steamId}}_default` resolves to the first save slot where applicable. |
| `as` | no | Result context key. |
Output: stored record object or `null`.
### `lookup`
Finds the first matching row from Game Values or a collection scan.
```yml
- id: item
type: lookup
source: values
table: shop_items
where:
field: id
op: "=="
value: "{{input.itemId}}"
```
| Field | Required | Notes |
|-------|----------|-------|
| `source` | yes | `values` for Game Values; any other value scans `collection`. |
| `table` | values source | Game Values table name. |
| `collection` | collection source | Collection name or id. |
| `where` | yes | Condition leaf, `all`/`any` group, or array of leaves. Arrays use AND logic. |
| `as` | no | Result context key. |
Output: matched row/record or `null`.
### `filter`
Returns every matching row from Game Values or a collection scan.
```yml
- id: unlocked_items
type: filter
source: values
table: shop_items
where:
field: level
op: "<="
value: "{{player.level}}"
limit: 20
```
| Field | Required | Notes |
|-------|----------|-------|
| `source` | yes | `values` or collection source. |
| `table` | values source | Game Values table name. |
| `collection` | collection source | Collection name or id. |
| `where` | yes | Condition leaf, `all`/`any` group, or array of leaves. Arrays use AND logic. |
| `limit` | no | Preserved metadata; current runner returns all matches. |
| `as` | no | Result context key. |
Output: array of matched rows/records.
### `lookup_many`
Fetches several rows by key from Game Values or a collection scan.
```yml
- id: items
type: lookup_many
source: values
table: shop_items
keyField: id
keys: "{{input.itemIds}}"
asMap: true
```
| Field | Required | Notes |
|-------|----------|-------|
| `source` | yes | `values` or collection source. |
| `table` | values source | Game Values table name. |
| `collection` | collection source | Collection name or id. |
| `keyField` | no | Field to match. Defaults to `id`. |
| `keys` or `values` | yes | Array or template resolving to an array. |
| `asMap` | no | Defaults to `true`. Use `false` to return an array in the same order as keys. |
| `as` | no | Result context key. |
Output: object keyed by requested key, or an array when `asMap: false`. Missing rows are `null`.
### `random_select`
Filters rows and returns one random row, optionally weighted.
```yml
- id: reward
type: random_select
source: values
table: rewards
where:
- field: tier
op: "=="
value: "{{input.tier}}"
- field: disabled
op: "!="
value: true
weightField: weight
```
| Field | Required | Notes |
|-------|----------|-------|
| `source` | yes | `values` or collection source. |
| `table` | values source | Game Values table name. |
| `collection` | collection source | Collection name or id. |
| `where` | yes | Condition leaf, `all`/`any` group, or array of leaves. Arrays use AND logic. |
| `weightField` | no | Numeric weight field. If omitted, selection is uniform. |
| `as` | no | Result context key. |
Output: selected row/record or `null`.
### `condition`
Checks a condition. On failure it can reject, skip steps, clamp input, flag, or fire a fail webhook.
```yml
- id: enough_gold
type: condition
check:
field: "{{player.gold}}"
op: ">="
value: "{{item.cost}}"
onFail:
status: 403
errorCode: NOT_ENOUGH_GOLD
message: "Not enough gold."
```
| Field | Required | Notes |
|-------|----------|-------|
| `check` | required unless `workflow` is used | Condition object. |
| `workflow` | no | Legacy condition-only workflow id. |
| `bindings` | no | Map workflow variable names to existing step ids for condition-only workflow mode. |
| `onFail` | no | Fail action object. |
Output: `true` or `false` in context when execution continues.
### `assert`
Fails immediately when a condition is false.
```yml
- id: has_gold
type: assert
check:
field: "{{player.gold}}"
op: ">="
value: "{{input.cost}}"
status: 403
errorCode: NOT_ENOUGH_GOLD
message: You need more gold.
```
| Field | Required | Notes |
|-------|----------|-------|
| `check` | yes | Condition object. |
| `status` | no | Defaults to `400`. |
| `errorCode` or `error` | no | Machine-readable error code. Defaults to `ASSERTION_FAILED`. |
| `message` or `errorMessage` | no | Human-readable failure message. |
Output: `true` when the assertion passes.
### `transform`
Returns either a resolved template/literal value or a math expression result.
```yml
- id: reward_xp
type: transform
expression: "floor({{player.level}} * 12.5)"
```
```yml
- id: display_name
type: transform
value: "{{default(input.name, 'Unknown')}}"
```
| Field | Required | Notes |
|-------|----------|-------|
| `expression` | one of `expression`, `value` | Safe math expression. |
| `value` | one of `expression`, `value` | Literal or template-resolved value. |
Output: the computed value.
### `object`
Builds one object from named fields, templates, and literals.
```yml
- id: response_meta
type: object
fields:
steamId: "{{steamId}}"
mode: "{{input.mode}}"
rewardsGranted: true
```
| Field | Required | Notes |
|-------|----------|-------|
| `fields` | yes | JSON/YAML object. Values are resolved deeply. |
Output: object containing the resolved fields.
### `array`
Builds one ordered array from templates, literals, and nested values.
```yml
- id: reward_list
type: array
items:
- starter_pack
- "{{input.extraReward}}"
- kind: gold
amount: "{{input.gold}}"
```
| Field | Required | Notes |
|-------|----------|-------|
| `items` | yes | Array resolved deeply in order. |
Output: resolved array.
### `merge`
Shallow-merges one or more objects into a new object. Later sources overwrite earlier properties.
```yml
- id: payload
type: merge
sources:
- "{{player}}"
- title: Rookie
grantedRewards: "{{reward_list}}"
```
| Field | Required | Notes |
|-------|----------|-------|
| `sources` | yes | Array of objects or object templates. Resolved left to right. |
Output: merged object.
### `sort`
Sorts an array into a new ordered array. The original array is not mutated.
```yml
- id: leaderboard
type: sort
source: "{{input.entries}}"
by: score
direction: desc
```
| Field | Required | Notes |
|-------|----------|-------|
| `source` | yes | Template resolving to an array. |
| `by` | no | Plain item field path such as `score` or `stats.rank`. |
| `expression` | no | Math/template expression evaluated with `item` and `index`. Overrides `by` when present. |
| `direction` | no | `asc` or `desc`. Defaults to `asc`. |
Output: sorted array.
### `switch`
Chooses the first matching case value from an ordered list of conditions.
```yml
- id: reward_tier
type: switch
cases:
- when:
field: "input.mode"
op: "=="
value: admin
then: admin
- when:
field: "input.gold"
op: ">="
value: 100
then: rich
default: starter
```
| Field | Required | Notes |
|-------|----------|-------|
| `cases` | yes | Ordered array of objects with `when` and `then`. |
| `default` | no | Fallback value when nothing matches. |
Output: the first matching `then` value, or `default`, or `null` when neither is present.
### `compute`
Builds an object from several fields. Fields can reference earlier fields in the same compute output as `{{stepId.field}}`.
```yml
- id: reward
type: compute
values:
base:
value: "{{values.combat.base_xp}}"
bonus:
expression: "floor({{base}} * 0.25)"
total:
expression: "{{base}} + {{bonus}}"
```
| Field | Required | Notes |
|-------|----------|-------|
| `values` or `fields` | yes | Object of output fields. |
| `mode` | no | Use `template` when string field definitions should resolve as templates instead of math. |
Each field definition can be an object with `expression`, an object with `value`, a nested object/array, a string, or a literal.
Output: object containing all resolved fields.
### `random`
Generates random values.
```yml
- id: roll
type: random
mode: int
min: 1
max: 101
```
| Mode | Fields | Output |
|------|--------|--------|
| `int` | `min`, `max` | Integer in `[min, max)`. Defaults: `0`, `100`. |
| `float` | `min`, `max`, `precision` | Float in `[min, max]`. Defaults: `0`, `1`, precision `2`. |
| `weighted` | `source`, `table`, `from`, `items`, `where`, `weightField` | Weighted item from a values table, previous array, or inline array. |
| `beta` | `alpha`, `beta`, `min`, `max`, `precision` | Beta-distributed number. Defaults: `1`, `1`, `0`, `1`, precision `2`. |
### `workflow`
Executes a saved workflow.
```yml
- id: purchase
type: workflow
workflow: validate_purchase
params:
player: "{{player}}"
item_id: "{{input.itemId}}"
```
| Field | Required | Notes |
|-------|----------|-------|
| `workflow` | yes | Workflow id. |
| `params` | no | Values passed into multi-step workflows. |
| `bindings` | no | Legacy alias for condition-only workflow variable bindings. |
Output: workflow `returns` object, or all workflow step outputs when `returns` is omitted.
### `write`
Queues changes to a collection record. All queued writes commit after the pipeline passes.
```yml
- id: save_player
type: write
collection: player_data
key: "{{playerKey}}"
ops:
- op: inc
path: gold
valueExpression: "{{reward.total}}"
source: reward
reason: "Awarded by endpoint"
when:
field: "{{reward.total}}"
op: ">"
value: 0
```
| Field | Required | Notes |
|-------|----------|-------|
| `collection` | yes | Collection name or id. |
| `key` | yes | Record key template. |
| `ops` | yes | Non-empty array of operations. |
| `rateLimitBypasses` | no | Object keyed by rate-limit rule id. Each value is a condition that bypasses that rule when true. |
Operation fields:
| Operation | Required fields | Notes |
|-----------|-----------------|-------|
| `set` | `path`, `value` | Sets a field. Numeric increases count for rate limits. |
| `inc` | `path`, numeric `value` | Increments a number. Positive increments count for rate limits. |
| `push` | `path`, `value` | Appends to an array, creating it if needed. |
| `pull` | `path`, `match` | Removes array items matching all object fields, or primitive items matching `match.value`. |
| `remove` | `path`, `value` | Removes primitive values or object values matching all supplied fields. |
| `delete` | `path`, `value` | Alias for `remove` inside write operations. |
Every operation can also use:
| Field | Notes |
|-------|-------|
| `valueExpression` | Math expression used to produce `value`. |
| `expression` | Legacy alias for `valueExpression`. |
| `when` | Condition object or boolean. Operation is skipped when false. |
| `if` | Alias for `when`. |
| `source` | Ledger metadata, truncated to 64 characters. |
| `reason` | Ledger metadata, truncated to 256 characters. |
Output: writes do not expose a context value during normal execution. Test/dry-run output lists pending writes and resolved operations.
### `delete`
Queues deletion of an entire collection record.
```yml
- id: delete_save
type: delete
collection: player_data
key: "{{playerKey}}"
```
| Field | Required | Notes |
|-------|----------|-------|
| `collection` | yes | Collection name or id. |
| `key` | yes | Record key template. |
| `when` | no | Preserved by source schema; runtime delete is normally gated with a preceding `condition`. |
Output: deletion result appears in write results/dry-run pending writes.
### `webhook`
Sends a Discord webhook embed.
```yml
- id: alert
type: webhook
url: "{{values.alerts.discord_webhook}}"
title: "Suspicious reward"
description: "Player {{steamId}} requested {{input.amount}}"
color: "ffcc00"
fields:
- name: Endpoint
value: award-match-xp
inline: true
```
| Field | Required | Notes |
|-------|----------|-------|
| `url` | yes at runtime | Must be a Discord webhook URL. |
| `target` | accepted by schema | Older docs used `target`; current runner sends to `url`. |
| `title` | no | Embed title template. |
| `description` or `message` | no | Runner uses `description`; schema also preserves `message`. |
| `color` | no | Hex color without `#`. Defaults to Discord blurple. |
| `fields` | no | Array of `{ name, value, inline }`. |
Output: `{ ok, status }`, or `{ ok, status, skipped, payload }` when webhooks are skipped in tests.
### `response`
Visual-editor compatibility step. Runtime skips it. Define endpoint output with the top-level `response` field instead.
## Source-Only Step Types
Source-only steps compile to runtime steps. The runtime never sees these types directly.
### `foreach`
Unrolls a loop over an array or object path.
```yml
- id: each_item
type: foreach
over: "{{input.items}}"
as: item
indexAs: item_index
maxIterations: 10
body:
- id: save_item
type: write
collection: inventory
key: "{{playerKey}}"
ops:
- op: push
path: items
value: "{{item}}"
```
| Field | Required | Default | Notes |
|-------|----------|---------|-------|
| `over` | yes | none | Template or path resolving to iterable data. |
| `as` | yes | none | Binding name for current item. |
| `indexAs` | no | none | Binding name for zero-based index. |
| `maxIterations` | no | `64` | Clamped to `1..64`. |
| `body` | yes | none | Non-empty array of source steps. |
### `while`
Repeats a body while the condition remains true.
```yml
- id: consume_queue
type: while
condition:
field: "{{queue_remaining}}"
op: ">"
value: 0
maxIterations: 8
body:
- id: queue_remaining
type: transform
expression: "{{queue_remaining}} - 1"
```
| Field | Required | Notes |
|-------|----------|-------|
| `condition` | yes | Condition checked before each unrolled iteration. |
| `maxIterations` | yes | Positive number, clamped to `1..32`. |
| `body` | yes | Non-empty array of source steps. |
### `until`
Repeats a body until the condition becomes true. It compiles like `while` with an inverted guard.
```yml
- id: roll_until_rare
type: until
condition:
field: "{{roll.rarity}}"
op: "=="
value: rare
maxIterations: 5
body:
- id: roll
type: random_select
source: values
table: rewards
where:
field: enabled
op: "=="
value: true
```
### `return_on`
Runs optional body steps, then returns early when `condition` is true.
```yml
- id: reject_if_banned
type: return_on
condition:
field: "{{player.banned}}"
op: "=="
value: true
status: 403
error: BANNED
message: "This account is banned."
```
| Field | Required | Notes |
|-------|----------|-------|
| `condition` | yes | Return when true. Compiler emits an inverted runtime `condition` fail. |
| `status` | no | Response status for the early return. |
| `error` | no | Error code. |
| `message` | no | Error message template. |
| `body` | no | Steps to run before the return check. |
### `call`
Inlines an imported library block.
```yml
- id: afford
type: call
block: economy.can_afford
inputs:
player: "{{player}}"
cost: "{{item.cost}}"
```
| Field | Required | Notes |
|-------|----------|-------|
| `block` | yes | Dotted library block name, such as `economy.can_afford`. |
| `inputs` | no | Object of values bound to block `requiredInputs` and templates. |
The compiler prefixes inlined block step ids with the call step id to avoid collisions.
## Conditions
Conditions support leaf checks, `all`, and `any`.
```yml
field: "{{player.gold}}"
op: ">="
value: "{{item.cost}}"
```
`field` can also be written as `left`, and `value` can also be written as `right`.
```yml
all:
- field: "{{player.level}}"
op: ">="
value: 10
- any:
- field: "{{player.role}}"
op: "=="
value: admin
- field: "{{player.vip}}"
op: "=="
value: true
```
| Operator | Aliases | Notes |
|----------|---------|-------|
| `==` | `eq` | Loose equality, useful for number/string comparisons. |
| `!=` | `neq`, `ne` | Loose inequality. |
| `>` | `gt` | Numeric comparison. |
| `<` | `lt` | Numeric comparison. |
| `>=` | `gte`, `ge` | Numeric comparison. |
| `<=` | `lte`, `le` | Numeric comparison. |
| `contains` | `includes`, `has` | String substring or array contains. |
| `in` | none | Right-hand string or array contains the field value. |
| `not_contains` | `not_includes`, `not_has`, `notcontains` | Negated contains. |
| `not_in` | `notin` | Negated `in`. |
| `starts_with` | `startswith` | String prefix. |
| `not_starts_with` | `notstartswith` | Negated string prefix. |
| `exists` | none | Field is not `undefined` and not `null`. |
| `not_exists` | `not_exist`, `notexists` | Field is missing or null. |
## Fail Actions
`condition.onFail` and workflow `onFail` accept these fields:
| Field | Type | Notes |
|-------|------|-------|
| `reject` | boolean | Defaults to rejecting. Set `false` to continue after failure. |
| `status` | number | HTTP status on rejection. Defaults to `200` for game-logic failures. |
| `errorCode` or `error` | string | Error code returned in `body.error.code`. |
| `errorMessage`, `message` | string | Template-resolved message returned in `body.error.message`. |
| `severity` | string | Returned as response metadata. |
| `flag` | boolean | Returned as `flagged` response metadata and logged by workflow execution. |
| `webhook` | boolean/object | Sends fail webhook where configured. |
| `clamp` | boolean | For simple input max checks, clamps the checked `input.<field>` value instead of rejecting. |
| `action` | string | Use `skip` to skip upcoming steps. |
| `skip` | number | Number of following steps to skip. |
| `skip` | string | `next` skips one step; any other string skips the step with that id. |
| `skip` | array | Skips steps with matching ids. |
| `skipSteps` or `steps` | number/string/array | Aliases for `skip`. |
| `skip.until` | string | Skip until the named step id is reached. |
| `skip.count` | number | Object form for skipping a number of following steps. |
Example skip gate:
```yml
- id: optional_guard
type: condition
check:
field: "{{input.runBonus}}"
op: "=="
value: true
onFail:
action: skip
skip: 2
```
## Templates
Templates use `{{path.to.value}}`. If the whole string is one template and the value is a number, boolean, array, or object, the raw value is returned. If a string contains multiple templates, the result is a string.
```yml
value: "{{player.gold}}"
reason: "Spent {{item.cost}} on {{input.itemId}}"
```
Supported helpers inside templates:
| Helper | Example | Notes |
|--------|---------|-------|
| `default(value, fallback)` | `{{default(input.name, 'Unknown')}}` | First non-null, non-empty value. |
| `coalesce(value, fallback)` | `{{coalesce(player.nickname, player.name, 'Player')}}` | Alias-style fallback helper. |
| `num(value, fallback)` | `{{num(input.amount, 0)}}` | Converts to number with fallback. |
| `get(root, path..., fallback)` | `{{get(player, 'inventory', input.slot, null)}}` | Safely reads a dynamic nested path. |
Template details:
- `{{-path}}` negates a numeric value.
- Nested template paths are supported, such as `{{player.inventory.{{input.slot}}.itemId}}`.
- Forbidden path segments are `__proto__`, `constructor`, and `prototype`.
- Maximum path depth is 10.
- Maximum template nesting depth is 8.
## Math Expressions
Math expressions are used by `transform.expression`, `compute` fields, `write.ops[].valueExpression`, and random bounds.
Supported operators:
| Operator | Meaning |
|----------|---------|
| `+` | add |
| `-` | subtract or unary negative |
| `*` | multiply |
| `/` | divide |
| `%` | modulus |
| `()` | grouping |
Supported functions:
| Function | Arguments | Notes |
|----------|-----------|-------|
| `floor(x)` | 1 | Round down. |
| `ceil(x)` | 1 | Round up. |
| `round(x)` | 1 | Nearest integer. |
| `min(a, b, ...)` | 2+ | Minimum. |
| `max(a, b, ...)` | 2+ | Maximum. |
| `sum(collection, path)` | 1-2 | Sum array/object items; optional field path or lambda selector. |
| `avg(collection, path)` | 1-2 | Average array/object items; optional field path or lambda selector. Empty collections return `0`. |
| `count(collection, predicate)` | 1-2 | Count array/object items; optional lambda predicate. |
| `any(collection, predicate)` | 1-2 | True when at least one array/object item matches. |
| `all(collection, predicate)` | 1-2 | True when every array/object item matches. |
| `first(collection, predicate)` | 1-2 | First item, optionally matching a lambda predicate. |
| `find(collection, predicate)` | 1-2 | Alias for first matching item. |
| `pluck(collection, path)` | 2 | Return an array of selected field/lambda values. |
| `abs(x)` | 1 | Absolute value. |
| `random(min, max)` | 2 | Random integer including max. |
| `pow(base, exponent)` | 2 | Power. |
| `log10(x)` | 1 | Base-10 log of a positive finite number. |
| `clamp(value, min, max)` | 3 | Clamp value to range. |
| `now()` | 0 | Current Unix milliseconds. |
| `nowS()` | 0 | Current Unix seconds. |
| `hour()` | 0 | Current UTC hour. |
| `dayOfWeek()` | 0 | Current UTC day, Sunday is 0. |
| `diffMs(a, b)` | 2 | `a - b` in milliseconds. |
| `diffS(a, b)` | 2 | `(a - b) / 1000`. |
Aggregate functions accept arrays or object maps and let endpoints avoid hard-coded index lists:
```yml
- id: inventory_totals
type: compute
values:
total_value: "sum({{player.inventory}}, Value)"
unsold_count: "count({{player.inventory}}, item => item.sold == false)"
has_legendary: "any({{player.inventory}}, item => item.rarity == \"legendary\")"
first_rod: { value: "{{first(player.inventory, item => item.type == \"rod\")}}" }
value_list: { value: "{{pluck(player.inventory, Value)}}" }
```
## Budgets And Limits
| Limit | Value | Applies to |
|-------|-------|------------|
| Runtime steps | 500 | Compiled endpoint/workflow steps. |
| Read-like steps | 25 | `read`, `lookup`, `filter`, `lookup_many`, `random_select` across endpoint and workflows. |
| Queued write/delete steps | 100 | Endpoint execution. |
| Write operations | 200 | Resolved write ops plus deletes. |
| Wall time | 30000 ms | Endpoint execution. |
| Workflow nesting | 8 | Nested `workflow` calls. |
| Debug trace | 64 KB | Dry-run/test trace before truncation. |
| Collection scan | 500 records | Lookup/filter/random collection scans. |
| `foreach` compile expansion | 64 iterations | Per source-only loop. |
| `while`/`until` compile expansion | 32 iterations | Per source-only loop. |
| Canonical plan nodes | 500 | Source compiler budget. |
| Source iterations | 256 | Sum of source loop max iterations in canonical plan. |
| Source nested calls | 8 | `call` plus `workflow` count in canonical plan. |
## Management API
The Management API accepts source-authored resources by sending `authoringMode`, `sourceFormat`, and `sourceText` with the resource.
```yml
authoringMode: source
sourceFormat: yaml
sourcePath: endpoints/award-match-xp.endpoint.yml
sourceText: "sourceVersion: \"1\"\nkind: endpoint\nname: Award Match XP\nslug: award-match-xp\n\
method: POST\nsteps: []\n"
```
`sourceFormat` accepts `yaml` or `yml`. A `.yml` or `.yaml` `sourcePath` also selects YAML Source.
Responses preserve authoring metadata and include compiler output:
| Field | Notes |
|-------|-------|
| `authoringMode` | `source` for YAML Source resources. |
| `sourceFormat` | `yaml` or `yml`. |
| `sourceVersion` | Source version string. |
| `sourceText` | Normalized source text. |
| `sourceObject` | Parsed source object. |
| `sourcePath` | Authored source path when supplied. System-generated paths are hidden at API/view boundaries. |
| `canonicalDefinition` | Normalized resource definition used by the compiler. |
| `canonicalHash` | Hash of canonical definition. |
| `executionPlan` | Runtime plan, source maps, dependency list, and budgets. |
| `executionPlanHash` | Hash of execution plan. |
| `diagnostics` | Errors, warnings, source paths, suggested fixes, and budget details. |
| `compilerFingerprint` | Compiler/schema/runtime version fingerprint. |
| `sourceHash` | Hash of the source text. |
| `dependencyHash` | Hash of source imports. |
| `compileStatus` | `compiled`, `stale`, or `recompile_failed`. |
| `compiledAt` | ISO compile timestamp. |
| `previousKnownGoodPlan` | Last valid plan retained after failed recompilation. |
## Compiled Runtime Output
Compiled runtime output strips source-only metadata such as `sourceText`, `sourceObject`, hashes, diagnostics, compiler fingerprints, and execution plans before execution.