statelyai/agent/generate-machine

Claude Code and agent skill by statelyai/agent

Trust tier 3
  • community
Source check pending

Checking that the SKILL.md file is still in the repository.

Author a @statelyai/agent workflow as JSON and check it before it runs. Use when asked to generate or author an agent machine, create a workflow config, write an AgentWorkflowConfig, or produce an LLM-authored state machine for an agent — and when a generated config fails verifyAgentConfig, Ajv validation, or agent lint and needs repair.

Install

npx vskill@latest install statelyai/agent/generate-machine

SKILL.md

Generate an agent machine

An agent machine is data. You author a JSON AgentWorkflowConfig, then run it through verifyAgentConfig, whose gates all work with no API key: validate → vocabulary → lower → lint → explore. Do not hand back a config that has not passed every gate.

split environment/policy → author (description = goal) → verifyAgentConfig → repair (≤ 3) → persist traces → hand back

To improve an existing policy from its traces, use the improve-machine skill instead.

0. Split environment and policy

Decide what is fixed before you write any JSON.

  • Environment (TypeScript, fixed): the rules of the world and the business invariants, as states and guards. People author it and verify it with canReach / explorePaths from @statelyai/agent/testing. You never generate it, and no generated config may change it.
  • Policy (JSON, generated): how the agent works toward the goal: which step next, which tool, when to ask a person, cheap judgment vs model call. This is the AgentWorkflowConfig you author.
  • The policy plugs into the environment at its decision points and outputs one event; the environment's guards decide whether to accept it.
  • No split needed for a single judgment inside a fixed workflow, or for a workflow where every step is dictated by rules. Author one machine.

If the request asks you to generate a business rule (an approval threshold, a payout limit), stop: that belongs in the environment, in TypeScript, reviewed by a person. Say so.

Before editing an existing machine, read it as text:

ts
import { describeMachine } from "@statelyai/agent";
const text = describeMachine(machine); // "Machine <id>", then "Goal: …", states, transitions, events

Pass { snapshot } to mark the active states with *. Concept page: Environment and policy.

1. Read the schema

The config shape is fixed by a shipped JSON Schema. Read it first; it is the contract.

  • Consuming the package: node_modules/@statelyai/agent/schemas/agent-workflow.json (importable as @statelyai/agent/agent-workflow.json)
  • In the statelyai/agent repo: schemas/agent-workflow.json

2. Author the config

Start with the root description: it is the machine's goal, the overall objective plus measurable success criteria. describeMachine renders it first as Goal: …, and an improver later climbs toward it. Write criteria a scorer can compute from outcomes (terminal states, effects in the world, cost), not "be helpful":

json
"description": "Resolve each support ticket in one session. Success: escalations only for tickets that need a human; every reply approved before sending; at most 2 model calls per ticket."

Rules the runtime enforces. Each one maps to a build error or a lint diagnostic:

  • Every value is a JSON literal or a whole-string "{{ }}" expression reading a dot path on input, context, or event. No JavaScript.
  • Guard expressions are truthy-only: no comparisons, operators, or negation.
  • A model call is either a named requests entry invoked with src: "<requestName>", or an inline invoke with src: "agent.decide" plus an allowedEvents list. A requests entry's templates read only its own input ("{{ input.ticket }}"); the invoke's input maps context into it.
  • A state invoking agent.decide MUST handle every allowed event in its on.
  • An agent.decide invoke has no onDone (a decision produces no output). Use onError for the retries-exhausted path.
  • A request invoke resolves with { result, messages }, so onDone.assign reads its result from "{{ event.output.result.<field> }}" ("{{ event.output.result }}" for a plain-text request). A non-request actors invoke reads "{{ event.output.<field> }}" directly.
  • Every path must reach a "type": "final" state, and each final state needs an output when schemas.output is declared.
  • Do not invent guard or action names. Only "{{ }}" guards, assign, and emit exist unless the host tells you which named guards/actions it implements.
  • Model refs are strings ("openai/gpt-6-luna"); the host resolves them.
  • To make a decision a closed choice, add payloads to the agent.decide input: one payload list per event type, without type ("payloads": { "REFUND": [{ "amount": 20 }, { "amount": 50 }], "ESCALATE": [] }). The model then picks one listed event instead of writing a payload. A type with no key is offered as { type }; [] withholds it.
  • An idle human step can carry meta.interaction for the host to render with getInteraction: a label, events (one button per event type), choices (one button per full event, { "label": "Refund $20", "event": { "type": "REFUND", "amount": 20 } }, or a context path string holding such a list), and textEvent for free text. A bare "textEvent": "REJECT" puts the text in a text field; { "type": "REJECT", "field": "reason" } names the field. meta is not template-evaluated.

Reference config — decision, text request, idle human step, one final state:

