The notes a model needs to write Yoke that compiles.

Generated from the language definition for language 1.0.

These are the notes a model needs to write Yoke that compiles and lints clean: the shape of a source, the invariants the compiler and the ruleset enforce, and the vocabulary the grammar is closed by.

A source

  • A FastYoke app is one Yoke source file. app <slug> "Name" { tagline "…" category "…" icon <name> } opens it; a bare slug is the first-party id fastyoke.<slug>, a backticked slug (app crm "CRM") is the app id verbatim.
  • Each entity Name "Singular" plural "Plural" { … } lists its fields as key: type lines with modifiers such as required, label "…", pii, mask except Role, and allow Role, Other. The types: text, longtext, number, money USD, boolean, date, datetime, enum a, b, c, file, json, ref Other, sequence, computed <type> = <expr>.
  • An entity's lifecycle is its workflow { initial S states A, B, S terminal Z event: A -> B } block. State names are PascalCase, exactly one is initial, terminal lists the states a job ends in, and Cancelled is reserved: cancellation is the admin override, never a state or a transition (fsm/cancelled-state).
  • A transition is event: From -> To, optionally guarded: requires field, other (the fields are present), when <expr> (a comparison over the record's own fields, e.g. when role == "manager" and notes < 5), or guard jsonlogic { … }. Events are snake_case verbs. A self-loop (From -> From) must carry a guard (fsm/unbounded-self-loop). A workflow may name its legal path with guarantee A -> B -> C: the first name is the initial state, the last is terminal, and each step has a transition. An edge that leaves that path or returns to it is marked aside. A self-loop is not a step of the guarantee. idempotency job, event, client_key names the retry key, on the workflow or on one transition. A date, a datetime, timestamp, or event_timestamp is rejected. fail Other after the target names the other outcome of a model call. by Manager names who may fire the event. It is an actor list, and it is not a guard. invariant <expr> on the entity or inside the workflow is a contract. The expression is the one the language already has.
  • A transition's do lines are effects; the verbs: append, update, delete, transition, spawn, webhook, run, algorithm, recompute, notify, schedule, infer, classify, extract, summarize.
  • do infer "…" from field schema { name: text, score: enum low, high } timeout 5s into name, score calls the tenant's configured model with only the named fields. The schema uses field types except ref, computed, and file. A matching answer moves the job to the target and writes into. Extra fields, a timeout, or text that is not the schema move it to the fail state. Replay of the same idempotency key does not call the model again. A pii field in from is effect/infer-over-pii. A tenant that is not entitled reports effect/infer-requires-ee and the engine refuses.
  • do classify urgency from notes into urgency timeout 5s, and the same shape with extract or summarize, is that same action. The name is the capability. The prompt lives in tenant policy or a package, so the workflow does not carry it, and lint does not call a model. The schema is the types of the into fields, or a schema { … } block when one is written. When no prompt is configured, the engine takes the fail state and does not call a model. do infer "…" is the form that still prints a prompt string.
  • Queries are query name(param: type) = select … from Entity where … sort … limit n. Pages are page slug "Title" list [Entity] { … } or page slug "Title" detail Entity { … } holding blocks; nav [a, b] and home a follow. seed demo { Entity row { key: value } state S } seeds records; a bare word in a value names another seeded row.
  • A view name (param: Entity, row: query) { … } is a reusable element tree; use name (args) mounts it from a page or another view. Inside a view only <param>.<field> and an each variable reach data (this is unbound there).
  • Identifiers may contain - between alphanumerics, so subtraction is written with spaces. // comments are allowed. Never add an engine block: a tenant-authored app cannot carry one. A block type the registry does not know is page/unknown-block-type.

Blocks and elements

A page or view body holds one block per line. Every head takes its subject, then options as <word> <value> in any order, then an optional #id pin, and any element takes a trailing when <expr> gate. A container's body is { … }. Anything else is the generic block <type> [#id] { <json config> }.

HeadRenders onSubjectOptions
listlist pagesEntitycolumns, label, size, search, sort, layout
boardlist pagesschemasummary, label, hide, size, layout
sectiondetail templates"…"—
fieldeitherfieldlabel, display
relateddetail templatesEntitycolumns, label, sum, state, empty, sort
jobsdetail templates—label, summary, updated
actioneither"…"on, confirm, help, permission, visible, enabled
stateither"…"—
charteither"…"kind
cardeither—title
columnseither——
tabseither——
texteitherexprstyle, format
badgeeitherexprcolour
imageeitherexpralt
linkeitherexpr—
eacheitherwordlayout, columns
useeitherview—

Examples

Three small sources. Each one compiles. The shipped catalogue is not this set.

Work order

A job moves along a legal path. A parts hold leaves that path and comes back.

app `field-service` "Field service" {
  tagline  "A service work order."
  category "Operations"
  icon     box
  tier     free
  brand    primary #4f46e5
}

entity ServiceWorkOrder "Service work order" plural "Service work orders" {
  notes: longtext

  workflow {
    initial Dispatched
    states Dispatched, EnRoute, OnSite, Invoiced, Closed, PartsHold
    terminal Closed
    guarantee Dispatched -> EnRoute -> OnSite -> Invoiced -> Closed
    idempotency job, event, client_key

    dispatch: Dispatched -> EnRoute
    arrive:   EnRoute -> OnSite
    note:     OnSite -> OnSite when notes is not null
    hold:     OnSite -> PartsHold aside
    release:  PartsHold -> OnSite aside
    invoice:  OnSite -> Invoiced requires notes
    close:    Invoiced -> Closed
  }
}

Approval

One role approves along the path, or denies and leaves it.

app `approvals` "Approvals" {
  tagline  "A request waits for one decision."
  category "Operations"
  icon     check
}

entity Request "Request" plural "Requests" {
  decision: enum approved, denied
  reason: text

  workflow {
    initial Submitted
    states Submitted, Approved, Denied
    terminal Approved, Denied
    guarantee Submitted -> Approved
    idempotency job, event, client_key
    invariant decision == "approved" or decision == "denied" or reason == ""

    approve: Submitted -> Approved when decision == "approved" by Manager
    deny: Submitted -> Denied when decision == "denied" aside by Manager
  }
}

Ledger posting

A draft is posted, or voided. A posted entry stays posted.

app `ledger` "Ledger" {
  tagline  "Post an entry, or void the draft."
  category "Finance"
  icon     book
}

entity Entry "Entry" plural "Entries" ledger {
  amount: money USD
  memo: text
  side: enum debit, credit

  workflow {
    initial Draft
    states Draft, Posted, Voided
    terminal Posted, Voided
    guarantee Draft -> Posted
    idempotency job, event, client_key
    invariant amount > 0

    post: Draft -> Posted requires memo by Bookkeeper
    void: Draft -> Voided aside by Bookkeeper
  }
}

Reserved words

Never an identifier; a colliding name is written in backticks: and, app, asc, bridge, by, contribute, data, desc, do, else, entity, false, from, guard, having, in, is, like, limit, me, nav, not, null, offset, or, page, query, requires, rule, script, seed, select, sort, this, true, use, view, when, where, workflow.

Replying

  • Reply with exactly one fenced ```yoke block holding what was asked for: the complete source when the ask is the app, or only the element lines of the addressed body when the ask is a body. Prose outside the fence is ignored; a second fence is a failure.
  • Carry everything the ask does not name through unchanged, comments included. Keep the app line's slug as it is.
  • A lint finding may carry suggestion. Apply suggestion.ops as edit operations and lint again. Do not invent a JSON dialect (states: [A, B], from:) and do not rewrite the file by hand when ops is present. A finding without suggestion has no single safe repair.