graview-workflows

graview-workflows is one of Graview's 31 authoring skills: instructions an AI coding assistant such as Claude Code or Codex reads before it changes a Graview app. What it teaches: Have a Graview app do something on its own — fill next week from a pattern, keep one record in step with another, set aside what is done, send what is public each morning — declared as a workflow (or a kind that follows another), checked by graview check and proven by a dry run before anything is kept.

Installed into a project by graview skills install ., and read by whichever assistant is working beside you.

The skill

Let the app do it on its own

A person says one sentence — "fill out next week's events every Sunday" — and expects it to keep happening. Writing that as an outside script loses everything the app already knows: who may do what, what a rerun must not duplicate, what a person changed by hand, what Activity says happened. A

workflow is the app's own actor given a clock and a list of steps, in the declaration (workflows, beside kinds), so the checker reads it, the Scene draws it, and every run is one row in Activity that undo takes back.

"workflows": {
  "next-weeks-events": {
    "title": "Fill out next week's events",          // what a person reads
    "when": [{ "every": "sunday 18:00" }],            // or cron, made, changed, synced, byHand
    "approve": "apply",                               // or "suggest", "ask"
    "steps": [ /* in order */ ]
  }
}

when is one or several: { "every": "day 03:00" | "weekday 09:00" | "sunday 18:00" | "month 1 09:00" | "hour" } or { "cron": "0 18 * * 0" }, both on the brand's timeZone; { "made": "<kind>", "where"? }, { "changed": "<kind>", "fields"? }, { "synced": "<connection>" }, { "byHand": true }. A step is one of a closed set: act (run an act, once or over the records a rule picks), agent (a model proposes acts from a list), generate (dated records from a pattern), archive (set aside by a rule). Read packages/core/src/workflow-shape.ts for every key.

Do this

Pick the recipe that matches the sentence. Each ends the same way: check, then a dry run, then the real one.

  1. "Fill out next week's events every Sunday." A generate step: for each day of the window a source's date falls on — a date, a date that repeats, a set of weekdays — one record, linked back to its source, made once per key ("{source}:{date}" unless you say one): ```json { "title": "Fill out next week's events", "when": [{ "every": "sunday 18:00" }], "steps": [{ "generate": "event", "from": [{ "kind": "routine", "where": "active", "dates": "startsAt" }], "window": "next week", "set": { "name": { "expr": "name" }, "startsAt": "$date" }, "link": "pattern" }] } ``` A rerun changes what the source changed and never what a person changed on an event, unless the step says "overwrite": true; a skipped or moved occurrence stays as the person left it. Another source is another entry in from (a plan item's date, a drive's weekdays "days").
  1. "Keep events in step with plan items." Not a workflow: the event kind follows its source, in the same change as the edit, so it needs no clock and is never a minute behind: ```json "event": { "edges": { "plan": { "to": ["plan-item"], "cardinality": "one" } }, "derives": { "from": "plan-item", "via": "plan", "set": { "name": { "expr": "name" } } }, … } ``` Renaming the plan item renames its event in that change; an edit on the event to a field set from a source field as it is flows back; archiving the source archives the event. A changed trigger on a workflow would do the same thing later, and twice — reach for derives first.
  1. "Archive done grocery lists after 3 days." An archive step sets aside, never deletes — the list is still there, Unarchive brings it back: ```json { "title": "Set aside done grocery lists", "when": [{ "every": "day 03:00" }], "steps": [{ "archive": "grocery-list", "where": "status == 'done'", "after": "3 days" }] } ``` A record a person said to leave alone is left; one a person brought back after a run is not set aside again.
  1. "Sync what's public each morning." The connection does the sending (graview-sync); the workflow decides what goes. An act step over the records a rule picks, keyed so a rerun changes the listing it made rather than making another: ```json { "title": "Send what's public each morning", "when": [{ "every": "day 07:00" }], "steps": [{ "act": "add-listing", "over": { "kind": "event", "where": "public" }, "args": { "name": { "expr": "name" }, "startsAt": { "expr": "startsAt" } }, "key": "{id}" }] } ``` A workflow whose steps write a kind a connection sends out defaults to "approve": "ask": each run waits in the Suggested tray as one suggestion, and a person approves it before anything leaves the app. Say "approve": "apply" only when the person asked for it unattended; the checker then warns workflow-sends-out-unasked — allowed, but said.

