---
title: "Graph schema — Workflows for Laravel | Roundly"
description: "The roundly.workflows.graph/v1 document — rules, value references, durations, parties — and the full graph, template and shared-definition JSON Schemas."
url: https://roundly-consulting.com/open-source/docs/workflows-for-laravel/graph-schema
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Workflows for Laravel](https://roundly-consulting.com/open-source/docs/workflows-for-laravel.md)

# Graph schema

A workflow is one JSON document. The package publishes it as JSON Schema (a draft 2020-12 subset) through Workflows::schema()->graph(); the template envelope through Workflows::schema()->template(); and the shared $defs (value, field, party, parties, duration, condition) through Workflows::schema()->definitions() — the graph schema adds retry and escalation. Node configs are typed per step type: see Workflows::catalogue()->export().

```php
Workflows::schema()->graph();         // the roundly.workflows.graph/v1 JSON Schema
Workflows::schema()->template();      // the roundly.workflows.template/v1 envelope
Workflows::schema()->definitions();   // the shared $defs: value, field, party, parties, duration, condition
Workflows::catalogue()->export();     // every node type's config schema, outcomes and availability
```

## The document

```jsonc
{
  "schema": "roundly.workflows.graph/v1",          // required; anything else → invalid_schema_version
  "settings": {                                     // optional; each key has a default
    "actor": "system" | "on_behalf_of",             // default "system"
    "timezone": "Europe/Bratislava",                // IANA; default workflows.timezone → app.timezone
    "calendar": "weekdays",                         // default business calendar
    "limits": { "max_steps": 200 },                 // may only LOWER the configured limit
    "retry": { "max_attempts": 3, "backoff": [30, 120, 600] }
  },
  "nodes": [ {
      "id": "approve_cfo",                          // ^[a-z][a-z0-9_]{0,63}$, unique
      "type": "approval",                           // a catalogue key
      "config": { },                                // validated against the type's config schema
      "retry": { },                                 // optional per-node override
      "escalations": [ ],                           // only on types that support them
      "ui": { "x": 120, "y": 80, "label": "CFO sign-off" }   // free-form; ignored by the engine
  } ],
  "edges": [ {
      "from": "approve_cfo", "to": "notify_rejected",
      "outcome": "rejected"                         // required unless the source's only outcome is `next`
  } ]
}
```

## Rules

- Exactly one trigger node, at least one end node.
- Size caps: nodes ≤ graph.max\_nodes (500), edges ≤ graph.max\_edges (2000), encoded graph ≤ graph.max\_bytes (512 KiB) → graph\_too\_large.
- An edge is identified by (from, outcome). Two edges with the same pair → duplicate\_outcome. One token moves through the graph: there are no parallel branches.
- An edge without outcome stands for next and nothing else, so it is allowed only from a node whose one outcome is next (set\_field, delay, an action step, …). A node with any other outcome — even a single one, like start\_workflow without wait (started), wait (received), webhook (queued) — names it on every edge, else outcome\_label\_required at publish.
- A cycle must pass through a breaker: a loop node’s continue edge, or a delay that waits a positive for. A loop’s exhausted edge and a zero-length, until or until\_field delay don’t count → cycle\_without\_breaker.
- ui is kept verbatim and never interpreted (a diff reports ui-only changes as moved).
- Saving a draft checks structure only, so a builder can store a half-finished graph. Publish runs every check.

## Value references

Wherever a schema allows #/$defs/value, a value is a literal, {"ref": "<path>"}, or {"relative": <duration>} — the run’s “now” plus (or, with a negative amount, minus) a duration, in the version timezone:

```jsonc
"value": "50000"                                          // a literal
"value": { "ref": "subject.amount" }                       // a registered field of the subject
"value": { "ref": "steps.approve.output.request_id" }      // the output of an ancestor step
"value": { "relative": "P7D" }                             // now + 7 days, in the version timezone
"value": { "relative": { "amount": -3, "unit": "days" } }  // now − 3 days
```

| Path | Reads |
| --- | --- |
| `subject.<field>` | A registered field of the subject (never an unregistered attribute). |
| `owner.<field>` | A registered field of the owner type. |
| `input.<name>` | A declared manual-trigger input. |
| `trigger.<key>` | The trigger payload (event payload, webhook body). |
| `steps.<node_id>.output.<key>` | The output of an ancestor step. |
| `now` | The current instant. |
| `payload.<path>` | The webhook body — only in a webhook trigger’s subject.value (the subject lookup runs before the instance exists). |

## Durations

An ISO 8601 string (calendar time in the version timezone) or an object with amount and unit — minutes, hours, days, weeks, months, business\_minutes, business\_hours, business\_days. calendar overrides settings.calendar:

```jsonc
"P2D"                                                       // ISO 8601: calendar time in the version timezone
"PT4H"
{ "amount": 2, "unit": "business_days", "calendar": "opening_hours:office" }
```

## Party references

```json
{"kind": "user", "id": 12}
{"kind": "field", "field": "subject.requester"}
{"kind": "role", "role": "legal"}
{"kind": "triggered_by"}
```

## Versioning of the schema

The schema string is the version. A future incompatible change gets a new string; roundly.workflows.graph/v1 documents keep validating and running.

## The graph JSON Schema

Generated by Workflows::schema()->graph() — limits reflect the default config:

```json
{
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "$id": "https://roundly-consulting.com/schemas/roundly.workflows.graph/v1.json",
    "title": "Workflow graph",
    "type": "object",
    "required": [
        "schema",
        "nodes",
        "edges"
    ],
    "properties": {
        "schema": {
            "const": "roundly.workflows.graph/v1"
        },
        "settings": {
            "type": "object",
            "properties": {
                "actor": {
                    "enum": [
                        "system",
                        "on_behalf_of"
                    ],
                    "default": "system"
                },
                "timezone": {
                    "type": "string",
                    "description": "IANA timezone; default workflows.timezone, else app.timezone."
                },
                "calendar": {
                    "type": "string",
                    "description": "Default business calendar (weekdays, opening_hours:<name> or a host calendar)."
                },
                "limits": {
                    "type": "object",
                    "properties": {
                        "max_steps": {
                            "type": "integer",
                            "minimum": 2,
                            "maximum": 200
                        }
                    },
                    "additionalProperties": false
                },
                "retry": {
                    "$ref": "#/$defs/retry"
                }
            },
            "additionalProperties": false
        },
        "nodes": {
            "type": "array",
            "maxItems": 500,
            "items": {
                "type": "object",
                "required": [
                    "id",
                    "type"
                ],
                "properties": {
                    "id": {
                        "type": "string",
                        "pattern": "^[a-z][a-z0-9_]{0,63}$"
                    },
                    "type": {
                        "type": "string",
                        "description": "A step catalogue key."
                    },
                    "config": {
                        "type": "object",
                        "description": "Validated against the type's config schema."
                    },
                    "retry": {
                        "$ref": "#/$defs/retry"
                    },
                    "escalations": {
                        "type": "array",
                        "maxItems": 5,
                        "items": {
                            "$ref": "#/$defs/escalation"
                        }
                    },
                    "ui": {
                        "type": "object",
                        "description": "Free-form (position, label); ignored by the engine."
                    }
                },
                "additionalProperties": false
            }
        },
        "edges": {
            "type": "array",
            "maxItems": 2000,
            "items": {
                "type": "object",
                "required": [
                    "from",
                    "to"
                ],
                "properties": {
                    "from": {
                        "type": "string"
                    },
                    "to": {
                        "type": "string"
                    },
                    "outcome": {
                        "type": "string",
                        "description": "Required unless the source type's only outcome is next."
                    }
                },
                "additionalProperties": false
            }
        }
    },
    "additionalProperties": false,
    "$defs": {
        "value": {
            "description": "A literal, {\"ref\": \"<path>\"} or {\"relative\": <duration>}."
        },
        "field": {
            "type": "string",
            "format": "field-path",
            "maxLength": 200
        },
        "party": {
            "type": "object",
            "required": [
                "kind"
            ],
            "properties": {
                "kind": {
                    "enum": [
                        "user",
                        "field",
                        "role",
                        "triggered_by"
                    ]
                },
                "id": {
                    "type": [
                        "integer",
                        "string"
                    ],
                    "x-workflows-owner-specific": true
                },
                "field": {
                    "$ref": "#/$defs/field"
                },
                "role": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 100
                }
            },
            "additionalProperties": false
        },
        "parties": {
            "type": "array",
            "items": {
                "$ref": "#/$defs/party"
            },
            "minItems": 1,
            "maxItems": 50
        },
        "duration": {
            "anyOf": [
                {
                    "type": "string",
                    "format": "duration"
                },
                {
                    "type": "object",
                    "required": [
                        "amount",
                        "unit"
                    ],
                    "properties": {
                        "amount": {
                            "type": "integer",
                            "minimum": -100000,
                            "maximum": 100000
                        },
                        "unit": {
                            "enum": [
                                "minutes",
                                "hours",
                                "days",
                                "weeks",
                                "months",
                                "business_minutes",
                                "business_hours",
                                "business_days"
                            ]
                        },
                        "calendar": {
                            "type": "string",
                            "minLength": 1,
                            "maxLength": 100
                        }
                    },
                    "additionalProperties": false
                }
            ]
        },
        "condition": {
            "type": "object",
            "description": "A declarative condition: {\"all\"|\"any\": [...]}, {\"not\": {...}} or {\"field\", \"op\", \"value\"}."
        },
        "retry": {
            "type": "object",
            "properties": {
                "max_attempts": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 10
                },
                "backoff": {
                    "type": "array",
                    "items": {
                        "type": "integer",
                        "minimum": 0
                    }
                }
            },
            "additionalProperties": false
        },
        "escalation": {
            "type": "object",
            "required": [
                "after",
                "actions"
            ],
            "properties": {
                "after": {
                    "$ref": "#/$defs/duration"
                },
                "actions": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 5,
                    "items": {
                        "type": "object",
                        "required": [
                            "type"
                        ],
                        "properties": {
                            "type": {
                                "enum": [
                                    "notify",
                                    "reassign",
                                    "start_workflow"
                                ]
                            }
                        }
                    }
                }
            },
            "additionalProperties": false
        }
    }
}
```

## The template JSON Schema

Generated by Workflows::schema()->template() — the envelope of an exported template and of a definition file:

```json
{
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "$id": "https://roundly-consulting.com/schemas/roundly.workflows.template/v1.json",
    "title": "Workflow template",
    "type": "object",
    "required": [
        "schema",
        "definition",
        "graph"
    ],
    "properties": {
        "schema": {
            "const": "roundly.workflows.template/v1"
        },
        "definition": {
            "type": "object",
            "required": [
                "key",
                "name"
            ],
            "properties": {
                "key": {
                    "type": "string",
                    "pattern": "^[a-z0-9][a-z0-9._-]{0,99}$"
                },
                "name": {
                    "type": "string"
                },
                "subject_type": {
                    "type": [
                        "string",
                        "null"
                    ]
                },
                "description": {
                    "type": [
                        "string",
                        "null"
                    ]
                }
            }
        },
        "graph": {
            "$ref": "roundly.workflows.graph/v1"
        },
        "review": {
            "type": "array"
        },
        "exported_at": {
            "type": "string",
            "format": "date-time"
        },
        "source": {
            "type": "object",
            "properties": {
                "checksum": {
                    "type": "string"
                }
            }
        }
    }
}
```

## Shared definitions

Generated by Workflows::schema()->definitions() — the $defs every step config schema may reference:

```json
{
    "value": {
        "description": "A literal, {\"ref\": \"<path>\"} or {\"relative\": <duration>}."
    },
    "field": {
        "type": "string",
        "format": "field-path",
        "maxLength": 200
    },
    "party": {
        "type": "object",
        "required": [
            "kind"
        ],
        "properties": {
            "kind": {
                "enum": [
                    "user",
                    "field",
                    "role",
                    "triggered_by"
                ]
            },
            "id": {
                "type": [
                    "integer",
                    "string"
                ],
                "x-workflows-owner-specific": true
            },
            "field": {
                "$ref": "#/$defs/field"
            },
            "role": {
                "type": "string",
                "minLength": 1,
                "maxLength": 100
            }
        },
        "additionalProperties": false
    },
    "parties": {
        "type": "array",
        "items": {
            "$ref": "#/$defs/party"
        },
        "minItems": 1,
        "maxItems": 50
    },
    "duration": {
        "anyOf": [
            {
                "type": "string",
                "format": "duration"
            },
            {
                "type": "object",
                "required": [
                    "amount",
                    "unit"
                ],
                "properties": {
                    "amount": {
                        "type": "integer",
                        "minimum": -100000,
                        "maximum": 100000
                    },
                    "unit": {
                        "enum": [
                            "minutes",
                            "hours",
                            "days",
                            "weeks",
                            "months",
                            "business_minutes",
                            "business_hours",
                            "business_days"
                        ]
                    },
                    "calendar": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 100
                    }
                },
                "additionalProperties": false
            }
        ]
    },
    "condition": {
        "type": "object",
        "description": "A declarative condition: {\"all\"|\"any\": [...]}, {\"not\": {...}} or {\"field\", \"op\", \"value\"}."
    }
}
```

## Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

[More ways to support, including crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/workflows-for-laravel.md)
