State Transition Validator
workflow.state_transition_validate
Overview
You send a state machine, the state something is in, the event that just happened, and optionally the context your guards look at. We answer whether that event is allowed, which state it leads to, and which transition matched — or, when it is not allowed, exactly why: the state is unknown, the state is terminal, no transition covers this event, a guard was not satisfied, or two transitions matched and the outcome would be ambiguous. Guards are data, not code: six comparisons over values picked from your context by JSON Pointer, so a condition that looks like a formula is compared as the literal text it is. Nothing is stored, nothing is moved, and no transition action is run.
Status rules end up spread across controllers, migrations and background jobs, and they drift: one path allows a refund on a cancelled order, another does not. Declaring the machine as data and asking one skill keeps every service on the same answer, with the denial reason available to show a user.
Reliability
Fully deterministic: the same machine, state, event and context always give the same answer, and reordering the states or transitions changes nothing except which id is named first in the reasons. Only EXISTS and NOT_EXISTS can be satisfied by a member that is absent — the comparing operators need a value, so a pointer that resolves nowhere denies the guard and says so with GUARD_POINTER_MISSING; a member that is present and null is a value and compares as null. Failures, all about the machine you sent rather than the event: STATE_MACHINE_INVALID means the machine cannot be evaluated — no states, a state or transition id declared twice, an id that is empty, too long, padded or carrying a control character, or a transition pointing at an undeclared state. STATE_LIMIT_EXCEEDED and TRANSITION_LIMIT_EXCEEDED mean more than 500 states or 5000 transitions, so split the machine. STATE_CONDITION_INVALID means a condition is not usable: an operator that does not exist, a pointer that is not valid RFC 6901, EQUALS or NOT_EQUALS without value, IN or NOT_IN without a 1-to-256-entry values array, a value supplied to an operator that compares nothing, or more than 32 conditions on one transition. STATE_CONTEXT_TOO_LARGE means the context is past 256 KB or nests deeper than 64 levels, so send only the members your guards point at. None of these are worth retrying unchanged.
Context footprint
The answer is one small object: a verdict, two ids and the reasons. The machine and context you send dominate the request, and no context value is returned. A successful call costs 1 execution credit.
When to use it
Before you change a status: send the machine, the current state and the event, and act on allowed together with nextState.
When the machine must be stored and advanced for you, when guards need real expressions, or when the caller may simply pick among several transitions.
Scope
Non-goals
- Does not store or advance the machine — the answer is yours to act on
- Does not run transition actions, hooks or side effects
- Does not evaluate expressions, scripts or templates in conditions
- Does not invent transitions for events the machine does not declare
- Does not echo the context values your guards read
Properties
Capabilities
Cost & billing
Flat rate per invocation
File handling
How we handle your data
What this means
This skill may receive sensitive content, but it does not target sensitive data and returns only a structured non-sensitive result.
Squillify guarantees
- Runs inside Squillify
- This skill runs inside Squillify.
- Payloads are not sent to an external service by this skill.
- Squillify does not permanently store submitted payloads.
- Telemetry records metadata only, not submitted content.
- Submitted payloads are not used for training.
- Submitted payloads are not reviewed by humans.
- This skill does not target sensitive data.
- The response does not contain submitted content.
Your responsibility
Squillify does not determine whether submitted content is sensitive. Your application is responsible for deciding what data it is allowed to submit. Your application is responsible for securing the response.
Input schema
| Field | Type | Required | Description |
|---|---|---|---|
| currentState | string | Yes | State the subject is in right now, compared exactly against machine.states. |
| event | string | Yes | Event that happened, compared exactly against each transition's event. |
| machine | object | Yes | Closed machine: 1 to 500 states with unique ids, and 0 to 5000 transitions whose from and to name declared states. |
| context | object | No | Purpose-bound JSON object guards read through JSON Pointers, at most 256 properties; omit it when no transition has conditions — unconstrained RFC 8259 JSON items. |
Output schema
| Field | Type | Required | Description |
|---|---|---|---|
| allowed | boolean | Yes | True when exactly one transition leaves the current state on this event with all conditions satisfied. |
| currentState | string | Yes | State the verdict was reached from, repeated exactly as sent so a stored answer says what it judged. |
| event | string | Yes | Event the verdict concerns, repeated exactly as sent because an allowed or denied answer means nothing once the event is lost. |
| nextState | string | No | Target of the matched transition; omit it when allowed is false. |
| matchedTransitionId | string | No | Id of the transition that matched; omit it when allowed is false. |
| terminal | boolean | Yes | Whether nextState is terminal when allowed, otherwise whether the current state is terminal. |
| evaluatedTransitions | integer | Yes | How many transitions left the current state on this event. |
| reasons | array | Yes | Denial reasons, each a closed {code, message} object with optional transitionId, op and pointer; empty when allowed. |
API contract
POST /v1/executecurl -sS -X POST "$BASE/v1/execute" -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d '{"skillKey":"workflow.state_transition_validate","input":{"currentState":"pending","event":"pay","machine":{"states":[{"id":"pending"},{"id":"paid"}],"transitions":[{"id":"t-pay","from":"pending","event":"pay","to":"paid"}]}}}'import { Squillify } from "@squillify/sdk";
const sq = new Squillify({ apiKey: process.env.SQUILLIFY_API_KEY });
const { output } = await sq.execute("workflow.state_transition_validate", {"machine":{"states":[{"id":"pending"},{"id":"paid"},{"id":"cancelled","terminal":true}],"transitions":[{"id":"t-pay","from":"pending","event":"pay","to":"paid","conditions":[{"value":"captured","op":"EQUALS","pointer":"/payment/status"}]},{"event":"cancel","to":"cancelled","from":"pending","id":"t-cancel"}]},"currentState":"pending","context":{"payment":{"status":"captured"}},"event":"pay"});Errors
| Code | HTTP | Retryable | Description |
|---|---|---|---|
| VALIDATION_ERROR | 400 | No | The request payload failed schema validation. |
| UNAUTHORIZED | 401 | No | Missing or invalid API key. |
| SKILL_NOT_ACCESSIBLE | 403 | No | The API key is not granted access to this skill. |
| SKILL_NOT_AVAILABLE_FOR_PLAN | 403 | No | This skill is not included in your current plan. |
| PLAN_ELIGIBILITY_UNAVAILABLE | 503 | Yes | Plan eligibility could not be decided. Retry after backoff. |
| SKILL_NOT_FOUND | 404 | No | The requested skill or version does not exist. |
| TENANT_RATE_LIMITED | 429 | Yes | Request rate limit exceeded. Retry after backoff. |
| QUOTA_EXCEEDED | 429 | Yes | Plan quota exhausted. Retryable once the quota period resets. |
| EXECUTION_TIMEOUT | 504 | Yes | Execution exceeded the allowed time budget. |
| STATE_MACHINE_INVALID | 400 | No | The request payload failed this skill's input validation. |
| STATE_LIMIT_EXCEEDED | 429 | No | A size, quota, rate, or time limit was exceeded. |
| TRANSITION_LIMIT_EXCEEDED | 429 | No | A size, quota, rate, or time limit was exceeded. |
| STATE_CONDITION_INVALID | 400 | No | The request payload failed this skill's input validation. |
| STATE_CONTEXT_TOO_LARGE | 429 | No | A size, quota, rate, or time limit was exceeded. |
Other limits
| Limit | Value |
|---|---|
| Recommended inline bytes | 32 KiB |
Limitations
- Does not store or advance the machine — the answer is yours to act on
- Does not run transition actions, hooks or side effects
- Does not evaluate expressions, scripts or templates in conditions
- Does not invent transitions for events the machine does not declare
- Does not echo the context values your guards read
- Persisting the new state or emitting events, which stays your call
- Machines whose guards need arbitrary expressions or a rules engine
- Choosing between several allowed transitions — that is ambiguity here, not a priority question
How to call
POST /v1/execute (requires authentication)
{
"skillKey": "workflow.state_transition_validate",
"input": {
"machine": {
"states": [
{
"id": "pending"
},
{
"id": "paid"
},
{
"id": "cancelled",
"terminal": true
}
],
"transitions": [
{
"id": "t-pay",
"from": "pending",
"event": "pay",
"to": "paid",
"conditions": [
{
"value": "captured",
"op": "EQUALS",
"pointer": "/payment/status"
}
]
},
{
"event": "cancel",
"to": "cancelled",
"from": "pending",
"id": "t-cancel"
}
]
},
"currentState": "pending",
"context": {
"payment": {
"status": "captured"
}
},
"event": "pay"
}
}Examples
An order is paid, and the guard on the amount is satisfied
{
"machine": {
"states": [
{
"id": "pending"
},
{
"id": "paid"
},
{
"id": "cancelled",
"terminal": true
}
],
"transitions": [
{
"id": "t-pay",
"from": "pending",
"event": "pay",
"to": "paid",
"conditions": [
{
"value": "captured",
"op": "EQUALS",
"pointer": "/payment/status"
}
]
},
{
"event": "cancel",
"to": "cancelled",
"from": "pending",
"id": "t-cancel"
}
]
},
"currentState": "pending",
"context": {
"payment": {
"status": "captured"
}
},
"event": "pay"
}{
"allowed": true,
"currentState": "pending",
"event": "pay",
"terminal": false,
"nextState": "paid",
"reasons": [],
"evaluatedTransitions": 1,
"matchedTransitionId": "t-pay"
}The same event with the guard unmet: denied, with the pointer that failed
{
"machine": {
"states": [
{
"id": "pending"
},
{
"id": "paid"
}
],
"transitions": [
{
"id": "t-pay",
"from": "pending",
"event": "pay",
"to": "paid",
"conditions": [
{
"value": "captured",
"op": "EQUALS",
"pointer": "/payment/status"
}
]
}
]
},
"currentState": "pending",
"context": {
"payment": {
"status": "pending"
}
},
"event": "pay"
}{
"event": "pay",
"reasons": [
{
"code": "GUARD_FAILED",
"transitionId": "t-pay",
"op": "EQUALS",
"pointer": "/payment/status",
"message": "The context value at this pointer does not satisfy the condition."
}
],
"currentState": "pending",
"terminal": false,
"evaluatedTransitions": 1,
"allowed": false
}Recommended use cases
- Guard an API endpoint that changes a status, before writing anything
- Keep one status policy that several services evaluate identically
- Show a user why an action is unavailable, with the reason rather than a bare 403
Avoid for
- Persisting the new state or emitting events, which stays your call
- Machines whose guards need arbitrary expressions or a rules engine
- Choosing between several allowed transitions — that is ambiguity here, not a priority question
Related skills
Composable with
Safety & privacy
- Runs without calling any external service.
- Your payloads are never stored permanently.
- Telemetry captures metadata only — never your payload contents.
- We never train models on your payloads.
- No human reviews your payloads.