# Workflow JSON

This guide is for builders, integrators, and LLM agents that need to author
Amon Sul workflow matrices as JSON.

The authoritative runtime contracts live in `@amon-sul/amon-contracts`:

- `workflow/action-config` validates action config, worker-action input contracts, output contracts, and input mappings.
- `workflow/sequence-input-schema` validates sequence-start JSON Schema contracts.
- `workflow/matrix-bundle` validates importable matrix bundles.

Download the [minimal matrix bundle](https://docs.amonsul.io/downloads/minimal-matrix-bundle.json).

## Mental Model

Amon Sul workflow JSON has three layers:

1. **Actions** are executable units. An action is one `CALL`, `CHECK`, `CODE`, or `ODATA` operation.
2. **Sequences** are ordered chains of actions. They define how caller inputs and previous outputs flow into each action.
3. **Matrices** are policy. They attach sequences to statuses and decide the next status from exit conditions.

Sequences produce facts. Matrices interpret those facts.

## Action Types

Every action record has the same outer shape:

```json
{
  "$key": "submit_claim",
  "matrix_id": "claim_matrix",
  "code": "SUBMIT_CLAIM",
  "name": "Submit claim",
  "type": "CALL",
  "description": "Submits the claim to the external warranty system.",
  "config": {},
  "input_contract": {},
  "output_contract": {}
}
```

Use `$key` values as local references inside the bundle. Import resolves these
keys to real database ids.

### CALL

Use `CALL` for HTTP calls through the integration layer.

Canonical keys:

- `system_code`: exact connection code resolved in the execution lane.
- `path`: relative API path or path template.
- `method`: `GET`, `POST`, `PUT`, `DELETE`, or `PATCH`.
- `headers`, `pathParams`, `queryParams`, `body`: optional request data.
- `responseMapping`: maps output names to response paths.
- `successCriteria`: HTTP and business success checks.
- `errorMapping`: response paths for error message/code.

Use `system_code` and a relative `path`. Both are required to execute or Test;
incomplete drafts may omit them. Full and authority URLs are rejected, including
after template substitution. Do not use `system_id`, `external_system_id`,
`external_system_code`, or `url`.

See the [CALL example](https://docs.amonsul.io/downloads/call.json).

### CHECK

Use `CHECK` for business-rule validation.

The `rules` array is JSON rules engine compatible. Rule events should put
workflow routing facts in `event.params`, especially:

- `exit_condition`
- `severity`
- `message`

See the [CHECK example](https://docs.amonsul.io/downloads/check.json).

### CODE

Use `CODE` for small custom transformations or calculations.

The config stores JavaScript in `code` and optional static `parameters`.
CODE runs from saved configuration in the local QuickJS sandbox; it does not
require a deployed action worker.

See the [CODE example](https://docs.amonsul.io/downloads/code.json).

### ODATA

Use `ODATA` for SAP/OData operations.

Common keys:

- `system_code`
- `path` (relative service path; metadata IDs stay in local editor state)
- `entity_set`
- `operation`
- `target_type`
- `key_fields`
- `select`
- `expand`
- `filter`
- `body`
- `successCriteria`

See the [ODATA example](https://docs.amonsul.io/downloads/odata.json).

## Worker-Action Input And Output Contracts

Worker-action input contracts remain field maps describing the data each action receives:

```json
{
  "claimId": {
    "required": true,
    "type": "string",
    "description": "Claim id supplied by the caller."
  }
}
```

Supported worker-action field `type` values are:

- `string`
- `number`
- `boolean`
- `object`
- `array`

Output contracts describe the named output variable an action contributes:

```json
{
  "variable_name": "submission",
  "description": "External submission response.",
  "fields": {
    "submissionId": "string",
    "decision": "string",
    "message": "string"
  }
}
```

## Sequence Steps

A sequence defines its callable inputs:

```json
{
  "$key": "submit_sequence",
  "matrix_id": "claim_matrix",
  "code": "SUBMIT_CLAIM_SEQUENCE",
  "name": "Submit claim sequence",
  "input_contract": {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "properties": {
      "claimId": {
        "type": "string",
        "description": "Claim id supplied by the caller."
      }
    },
    "required": ["claimId"],
    "additionalProperties": false
  }
}
```

Sequence-start contracts use this constrained JSON Schema Draft 2020-12 shape even when there are no inputs. The canonical empty contract keeps the same five root keys with empty `properties` and `required`. A supplied `{}` or worker-action field map is invalid.

The ordered action list lives in `action_sequence_actions`:

```json
{
  "$key": "step_submit",
  "sequence_id": "submit_sequence",
  "action_id": "submit_claim",
  "position": 3,
  "input_mapping": {
    "claimId": "inputs.claimId",
    "normalizedComment": "outputs.normalization.normalizedComment"
  }
}
```

Use `position` for deterministic order. Keep `input_mapping` explicit so an LLM
or human can understand the data flow without reading code.

## Matrix Routing

Statuses belong to one matrix:

```json
{
  "$key": "draft",
  "matrix_id": "claim_matrix",
  "code": "DRAFT",
  "name": "Draft",
  "is_initial": true
}
```

`action_matrix_sequences` attaches a sequence to a status:

```json
{
  "$key": "draft_submit",
  "action_matrix_id": "claim_matrix",
  "status_id": "draft",
  "sequence_id": "submit_sequence",
  "default_outcome_status_id": "submitted",
  "default_outcome_preserves_status": false,
  "default_technical_failure_status_id": "technical_failure",
  "outcome_rules": [
    {
      "exit_condition": "rejected",
      "status_id": "rejected",
      "severity": "error"
    }
  ]
}
```

Routing rules:

- If an action produces an `exit_condition` that matches an outcome rule, the matrix uses that rule's `status_id`.
- If the sequence succeeds without a matching rule, the matrix uses `default_outcome_status_id`.
- To keep the current status after an unmatched successful outcome, set `default_outcome_status_id` to `null` and `default_outcome_preserves_status` to `true`.
- If the sequence fails technically, the matrix uses `default_technical_failure_status_id`.

Optional `position` orders the sequences offered at one status (lowest first, then sequence code); Amon never interprets it. Without it, rows keep their bundle order at that status.

Every mapping must configure exactly one default mode: a non-null
`default_outcome_status_id`, or `default_outcome_preserves_status: true` with a
null target. Older bundles that omit the boolean are treated as
`default_outcome_preserves_status: false`. Preservation never changes explicit
exit-rule or technical-failure routing.

## Bundle Shape

An importable bundle has these top-level arrays:

```json
{
  "meta": { "format_version": 1 },
  "entity_types": [],
  "action_matrixes": [],
  "action_statuses": [],
  "action_actions": [],
  "action_sequences": [],
  "action_sequence_actions": [],
  "action_matrix_sequences": [],
  "action_entity_matrix": []
}
```

`entity_types` and `action_entity_matrix` are optional, but include them when
the matrix should be available to an entity type such as `claim` or `vehicle`.

## Validation and import

Review JSON against the workflow contracts and have your definition administrator
validate it before import. Import and publication change tenant definitions;
review the tenant, references and lane binding before activation.

## Composite Bundles

A composite binds one or more **published** matrices together as roles, tracks
role-scoped milestones, and lets one role's sequence gate on another role's
milestone. This is how multi-track policies (a warranty claim's dealer and
vendor tracks, a vehicle's logistics and retail tracks) coordinate.

```json
{
  "meta": { "format_version": 1 },
  "code": "warranty_claim",
  "name": "Warranty claim",
  "description": "Warranty claim with a dealer track and a vendor recovery track.",
  "roles": [
    { "role_slug": "dealer", "role_name": "Dealer", "matrix_code": "warranty_dealer" },
    { "role_slug": "vendor", "role_name": "Vendor", "matrix_code": "warranty_vendor" }
  ],
  "milestones": [
    {
      "role_slug": "dealer",
      "code": "DEALER_APPROVED",
      "name": "Dealer claim approved",
      "status_codes": ["APPROVED", "CREDITED"]
    }
  ],
  "guards": [
    {
      "consumer_role_slug": "vendor",
      "consumer_sequence_code": "raise_recovery",
      "required_role_slug": "dealer",
      "required_milestone_code": "DEALER_APPROVED",
      "message": "Dealer claim must be approved before recovery can be raised (DEALER_APPROVED)"
    }
  ],
  "entity_types": []
}
```

- `roles[].matrix_code` resolves to that matrix's latest published version at
  import time, so publish the referenced matrices first.
- `milestones[].status_codes` are the statuses, in the role's matrix, that
  satisfy the milestone.
- `guards[]` block a role's sequence (`consumer_role_slug` +
  `consumer_sequence_code`) until another role's milestone
  (`required_role_slug` + `required_milestone_code`) is reached, and carry a
  `message` explaining the block to the caller.
- `entity_types` lists the entity types the composite governs. Pass
  `--entity-types=a,b` on import to replace the bundle's list for a tenant.

Re-importing the same `code` creates a new composite version and moves each
bound entity type's binding onto it. A binding already pointing at a
*different* composite code is a conflict. An unpublished draft for the code
blocks the import until it is published or deleted.

Outcome rules on `action_matrix_sequences` may omit `status_id`: that records
the matched exit with its severity and lets the sequence continue. Only a
rule with a `status_id` is a routed exit, and it stops the sequence at that
action.

## Builder Rules

- Prefer one clear action per external call, check, or transform.
- Keep sequence input contracts small and explicit.
- Do not hide routing policy inside code actions. Emit `exit_condition` facts and let the matrix route.
- Use stable uppercase codes for statuses, actions, and sequences.
- Use `$key` references inside JSON bundles instead of database ids.
- Keep example bundles valid against `@amon-sul/amon-contracts`.