What its changes become. apply makes them. suggest makes what it is sure enough of and puts the rest in the Suggested tray. ask gathers a run into one suggestion a person approves whole or item by item. A step may say its own approve. An agent step — { "agent": "<instruction>", "reads": [...], "acts": [...] } — needs the host's model; with none lent the step is skipped and the run says so, never fails.

Labeling each record. A classify step — { "classify": "feedback", "field": "category" } — asks the model which option fits each record. "otherwise": "needs-look" labels a record no option clears (else it is left unlabeled for a person); "applyAt": { "spam": 0.85 } sets a label at least that sure without asking, and asks below it. An option in confirm is always asked. An option the field lacks, or a number past 0 to 1, is a workflow-classify-* finding.

Reaching another app. An agent step's reads and acts, and an act step's act, may name another app's kind or act the way a relation to another app does: "@self/graview-public/ticket", "@self/graview-public/set-ticket-status" — @, the app's address, /, the name. The run reaches it only through the host's farApps (FarApps.otherApp(address, origin) → { shape, read, act }), as the workflow's actor; a dry run lists each far act in run.far, naming the app, and keeps nothing there; a real one runs it there with the run's lineage, so that app's log says which run made it. A host with none refuses the step: "this host does not let workflows reach other apps". A host refuses in its own words by answering { refused: "sentence" }; the step says that sentence. farAppsOver({ "@self/graview-public": { store, name, refuses } }) is that host over stores in memory.

Say it in TypeScript the same way: defineApp({ workflows }) takes the same objects.

Then find out whether it worked

npx graview check                     # the workflow-* findings, each with its fix
npx graview workflow ./app.json       # every workflow in a sentence, paused or not, when it next runs
npx graview workflow ./app.json next-weeks-events --data ./data --dry --now 2026-11-01T18:00:00

graview check refuses a name that is not there — workflow-act-unknown, workflow-kind-unknown, workflow-field-unknown, workflow-connection-unknown, workflow-key-field-unknown — a rule it cannot read (workflow-where-invalid), a clock it cannot read (workflow-schedule-invalid, naming what it can), a step list or an agent's acts left empty, and another app's name that is not @<address>/<name> (workflow-far-reference). Whether that app has the kind or act is only known to a host that passes resolveFarApp (workflow-far-app-unknown, workflow-far-args). Each says the edit that fixes it.

The dry run prints what the run would make, change, set aside and suggest, and keeps nothing. Read it against the sentence: next week's seven days, not this week's; the done lists, not the open ones. Then run it without --dry twice: the first after a change is dry whatever you ask (it says "First run after a change: nothing was kept"), the next keeps what it does, and a third over the same records says nothing-new — the proof a rerun makes nothing twice. The runs are kept beside the store in workflow-runs.json.

From altitude the Scene draws each workflow in the street beside the district it writes, wired to what it reads and writes, a paused one dimmed, saying its last run when the host hands workflowRuns. An owner's seat runs, pauses and resumes one through run_workflow, pause_workflow and resume_workflow when its host hands seatWorkflows(…) (`graview mcp

--workflows); Graview Cloud's chat does it through propose_change with add-workflow, set-workflow, pause-workflow, resume-workflow`.

A host runs workflows on their clocks itself: nextRunAt(workflow, after, zone) and triggeredBy(workflow, commit, app) from @graview/core/workflows say when, runWorkflow from @graview/tools/workflows runs one, and its WorkflowLog keeps the runs. A run says what its model steps spent (spent: calls and tokens, by step and in all) — meter from that, never by wrapping the model. Pass a trigger's by (the workflow whose run fired it) into the run: it carries its chain, and one deeper than chainLimit (3) is held and says so, so two workflows that wake each other stop. workflowRuns takes counts (made: 3) as well as ids.

What the check cannot see

  • Whether the sentence was understood. graview check holds every name to the declaration; only the dry run shows the step picks the records the person meant. Read its made and archived, not just its outcome.
  • Time zones in the person's head. "Sunday at six" is the brand's timeZone; an app with none runs on UTC. Say the zone.
  • A model's judgment. An agent step proposes; whether its proposals are right is a person's to judge in the tray. Keep it on suggest or ask until it has earned apply.
  • That anything runs it. A declaration is not a cron. graview serve, Graview Cloud or your own host must call runWorkflow on its clock; a local folder only runs when you run graview workflow.