← Skills

typedb

typedb

The brain of record — TypeQL against the ONE substrate on TypeDB 3.12.1. Use when editing any .tql under schema/, writing a fun, composing match/insert/update/put, reading through the /v1/ HTTP API, or diagnosing a TypeDB error code or a query that hangs. Triggers — "write a TypeQL query", "add a fun", "change the schema", "deploy the schema", "this query times out", "why is this .tql rejected", "TypeDB 2.x vs 3.x", "which schema files are deployed". Depth lives in .claude/skills/typedb; this is the runtime agent's copy of the rules that bite.

TypeDB — the brain, never the request path

The rule that decides most reviews

TypeDB is the brain of record and is never on the request path. It syncs to
JSON in RAM (BrainDO /brain/* + a KV snapshot) and a receiver reads the
snapshot. Writes go fire-and-forget or through the hourly sync/ worker. The
same door answers in 26–58ms from the snapshot and 1.2–1.6s as a live
query. A receiver you add or touch reads KV / /brain/* JSON, never
typedbQuery. resolvers/factory.ts is the deliberate exception: there the
graph write IS the deliverable, so it throws on failure.

Instrument: bash .claude/scripts/signal-watch.sh — a receiver that reaches for
the brain reads red instead of merely feeling slow.

The deployed stack is not every .tql in the repo

Measured: one.tql roles.tql do.tql reason.tql router.tql channels.tql.
world.tql is NOT loaded. A fun declared in an undeployed file is
reachable through fn:run's allowlist and still answers upstream_400
which reads exactly like a broken query and is not one. Check the stack
(.claude/scripts/typedb-env.sh) before debugging a fun that "does not work".

Same trap in the field names: one.tql names a signal's payload field
payload; world.tql names it data. Code written against data under a
.catch(() => {}) fails silently forever.

Writing a fun

  • Functions only over the six locked dimensions. A fun that needs a seventh
    dimension is a fun that is wrong.
  • A $beat-style parameter is passed as the FULL pair ("\"beat\":\"want\""),
    never assembled by string concat — the payload is matched with contains.
  • Every id you interpolate must be TQL-safe by construction, not by escaping.
  • Register the fun in the allowlist or fn:run refuses it by name, which is a
    clearer failure than a silent empty result — prefer it.

An absent answer is not a benign answer

The house bug in this repo: a fun over data nobody writes returns 0 or empty,
and a caller reads that as "fine". story_beats_told answered 0 for every story
because three funs read a "beat" convention with zero writers. When you add a
fun, say in one line what it answers when the data does not exist yet, and make
that answer distinguishable from a real zero.

Cannot run

§ An absent answer is not a benign answer is the house bug. Applied to yourself,
it has a name: say cannot-run rather than return the empty result.

  • You do not know which stack is deployed. Measured, the deployed set is
    one.tql roles.tql do.tql reason.tql router.tql channels.tql and world.tql
    is not in it
    . A fun in an undeployed file answers upstream_400, which reads
    exactly like a broken query. Until .claude/scripts/typedb-env.sh has told you
    which stack you are on, a failing fun is undiagnosed, not diagnosed.
  • A missing fun and an undeployed schema are the same bytes. The
    fn-allowlist.ts gate masks deployment status: a fun absent from the
    allowlist is unreachable from every surface whether or not the schema carrying
    it ever landed. fn:run <name> returning rows, or returning REP4, is the only
    thing that separates them, and either answer settles it — so halt and say
    which was not done rather than reporting a schema as live. The same refusal
    covers "is this migration applied": the numbered file on disk is evidence it
    was written, never that it ran.
  • A zero has no writer behind it. story_beats_told answered 0 for every
    story because three funs read a "beat" convention nothing wrote. If you
    cannot point to the code that writes the rows your query counts, 0 is
    cannot-run and must never be reported as "none". An empty result is not a
    clean graph, and in a child-protection surface "no concerns recorded" and "we
    could not ask" must never be the same answer.
  • The two field names are in play. one.tql calls a signal's payload
    payload and world.tql calls it data. Where the caller is under a
    .catch(() => {}) you will see silence from both a wrong name and an empty
    table — that is indistinguishable, and indistinguishable is cannot-run.
  • You were asked to put it on the request path. That is not an unanswerable
    question, it is a refused one: the brain is never on the request path, and the
    answer is the snapshot. Say so and stop.

Return { ok: false, reason: "<which of those, and the line that says so>" }
and close with warn.