Schematics

Small libraries for Effect Schema

Every app hides a second program.

The rules about who may do what. Which records point at which. What happens next — and what happens when the server restarts halfway through. It's smeared across if-statements, foreign keys, and cron jobs, where no person or agent can read it.

Schematics is a family of small Effect libraries that pull that program out into data: something you can inspect, store, test, and explain.

repl — schema-reflection

↑ one real call per package · all six below

00 The hidden program

It's already in your codebase. It just can't talk.

X-ray almost any app and you'll find the same six kinds of knowledge, stashed in files that were never meant to hold them. Each gets its own package. Start with the one that hurts.

01–06 The packages

Six packages. One idea: meaning belongs in data.

Each package does one job and stands on Effect Schema. Every example below uses the real API — the npm release where one exists, the source where it doesn't yet.

Published · 0.1.0

Know what points at what.

The 2 a.m. page

Someone deletes the send-welcome-email action. Two workflows still call it. Nobody notices until a new hire's first day passes without a single email.

Declare ids and references right on your Effect Schema fields. Algebra extracts the graph and reports duplicate ids and dangling references with exact paths — the same graph that powers rename, go-to-definition, and “what breaks if I change this?”

Where it shows up
  • Pricing plans that bundle features
  • CMS entries that link to each other
  • Workflows that call reusable actions
  • Autocomplete and rename for ids in an editor
pnpm add @schema-reflection/algebranpm Source
Explore algebra →
workspace.tsnpm API
import { Schema } from "effect"
import { Relation } from "@schema-reflection/algebra"

const Action = Schema.Struct({
  id: Relation.id("Action"),
  label: Schema.String,
})

const Workflow = Schema.Struct({
  id: Relation.id("Workflow"),
  actionIds: Relation.refs("Action"),
})

const Workspace = Schema.Struct({
  actions: Schema.Array(Action),
  workflows: Schema.Array(Workflow),
})

Relation.validate(Workspace, config)
// → [{ code: "unresolved-ref", path: [...], message: ... }]
▶ try itsimulated in your browser

Interactive simulation loads when this panel comes into view.

Published · 0.1.0

Rules you can read back.

The Slack thread

“Can a team lead approve a $700 refund?” The answer is an if statement in a file support will never open — so they ask an engineer. Every time.

Conditions become immutable JSON. You define the leaf language with Effect Schema; the library gives you validated and / or composition, normalization, evaluation, and a fold to turn the same rule into a sentence, a query, or anything else.

Where it shows up
  • Refund and approval policies an admin UI can show
  • Feature-flag targeting stored in a database
  • Eligibility rules for plans and promotions
  • Audience segments people can edit safely
pnpm add @schema-reflection/predicatesnpm Source
Explore predicates →
refund-policy.tsnpm API
import { Schema } from "effect"
import { Predicate } from "@schema-reflection/predicates"

const Role = Schema.Struct({
  _tag: Schema.Literal("Role"),
  is: Schema.Literals(["agent", "lead", "finance"]),
})
const MaxAmount = Schema.Struct({
  _tag: Schema.Literal("MaxAmount"),
  usd: Schema.Number,
})

const Policy = Predicate.nested({ leafSchemas: [Role, MaxAmount] })

const CanApproveRefund = Policy.or([
  { _tag: "Role", is: "finance" },
  Policy.and([{ _tag: "Role", is: "lead" }, { _tag: "MaxAmount", usd: 500 }]),
])

Policy.evaluate(CanApproveRefund, (leaf) =>
  leaf._tag === "Role" ? user.role === leaf.is : refund.usd <= leaf.usd)
▶ try itsimulated in your browser

Interactive simulation loads when this panel comes into view.

Published · 0.1.0

See what an action does before it does it.

The code review

An agent wants to call approveCandidate. What will it write? Which events will it fire? Who is allowed to call it? Today the honest answer is “read the code and hope.”

Actions are syntax trees built from small primitives — require, set, emit, match, retry — and run by an Effect interpreter. Print one for review, serialize it to JSON, and run it in memory to assert on its result, state, and events.

Where it shows up
  • Business actions an agent can call and a human can review
  • Automation steps stored as JSON and versioned
  • Testing rules by their observable effects
  • Swapping storage and event sinks per environment
pnpm add @schema-reflection/logicnpm Source
Explore logic →
approve-candidate.tsnpm API
import { Logic } from "@schema-reflection/logic"

const ApproveCandidate = Logic.Action("ApproveCandidate", ($) =>
  $.do(
    $.require($.eq($.ref.actor.role, "reviewer"), "forbidden"),
    $.set($.ref.candidate.status, "approved"),
    $.emit("candidate.approved", { id: $.ref.candidate.id }),
    $.get($.ref.candidate.status),
  ),
)

Logic.print(ApproveCandidate)     // review it
Logic.serialize(ApproveCandidate) // store it
await Logic.run(ApproveCandidate, { actor, candidate }) // test it
▶ try itsimulated in your browser

Interactive simulation loads when this panel comes into view.

Experimental prototype · source only

Survive the deploy in the middle.

The deploy

