glyphrail
v0.2.0
Published
CLI-first workflow orchestration for deterministic, AI-native runs
Maintainers
Readme
Glyphrail
Glyphrail is a Bun-native, CLI-first workflow engine for deterministic orchestration and bounded agent execution.
This repository currently implements the v0.1.0 MVP through Slice 6:
- workflow authoring, validation, linting, and explanation
- typed local tool registries
- deterministic workflow execution with persisted state and trace artifacts
- structured agent steps through a built-in
mockadapter - retries,
continue, andgotoerror policies - run inspection, checkpointing, and resume
- project-wide checks and capability discovery
The important architectural idea is simple:
- the engine owns control flow, persistence, budgets, and safety
- tools own typed side effects
- agent steps own bounded judgment inside a deterministic shell
- the CLI is a machine-operable contract, not just a human UX
What This Codebase Actually Is
Glyphrail is not a chat agent framework and it is not an open-ended autonomous loop runner.
It is a local workflow runtime with:
- a YAML workflow DSL
- a JSON runtime state model
- a TypeScript tool registry
- a deterministic execution engine
- a structured agent interface
- persisted run artifacts you can inspect and resume
The repo is designed for two kinds of "agentic" use:
- An external operator agent uses the CLI to create, validate, execute, inspect, and resume workflows.
- A workflow can contain
agentsteps, but those steps run under explicit schemas, retry rules, timeouts, and persisted traces.
That separation is the core design constraint throughout the implementation.
Current MVP Shape
Implemented now:
assign,tool,agent,if,for_each,while,return,fail,noop- YAML loading with workflow normalization and validation
- expression parsing over explicit namespaces
- tool input/output validation with a minimal JSON Schema subset
- structured agent execution with prompt assembly, optional output repair, and schema validation
- persisted runs under
.glyphrail/runs/run_<id>/ run,resume,runs *,tool *,workflow *,check,capabilities,schema,init
Intentionally deferred or partial:
parallelexists in the DSL and explanation/lint inventory, but execution is not implemented yetagent.mode=tool-useis rejected in the current MVP slice- only the built-in
mockagent adapter is available - workflow imports and packaging are not implemented
- config exposes
defaultOutputModeand the capability document listsjsonl, but the current CLI surface practically uses human output and--json - trace event type catalogs include
run.paused, but the current runtime models pause through persisted metadata and checkpoints rather than emitting a dedicatedrun.pausedevent
Why The CLI Matters
Glyphrail is intentionally CLI-first because the CLI is the contract an external AI agent can automate.
A human or agent can:
- discover capabilities with
capabilities --json - inspect schemas with
schema --json - validate a workflow before execution
- run a workflow and receive a stable JSON envelope
- inspect the exact persisted state, output, and trace after the run
- resume a paused run by ID without rebuilding hidden context
That is the "agentic workflow" at the product level: the whole system is shaped so another agent can operate it through explicit terminal primitives.
Architecture Overview
The repo is organized around a small set of core subsystems:
| Area | Purpose |
| --- | --- |
| src/cli | command registry, parser, help formatting, and JSON envelopes |
| src/core | execution engine, errors, trace types, runtime state, schema validation, run storage |
| src/dsl | workflow normalization, schema, and static validation/linting |
| src/tools | tool contracts, registry loading, direct invocation, and policy enforcement |
| src/agent | agent adapter contracts, prompt building, mock adapter, and structured output repair |
| src/config | project config discovery and defaults |
| templates | init, workflow, and tool scaffolding templates |
| playground/mvp | self-contained manual verification project |
| test | integration and unit coverage for CLI, runtime, validation, tools, and resume |
Execution flows roughly like this:
- CLI resolves
--cwd, config, and command arguments. - Workflow YAML is loaded and normalized into the internal AST.
- Validation checks step shapes, expressions, write targets, and declared tools.
- Runtime state is created from
input, top-levelstate, and system/context metadata. - The execution engine walks steps deterministically and persists progress after each completed step.
- Tool and agent steps validate their inputs and outputs against the same schema subset.
- Run artifacts are written to disk so later commands can inspect or resume the run.
Quickest Way To Try It
The fastest path is the included MVP playground.
From the repository root:
bun run src/cli/index.ts --cwd playground/mvp capabilities --json
bun run src/cli/index.ts --cwd playground/mvp check --json
bun run src/cli/index.ts --cwd playground/mvp run workflows/linear.gr.yaml --input inputs/linear.ada.json --jsonFor a full manual smoke pass:
./playground/mvp/smoke.shIf you want shorter commands while working from source:
gr() {
bun run src/cli/index.ts --cwd playground/mvp "$@"
}Then:
gr capabilities --json
gr workflow explain workflows/agent-success.gr.yaml --json
gr run workflows/agent-success.gr.yaml --jsonPrerequisites
- Bun
>= 1.3.0 - TypeScript source is executed directly through Bun
- There are currently no runtime dependencies declared in
package.json
Install From npm
Glyphrail can be published directly to npm and installed globally:
npm install -g glyphrail
glyphrail --helpThe package uses a small Node launcher that delegates to Bun, so the target machine still needs Bun on PATH.
Once installed globally, the CLI uses the folder you run it from by default:
cd /path/to/your/project
glyphrail init
glyphrail check --jsonUseful commands from the repo root:
bun test
bun run src/cli/index.ts --help
bun run src/cli/index.ts capabilities --jsonGetting Started In A Fresh Project
If Glyphrail is installed globally or linked so glyphrail or gr is on your path:
glyphrail init
glyphrail check --json
glyphrail run workflows/hello.gr.yaml --input-json '{"name":"Ada"}' --jsoninit creates:
glyphrail.config.jsonworkflows/hello.gr.yamlglyphrail.tools.ts.glyphrail/runs/
The generated hello workflow is intentionally small:
version: "1.0"
name: hello-world
description: Sample workflow generated by glyphrail init
inputSchema:
type: object
properties:
name:
type: string
required: [name]
defaults:
timeoutMs: 10000
state:
name: null
greeting: null
steps:
- id: init
kind: assign
set:
name: ${input.name}
- id: greet
kind: tool
tool: makeGreeting
input:
name: ${state.name}
save: state.greeting
- id: done
kind: return
output:
greeting: ${state.greeting}The generated tools entry exports a typed local registry:
import type { Tool } from "glyphrail";
const makeGreeting: Tool<{ name: string }, string> = {
name: "makeGreeting",
description: "Create a friendly greeting for the provided name.",
inputSchema: {
type: "object",
properties: {
name: { type: "string" }
},
required: ["name"],
additionalProperties: false
},
outputSchema: {
type: "string"
},
sideEffect: "none",
async execute(input) {
return {
ok: true,
output: `Hello, ${input.name}!`
};
}
};
export default [
makeGreeting
];The registry file no longer needs a runtime import from glyphrail, so a globally installed CLI can run an initialized project without adding a local package dependency first.
If you want editor autocomplete and type-checking for custom tool modules that use import type { Tool } from "glyphrail", install glyphrail in the project as a dev dependency as well.
Publishing To npm
From the repository root:
npm login
npm run prepublishOnly
npm publishFor a scoped package, use a lowercase scope and publish publicly:
npm publish --access publicFor the common release path, use the helper scripts instead:
npm run release:patch
npm run release:minor
npm run release:majorThe release helper expects a clean git worktree and valid npm auth. It runs npm run prepublishOnly, bumps package.json, syncs src/version.ts, creates a release: vX.Y.Z commit and vX.Y.Z tag, publishes to npm, and pushes the current branch plus tags.
If you need a prerelease bump, use:
npm run release -- prereleaseRecommended Operator Workflow For An External Agent
If you are building another AI system on top of Glyphrail, this is the CLI sequence that matches the current codebase best.
1. Discover the installation contract
glyphrail capabilities --json
glyphrail schema workflow --json
glyphrail schema tool --jsonWhy this matters:
capabilitiesis the machine-readable entrypoint for available commands, step kinds, trace event types, agent adapters, and exit codesschemaexposes the static contract documents the implementation is centered around
2. Check project health
glyphrail check --json
glyphrail tool list --jsoncheck aggregates:
- workflow discovery under the configured workflows directory
- validation and lint warnings for each workflow
- tool registry resolution and contract issues
3. Inspect before executing
glyphrail workflow validate workflows/my-flow.gr.yaml --json
glyphrail workflow explain workflows/my-flow.gr.yaml --json
glyphrail workflow lint workflows/my-flow.gr.yaml --jsonworkflow explain --json is especially useful for agents because it exposes:
- metadata
- flattened step inventory
- referenced tools
- control-flow constructs
- policies
- risk points from lint warnings
4. Dry-run the execution plan
glyphrail run workflows/my-flow.gr.yaml --dry-run --input-json '{"name":"Ada"}' --jsonThis validates workflow and input and returns:
- workflow identity
- effective policies
- referenced tools
- effective input after
--setoverrides
5. Execute with stable JSON output
glyphrail run workflows/my-flow.gr.yaml --input-json '{"name":"Ada"}' --jsonSuccess envelope shape:
{
"ok": true,
"command": "run",
"runId": "20260316035745113_4d6cdfb0",
"status": "completed",
"output": {}
}Failure envelope shape:
{
"ok": false,
"error": {
"code": "AGENT_OUTPUT_VALIDATION_ERROR",
"message": "Agent step 'analyze' output failed schema validation.",
"runId": "20260316040000000_deadbeef",
"stepId": "analyze",
"details": {}
}
}6. Inspect the run instead of guessing
glyphrail runs show <run-id> --json
glyphrail runs state <run-id> --json
glyphrail runs output <run-id> --json
glyphrail runs trace <run-id> --json
glyphrail runs explain <run-id> --json
glyphrail runs step <run-id> <step-id> --jsonThis is the intended debugging loop for both humans and agents.
7. Resume if the run is paused
glyphrail runs list --json
glyphrail resume <run-id> --jsonThe runtime resumes from persisted state plus execution cursor, not from prompt history or hidden in-memory context.
Workflow DSL
Glyphrail workflows are YAML documents with a strict top-level shape.
Top-level fields recognized by the validator:
versionnamedescriptioninputSchemaoutputSchemadefaultspoliciesstatestepsoutput
State Model
At runtime, values live in explicit namespaces:
input: the initial JSON inputstate: mutable workflow statecontext: step/runtime context such as current step and loop infosystem: run metadata such as run ID, workflow file, and start time
Expressions can reference:
inputstateenvcontextitembranch
Example:
condition: ${state.count != 3}Supported expression features:
- references like
${state.foo} - literals: strings, numbers, booleans,
null - operators:
==,!=,&&,||,+,-,*,/,%, unary!and- - parentheses
Not supported in the current parser:
- function calls
- arbitrary JS
- array indexing syntax
Step Kinds
| Kind | Status | What it does |
| --- | --- | --- |
| assign | implemented | writes literal or expression-evaluated values into workflow state |
| tool | implemented | invokes a registered TypeScript tool with validated input and optional state write-back |
| agent | implemented in structured mode only | calls a bounded agent adapter and validates the structured output |
| if | implemented | executes then or else deterministically |
| for_each | implemented | iterates over an evaluated array and exposes item |
| while | implemented | loops while a condition remains truthy, bounded by maxIterations |
| parallel | declared but not executable | validation/explanation exist, runtime rejects execution |
| return | implemented | returns explicit output or materialized workflow output |
| fail | implemented | raises a workflow error immediately |
| noop | implemented | intentionally does nothing |
Write Directives
tool and agent steps can use one write directive:
save: state.pathappend: state.arrayPathmerge: state.objectPath
assign uses set.
Rules enforced by validation:
- only one of
save,append, ormerge - the path must target
state.* appendrequires an array targetmergerequires an object target
Control Flow
Implemented control flow:
whenguards on any stepiffor_eachwhilegotothroughonError
while is always bounded by maxIterations.
Example:
- id: count_up
kind: while
condition: ${state.count != 3}
maxIterations: 5
steps:
- id: increment
kind: assign
set:
count: ${state.count + 1}Error Policies
Supported onError.strategy values:
retryfailcontinuegoto
Retries are persisted in run metadata so inspection and resume stay consistent.
Tool Registry And Tool Execution
The tool registry is a default-exported Tool[] array from the configured glyphrail.tools.ts file.
Tool contract fields:
namedescriptioninputSchemaoutputSchema(optional)sideEffect:none,read,write, orexternaltimeoutMs(optional)tags(optional)execute(input, ctx)
Direct tool commands:
glyphrail tool list --json
glyphrail tool show makeGreeting --json
glyphrail tool validate --json
glyphrail tool call makeGreeting --input-json '{"name":"Ada"}' --json
glyphrail tool scaffold format-handle --jsonPolicy behavior that matters operationally:
- workflow
policies.allowToolscan restrict which tools a workflow may call - project config
policies.allowExternalSideEffectsdefaults tofalse - tools with
sideEffect: "write"orsideEffect: "external"are blocked when external side effects are disabled
Example failure:
bun run src/cli/index.ts --cwd playground/mvp tool call sendWebhook --input-json '{"url":"https://example.com"}' --jsonReturns:
{
"ok": false,
"error": {
"code": "POLICY_VIOLATION",
"message": "Tool 'sendWebhook' is blocked because external side effects are disabled."
}
}Agent Execution, In Detail
This is the most important runtime behavior to understand.
Glyphrail does not let an agent own the workflow. The engine still controls:
- when the step runs
- how many times it may retry
- what schema the output must satisfy
- how long it may run
- where the result is written in state
- how the failure is recorded and resumed
What An agent Step Looks Like
The main working example is playground/mvp/workflows/agent-success.gr.yaml:
- id: analyze
kind: agent
mode: structured
provider: mock
model: mock
objective: Determine whether evidence is sufficient
instructions: |
Return strict JSON only.
Be conservative.
input:
goal: ${state.goal}
vendors:
- alpha
- beta
outputSchema:
type: object
properties:
enoughEvidence:
type: boolean
reason:
type: string
required: [enoughEvidence, reason]
additionalProperties: false
save: state.decision
meta:
mockResponse:
rawOutput: |
```json
{"enoughEvidence":true,"reason":"Two vendors are enough for the demo."}
```What The Runtime Does
For each agent step, the engine performs this sequence:
- Evaluate
whenif present. - Resolve
inputexpressions against runtime namespaces. - Build a structured prompt from:
objectiveinstructions- rendered input JSON
- Resolve the adapter:
- currently only
provider: mockexists
- currently only
- Call
runStructured(...)on the adapter with:- run ID
- step ID
- provider/model
- prompt
- attempt number
- input
- output schema
- timeout
meta
- If the adapter returns raw output that is not yet valid structured JSON, attempt one repair pass.
- Validate the resulting structured output against
outputSchema. - Apply
save,append, ormergeif the step succeeds. - Emit trace events and persist checkpoints and metadata.
Prompt Construction
The prompt builder is intentionally minimal and explicit. It produces a string with three sections:
Objective:Instructions:if providedInput JSON:
The agent.called trace event records the generated prompt, which means you can audit what the agent actually saw.
Example trace payload from the current implementation:
{
"event": "agent.called",
"meta": {
"provider": "mock",
"model": "mock",
"mode": "structured",
"attempt": 1,
"prompt": "Objective:\nDetermine whether evidence is sufficient\n\nInstructions:\nReturn strict JSON only.\nBe conservative.\n\nInput JSON:\n{\n \"goal\": \"select vendor\",\n \"vendors\": [\n \"alpha\",\n \"beta\"\n ]\n}"
}
}Structured Output Repair
The repair path is deliberately narrow. If the adapter returns raw text, Glyphrail will try a few safe candidates:
- the trimmed raw output
- content inside fenced code blocks
- the largest JSON object or array fragment it can extract
If one candidate parses as JSON, that repaired value is validated against outputSchema.
This is useful for cases like fenced JSON responses:
```json
{"enoughEvidence":true,"reason":"Two vendors are enough for the demo."}
```The repair attempt is recorded in trace metadata:
repairAttemptedrepairSucceededrepairCandidate
Failure Handling For Agent Steps
An agent step can fail in several ways:
- adapter missing or unsupported provider
- adapter returned an error
- raw output could not be parsed
- parsed output did not satisfy
outputSchema - the step timed out
Those failures then flow through normal step error policy resolution:
retrycontinuegotofail
The engine, not the adapter, owns retry bookkeeping.
That means:
- retry counters are persisted
- traces show attempts and retries
runs explainandruns stepcan reconstruct what happened later
Mock Adapter Model
The built-in mock adapter exists so workflows and tests stay deterministic.
It looks for:
meta.mockResponse- or
meta.mockResponses
Useful patterns:
- force a success with
output - force parse/repair behavior with
rawOutput - force a failure with
error - script retry scenarios with
mockResponses
Example retry fixture:
meta:
mockResponses:
- output:
choice: 42
- output:
choice: vendor-aOn the first attempt the schema fails, the step retries, and the second response succeeds.
What Is Not Implemented For Agents Yet
- no live provider adapters beyond
mock - no
tool-usemode execution - no planner that rewrites workflow structure
- no freeform text-output mode for runtime agent steps
This is deliberate. The implementation is aiming for inspectable and bounded behavior first.
Run Lifecycle And Persistence
Every run gets a durable directory:
.glyphrail/runs/run_<id>/Artifacts:
meta.json: run status, workflow identity, policies, counters, retry counters, cursorinput.json: original run inputstate.latest.json: latest persisted workflow stateoutput.json: final output when the run completedtrace.jsonl: append-only execution tracecheckpoints/: optional checkpoint snapshots after each completed step
The default config sets:
defaultCheckpointEveryStep = true
So the current runtime normally persists a checkpoint after every completed step.
Runtime Counters
The engine tracks and persists:
completedStepsfailedStepsretriesloopIterationscheckpoints
Budgets And Timeouts
The engine enforces:
- max run steps
- max run duration
- per-step timeout
- bounded
whileloops viamaxIterations
Effective values come from:
- command-line overrides
- workflow policies/defaults
- project config defaults
Materialized Output
Workflow output is chosen in this order:
- explicit
returnstep output - top-level workflow
output - full current workflow state snapshot
If outputSchema is defined at the workflow level, the final output is validated before the run is marked completed.
Resume Semantics
Resume is one of the stronger parts of the current implementation.
The engine persists:
- workflow identity
- policies
- execution cursor
- counters
- retry counters
- latest state
resume <run-id> validates that:
- the run is marked
paused - the workflow file still exists
- the workflow still validates
- the workflow name and version still match what the run started with
- a cursor is present
Then it reconstructs runtime namespaces and continues from the persisted cursor.
Important Nuance
The included resume demo is driven by an external interruption pattern, not by a dedicated pause step.
playground/mvp/workflows/resume-loop.gr.yaml uses a tool called interruptOnce that exits the process with code 86 the first time it runs. Because Glyphrail persists state and metadata after completed steps, the run can still be resumed deterministically from the last safe cursor.
That means the current MVP supports resume very well, but "pause" is best thought of as:
- an interrupted process with persisted artifacts
- not a first-class interactive pause/resume protocol yet
Resume Walkthrough
Start the interrupted run:
gr run workflows/resume-loop.gr.yaml --jsonExpected result:
- process exits with code
86 - a paused run is still listed in
.glyphrail/runs/
Inspect and resume:
gr runs list --json
gr runs show <run-id> --json
gr runs state <run-id> --json
gr resume <run-id> --jsonIn the current fixture, the paused metadata should show:
status = "paused"currentStepId = "maybe_interrupt"counters.loopIterations = 2- state
count = 2
After resume, the same run ID completes with:
{
"count": 3,
"resumeSignal": {
"resumed": true
}
}Inspection And Debugging Commands
Workflow inspection
glyphrail workflow validate workflows/linear.gr.yaml --json
glyphrail workflow explain workflows/conditional.gr.yaml --json
glyphrail workflow lint workflows/lint.gr.yaml --jsonRun inspection
glyphrail runs list --json
glyphrail runs show <run-id> --json
glyphrail runs state <run-id> --json
glyphrail runs output <run-id> --json
glyphrail runs trace <run-id> --json
glyphrail runs trace <run-id> --event tool.completed --json
glyphrail runs step <run-id> analyze --json
glyphrail runs explain <run-id> --jsonruns explain is especially useful after agent or retry scenarios because it summarizes:
- attempts
- retries
- tool calls
- agent calls
- terminal status
- last recorded error
Project Config
Default glyphrail.config.json:
{
"schemaVersion": "0.1.0",
"workflowsDir": "./workflows",
"runsDir": "./.glyphrail/runs",
"toolsEntry": "./glyphrail.tools.ts",
"defaultOutputMode": "pretty",
"defaultCheckpointEveryStep": true,
"policies": {
"maxRunSteps": 100,
"maxRunDurationMs": 300000,
"allowExternalSideEffects": false
}
}Config discovery walks upward from --cwd until it finds glyphrail.config.json.
JSON Schema Support
Glyphrail validates tool contracts, workflow input/output, and agent output using a documented minimal JSON Schema subset.
Supported keys include:
typepropertiesitemsrequiredenumconstdefaultadditionalPropertiesminItemsmaxItemsminLengthmaxLengthminimummaximumoneOfanyOf
If you need the exact contract:
glyphrail schema json-schema-subset --jsonCommand Surface
Bootstrap and discovery:
capabilitiesschemainitcheck
Workflow authoring:
workflow createworkflow validateworkflow explainworkflow lint
Tools:
tool listtool showtool calltool validatetool scaffold
Execution and inspection:
runresumeruns listruns showruns stateruns outputruns traceruns stepruns explain
Global flags:
--cwd <path>--config <path>--json--quiet--verbose--color <auto|always|never>--no-color--trace--profile--help--version
Playground Coverage
The MVP playground under playground/mvp covers the implemented execution matrix:
- linear workflow
- deterministic branching
for_each- successful
while whilebudget exhaustion- tool retry
- structured agent success with repair
- structured agent validation failure
- interruption plus resume
Manual guide:
docs/mvp-playground.md
Development
Useful commands:
bun test
bun run src/cli/index.ts --help
bun run src/cli/index.ts capabilities --jsonTests currently cover:
- CLI contract behavior
- workflow validation and explanation
- tool listing, calling, policy enforcement, and scaffolding
- deterministic execution for linear, branch, loop, fail, retry, and agent scenarios
- resume after interruption
Repository Map
Key files worth reading first:
src/core/execution-engine.ts: runtime heart of workflow executionsrc/dsl/validation.ts: workflow validation and linting logicsrc/tools/runtime.ts: tool invocation and policy enforcementsrc/agent/runtime.ts: prompt building and structured output repairsrc/agent/mock-adapter.ts: deterministic adapter used by fixtures and testssrc/cli/commands/run.ts: execution commandsrc/cli/commands/resume.ts: resume commandplayground/mvp/workflows/agent-success.gr.yaml: clean agent-step exampleplayground/mvp/workflows/resume-loop.gr.yaml: resume exampleplayground/mvp/glyphrail.tools.ts: representative local tool registry
Summary
Glyphrail is already a coherent local runtime for deterministic, inspectable workflow execution with bounded structured agent steps. The strongest parts of the current codebase are:
- clear separation between deterministic orchestration and bounded agent behavior
- strong CLI coverage for authoring, execution, and inspection
- persisted run artifacts that make debugging and resume practical
- deterministic testing through the mock adapter and fixture workflows
The main boundaries to keep in mind are also clear:
paralleland agent tool-use are not live yet- real provider adapters are not implemented yet
- the runtime favors explicitness over convenience
If you approach it with that mental model, the current codebase is already usable as a small, local, AI-operable workflow engine.