json
{
"id": "support-ticket",
"description": "Resolve each support ticket in one session. Success: escalations only for tickets that need a human; every reply approved before sending; at most 2 model calls per ticket.",
"schemas": {
"input": {
"type": "object",
"properties": { "ticket": { "type": "string" } },
"required": ["ticket"]
},
"context": {
"type": "object",
"properties": {
"ticket": { "type": "string" },
"reply": { "type": "string" },
"resolution": { "type": "string" }
},
"required": ["ticket"]
},
"events": {
"ESCALATE": {
"type": "object",
"properties": { "reason": { "type": "string" } },
"required": ["reason"]
},
"REPLY": { "type": "object", "properties": {} },
"APPROVE": { "type": "object", "properties": {} },
"REJECT": { "type": "object", "properties": {} }
},
"output": {
"type": "object",
"properties": { "resolution": { "type": "string" }, "reply": { "type": "string" } },
"required": ["resolution"]
}
},
"context": { "ticket": "{{ input.ticket }}" },
"requests": {
"draftReply": {
"model": "openai/gpt-6-luna",
"system": "Draft a short, courteous support reply.",
"prompt": "{{ input.ticket }}",
"input": {
"type": "object",
"properties": { "ticket": { "type": "string" } },
"required": ["ticket"]
},
"output": {
"type": "object",
"properties": { "reply": { "type": "string" } },
"required": ["reply"]
}
}
},
"initial": "triaging",
"states": {
"triaging": {
"invoke": {
"id": "triageDecision",
"src": "agent.decide",
"input": {
"model": "openai/gpt-6-luna",
"system": "Decide whether this ticket needs escalation or a drafted reply.",
"prompt": "{{ context.ticket }}",
"allowedEvents": ["ESCALATE", "REPLY"],
"maxRetries": 2
},
"onError": { "target": "resolved", "assign": { "resolution": "escalated" } }
},
"on": {
"ESCALATE": { "target": "resolved", "assign": { "resolution": "escalated" } },
"REPLY": { "target": "drafting" }
}
},
"drafting": {
"invoke": {
"id": "draft",
"src": "draftReply",
"input": { "ticket": "{{ context.ticket }}" },
"onDone": {
"target": "awaitingApproval",
"assign": { "reply": "{{ event.output.result.reply }}" }
}
}
},
"awaitingApproval": {
"description": "Idle: nothing to do until a human approves or rejects the draft.",
"on": {
"APPROVE": { "target": "resolved", "assign": { "resolution": "replied" } },
"REJECT": { "target": "resolved", "assign": { "resolution": "escalated" } }
}
},
"resolved": {
"type": "final",
"output": { "resolution": "{{ context.resolution }}", "reply": "{{ context.reply }}" }
}
}
}

3. Verify with verifyAgentConfig

verifyAgentConfig(config, options) from @statelyai/agent/validate runs every pre-run gate and stops at the first failure. It never throws on a bad config; each gate is a record { name, ok, detail? }. It needs ajv installed (an optional peer of the package).

GateChecks withFails on
validatevalidateAgentConfig (shipped schema)malformed config shape
vocabularyallow listsan invoke src, named guard, named action, agent.decide event, or model ref not listed
lowersetupAgent.fromConfigunresolved named guard/action, onDone on an agent.decide invoke
lintlintAgentMachineerror findings (warnings too with lint: { warnings: true })
exploreexplorePaths, once per inputa path ending outside explore.terminals (default ['done']), a loop past maxDepth, a maxPaths/maxNodes cap, untried payloads (unexpandedEvents)

compileSchema turns the JSON Schemas _inside_ the config into Standard Schema validators for fromConfig. The library bundles no JSON Schema engine:

ts
import Ajv from "ajv";
import type { SchemaCompiler, StandardSchemaV1 } from "@statelyai/agent";
const ajv = new Ajv({ strict: false });
export const compileSchema: SchemaCompiler = (jsonSchema, name): StandardSchemaV1 => {
const validate = ajv.compile(jsonSchema);
return {
"~standard": {
version: 1,
vendor: "ajv",
validate: (value) =>
validate(value)
? { value }
: {
issues: (validate.errors ?? []).map((e) => ({
message: `${name}${e.instancePath} ${e.message}`,
})),
},
// Expose the source JSON Schema so lint and describeMachine can read the shape.
jsonSchema: { input: () => jsonSchema },
},
};
};

Run the gates. List the host's real names in allow; an omitted key is unrestricted:

ts
import { verifyAgentConfig } from "@statelyai/agent/validate";
const result = await verifyAgentConfig(candidate, {
compileSchema,
allow: {
actors: [],
requests: ["draftReply"],
guards: [],
actions: [],
events: ["ESCALATE", "REPLY", "APPROVE", "REJECT"],
models: ["openai/gpt-6-luna"],
},
explore: {
inputs: [{ ticket: "Export downloads a 0-byte CSV on Safari." }],
outputs: { draftReply: [{ reply: "" }] },
// ESCALATE's `reason` is a free-form string: give exploration one to try.
payloads: () => ({ ESCALATE: [{ reason: "needs a human" }] }),
},
});
// result.ok, result.gates, result.config (after validate), result.machine (after lower)

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 95 of 100Pattern checks on the skill's files.
  • Model reviewPassed

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

7-day trend score
5

Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/statelyai/agent/generate-machine/badge)](https://verified-skill.com/skills/statelyai/agent/generate-machine)

Something wrong with this skill or its page? Tell us.