Skip to main content

Squillify development is paused indefinitely. Read why, and discover what this experiment achieved. Read the story

MicroSkills Reference

State Transition Validator

workflow.state_transition_validate

ProductionBounded custom (Class B)Deterministicstrictfastv1.0.0
No network

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

Note
Evaluate one event against a declarative state machine and return whether it is allowed, where it leads, and the structured reasons when it is not.

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

IdempotentRetry safeStreaming supportedFile input supportedRequires external service

Capabilities

validation

Cost & billing

Flat rate per invocation

MetercallsBillable onsuccessCost per call1 credit

File handling

Accepts file_refProduces file_refMax input1 MB

How we handle your data

Sensitive input, safe output
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.

Read the Microskill Data Handling Policy

Input schema

FieldTypeRequiredDescription
currentStatestringYesState the subject is in right now, compared exactly against machine.states.
eventstringYesEvent that happened, compared exactly against each transition's event.
machineobjectYesClosed machine: 1 to 500 states with unique ids, and 0 to 5000 transitions whose from and to name declared states.
contextobjectNoPurpose-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

FieldTypeRequiredDescription
allowedbooleanYesTrue when exactly one transition leaves the current state on this event with all conditions satisfied.
currentStatestringYesState the verdict was reached from, repeated exactly as sent so a stored answer says what it judged.
eventstringYesEvent the verdict concerns, repeated exactly as sent because an allowed or denied answer means nothing once the event is lost.
nextStatestringNoTarget of the matched transition; omit it when allowed is false.
matchedTransitionIdstringNoId of the transition that matched; omit it when allowed is false.
terminalbooleanYesWhether nextState is terminal when allowed, otherwise whether the current state is terminal.
evaluatedTransitionsintegerYesHow many transitions left the current state on this event.
reasonsarrayYesDenial reasons, each a closed {code, message} object with optional transitionId, op and pointer; empty when allowed.

API contract

POST /v1/execute
curl
curl -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"}]}}}'
Integrations
TypeScript SDK
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

CodeHTTPRetryableDescription
VALIDATION_ERROR400NoThe request payload failed schema validation.
UNAUTHORIZED401NoMissing or invalid API key.
SKILL_NOT_ACCESSIBLE403NoThe API key is not granted access to this skill.
SKILL_NOT_AVAILABLE_FOR_PLAN403NoThis skill is not included in your current plan.
PLAN_ELIGIBILITY_UNAVAILABLE503YesPlan eligibility could not be decided. Retry after backoff.
SKILL_NOT_FOUND404NoThe requested skill or version does not exist.
TENANT_RATE_LIMITED429YesRequest rate limit exceeded. Retry after backoff.
QUOTA_EXCEEDED429YesPlan quota exhausted. Retryable once the quota period resets.
EXECUTION_TIMEOUT504YesExecution exceeded the allowed time budget.
STATE_MACHINE_INVALID400NoThe request payload failed this skill's input validation.
STATE_LIMIT_EXCEEDED429NoA size, quota, rate, or time limit was exceeded.
TRANSITION_LIMIT_EXCEEDED429NoA size, quota, rate, or time limit was exceeded.
STATE_CONDITION_INVALID400NoThe request payload failed this skill's input validation.
STATE_CONTEXT_TOO_LARGE429NoA size, quota, rate, or time limit was exceeded.

Other limits

LimitValue
Recommended inline bytes32 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)

Request bodyjson
{
  "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

Inputjson
{
  "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"
}
Expected outputjson
{
  "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

Inputjson
{
  "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"
}
Expected outputjson
{
  "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
}
  • 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

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.

Tags

workflowstate-machinevalidation