A hiring approval waits three days for a reviewer. On day two you ship a release, the process restarts, and the approval quietly evaporates.

Workflows are data: run an action, await a correlated event, sleep, branch. Pure start, advance, and resume functions return the commands to dispatch and a JSON checkpoint — store it anywhere, reload it in a fresh process, and pick up exactly where you left off.

Where it shows up
  • Hiring and purchase approvals
  • Document signing that waits on people
  • Onboarding sequences with timers
  • Human-in-the-loop agent tasks
Not on npm yet.Read the source
Explore workflow →
review.tsexperimental source API
import { Workflow, Expr } from "@schema-reflection/workflow"

const Review = Workflow.define("Review", { version: 1, input, output },
  ({ input, steps }) => {
    const request = Workflow.run(steps.id("request"), RequestReview, {
      candidateId: input.at("candidateId"),
    })
    const review = Workflow.awaitEvent(steps.id("review"), ReviewSubmitted, {
      correlate: request.output.at("requestId"),
      timeoutMs: 259_200_000, // three days
    })
    return Workflow.sequence(request, review,
      Workflow.branch(steps.id("decision"),
        review.output.at("decision").pipe(Expr.equal("approved")),
        Workflow.complete("approved"),
        Workflow.complete("rejected")))
  })

const { checkpoint, commands } =
  Workflow.start(Review, "review-1", { candidateId: "c1" }, Date.now())
▶ try itsimulated in your browser

Interactive simulation loads when this panel comes into view.

Experimental · source only

Ship the schema, not just the type.

The handoff

Your Candidate type lives in TypeScript. Your database, your partner's service, and the agent on the other end of a tool call can't import TypeScript.

MetaSchema encodes a portable subset of Effect Schema as versioned JSON and rebuilds a working schema on the other side. Anything it can't carry faithfully — refinements, transformations — fails loudly with the path instead of being silently dropped. It is the foundation predicates, logic, and workflow share.

Where it shows up
  • Contracts stored next to the data they describe
  • Schemas sent across a service boundary
  • Offline tools that inspect what a program accepts
  • Portable expressions shared by every other package
Not on npm yet.Read the source
Explore core →
candidate.tsexperimental source API
import { Schema } from "effect"
import { MetaSchema } from "@schema-reflection/core"

const Candidate = Schema.Struct({
  id: Schema.String,
  status: Schema.Literals(["pending", "approved"]),
  note: Schema.optionalKey(Schema.String),
})

const json = MetaSchema.encode(Candidate)   // send it anywhere
const contract = MetaSchema.decode(json)    // checked on arrival
const schema = MetaSchema.toSchema(contract) // a working schema again

MetaSchema.validate(contract, { id: "c1", status: "pending" })
▶ try itsimulated in your browser

Interactive simulation loads when this panel comes into view.

Planned · spec only

A folder is a database with no schema.

The config repo

Four hundred YAML files. Half of them reference the other half. The only thing checking them is a script someone wrote in 2021 — or nothing at all.

One Layout declares which files may exist, how each is parsed, the schema it must satisfy, and which values point at other files. From that: diagnostics with file, line, and column across the whole folder — and later, edits that keep it valid and preserve formatting.

Where it shows up
  • Config-as-code repositories
  • Content folders with cross-links
  • Agent workspaces that must stay valid
  • Monorepo metadata
Not implemented or installable yet.Read the specification
Explore filesystem →
layout.ts · proposed APIproposed API
const Project = Layout.make({
  files: {
    "project.yaml": Layout.file(Format.yaml, ProjectSchema, { required: true }),
    "actions/:action.json": Layout.file(Format.json, ActionSchema, {
      identity: { type: "Action", capture: "action" },
    }),
    "workflows/**/*.yaml": Layout.file(Format.yaml, WorkflowSchema),
  },
  unmatched: "error",
})
▶ what you'd getillustration

07 All together

One approval.
Four packages.
Just JSON.

The approval example is authored entirely as JSON. core describes the shapes, predicates decides who may approve, logic says what approving does, and workflow waits — across restarts — for a person to decide. An independent TypeScript definition produces exactly the same artifact.

08 Before you bet on it

What you should know first.

It is pre-1.0.
Every package is experimental and every artifact format is versioned. Pin exact versions and expect breaking changes.
It rides Effect 4 release candidates.
Each package pins an exact Effect 4 pre-release. Today's npm releases peer on different ones (algebra on 4.0.0-beta.68, predicates and logic on 4.0.0-rc.113), so check before mixing them in one runtime.
Take only what you need.
Algebra stands alone. Core sits under predicates, logic, and workflow, and nothing depends upward — using predicates never drags in an action language.
The demos are simulations.
Each “try it” panel runs on Foldkit and Foldworks and mirrors behavior documented in the package README; the code beside it is the real API.

09 Part of the WorldVM family

Small experiments. Bigger worlds.

Schematics is one thread in a family of open experiments in software that can describe, remember, coordinate, and explain itself. WorldVM is where they meet: a runtime for operational worlds of entities, rules, goals, and actions.

Meet the whole family at worldvm.com