Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Make a canister's data queryable by the Caffeine Data Intelligence agent. Use whenever an app stores structured data (Maps/Lists/arrays of records) that should be answerable in natural language — "top customers", "revenue by region", "active projects". Adds a discoverable `schema()` and a JSON `execute()` query endpoint via the `caffeineai-oql` mops package's `Expose` mixin.
.claude/skills/aiskillstore-extension-oql/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 49% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 85% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 117% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 522% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 164% | 0% |
Go over the actor's fields (non-transient) and, for each collection worth querying, consider how its data maps to a table in a database (an entity). You only declare one entity per table — the Expose mixin makes them queryable.
Each entity carries an authorization level; the default .controllerOnly() is safe (private to users, still readable by the Data Intelligence agent). Model your entities first, then pick a level per entity — see ## Auth.
Run mops add caffeineai-oql@0.4.0 in the same write batch as your first mo:caffeineai-oql/... import. Auto-derivation requires moc >= 1.11 (the generated-app template already satisfies this).
.toEntity(name, typeName, primaryKey) turns a collection of records into a queryable entity; the compiler auto-derives the fields. Each entity sets its own authorization level (see ## Auth); the example below shows one table per level. Expose adds only the OQL query methods (schema / execute) — your existing state, types, and shared methods are untouched.
motoko filepath=src/backend/main.moimport Map "mo:core/Map"; import Nat "mo:core/Nat"; import Principal "mo:core/Principal"; import OQL "mo:caffeineai-oql"; import Expose "mo:caffeineai-oql/Expose"; actor { type Product = { id : Nat; name : Text; priceUsd : Nat }; type Vendor = { id : Nat; name : Text }; type AuditLog = { id : Nat; action : Text; atNs : Nat }; type Note = { id : Nat; user : Principal; body : Text }; type Document = { id : Nat; owner : Principal; title : Text }; type User = { id : Principal; isAdmin : Bool }; let products = Map.empty<Nat, Product>(); let vendors = Map.empty<Nat, Vendor>(); let supplies = Map.empty<Product, Vendor>(); let auditLogs = Map.empty<Nat, AuditLog>(); let notes = Map.empty<Nat, Note>(); let documents = Map.empty<Nat, Document>(); // not all collections need to be exposed if there is no need — `users` backs // auth only, so it is intentionally never turned into an entity below let users = Map.empty<Principal, User>(); let anyP = Principal.fromText("aaaaa-aa"); // sample owner; the value is ignored // Look up whether a caller is an admin. func isAdmin(p : Principal) : Bool = switch (users.get(p)) { case (?u) u.isAdmin; case null false }; // A custom .ownedByWith rule: admins see every document, everyone else only // their own. `owner` is the field's Value — a Principal column arrives as #text. func canSeeDocument(caller : Principal, owner : OQL.Value) : Bool = isAdmin(caller) or owner == #text(caller.toText()); include Expose({ entities = [ // #public_ — anyone, incl. anonymous, reads the whole catalogue products.toEntity("product", "Product", "id") .sample({ id = 0; name = ""; priceUsd = 0 }) .public_() .build(), vendors.toEntity("vendor", "Vendor", "id") .sample({ id = 0; name = "" }) .public_() .build(), // `supplies : Map<Product, Vendor>` — a map between two non-primitive types. // The identity lives in the key/value records, not a field, so iterate // .entries() in manual mode, promote each side's id, and .edge both — a // query can then traverse "product.name" and "vendor.name". OQL.Entity.manual<(Product, Vendor)>("supply", func () = supplies.entries(), "Supply", "key") .payload("key", func ((p, v)) = p.id.toText() # ":" # v.id.toText()) .payload("product", func ((p, _)) = p.id) .edge("product", "product") .payload("vendor", func ((_, v)) = v.id) .edge("vendor", "vendor") .controllerOnly() .build(), // #controllerOnly (the default, shown explicitly) — only the platform reads auditLogs.toEntity("auditLog", "AuditLog", "id") .sample({ id = 0; action = ""; atNs = 0 }) .controllerOnly() .build(), // #scopedPerUser — each signed-in user reads only their own rows notes.toEntity("note", "Note", "id") .sample({ id = 0; user = anyP; body = "" }) .ownedBy("user") .scopedPerUser() .build(), // #controllerOrScoped — controller reads all; scoped reads use canSeeDocument. documents.toEntity("document", "Document", "id") .sample({ id = 0; owner = anyP; title = "" }) .ownedByWith("owner", canSeeDocument) .controllerOrScoped() .build(), ]; }); }
Authorization is per entity — each builder declares a level, and schema() and execute() both run the check against the live caller. No app-wide config, no tokens. The default when none is set is #controllerOnly.
| Builder call | Who reads | Rows returned | |---|---|---| | .public_() | anyone (incl. anonymous) | all | | .controllerOnly() (default) | controllers only | all | | .scopedPerUser() | any signed-in caller | only the caller's own | | .controllerOrScoped() | controllers + signed-in callers | controller: all; user: own |
Pick per entity by who should read its rows — when in doubt, keep the default.
.controllerOnly() (default) — private app data the agent should answerover, but no end user reads directly (orders, metrics, audit logs, config). The agent calls as the controller, so it reads everything while the data stays private to users.
.public_() — world-readable data, including logged-out visitors (publiccatalogue, published content, leaderboards).
.controllerOrScoped() — per-user data where each user reads only their ownrows, but the agent must still answer aggregate questions (profiles, a user's orders). Requires an owner column.
.scopedPerUser() — strictly private per-user data: each user reads onlytheir own, and the agent is scoped too, so it cannot answer over this table (DMs, private journals). Requires an owner column — prefer .controllerOrScoped() unless the agent must be blind to it.
The user may override per entity; if a request implies per-user data but is ambiguous, ask.
Scoped levels (.scopedPerUser(), .controllerOrScoped()) need a way to know which rows belong to the caller — an owner column or a subject-honouring source. .build() traps if a scoped entity has neither, and also traps if a .public_() entity declares an owner (the check would never run). This is the guardrail against the common data-leak footgun.
When to tag: a Principal field is the signal.
.ownedBy(field) — the field is the owner; visibility is identity equality..ownedByWith(field, canSee) — custom visibility (teams, admins, sharing).canSee : (caller : Principal, owner : Value) -> Bool decides per row; field need not be a Principal, and the closure can read actor state.
A scoped caller sees only its owned rows — both as the query target and through a join — so traversal can never leak another owner's rows.
mo// Per-user notes: each signed-in user reads only their own rows. notes.toEntity("note", "Note", "id") .sample({ id = 0; owner = Principal.fromText("aaaaa-aa") /* any principal */; body = "" }) .ownedBy("owner") .scopedPerUser() .build() // .ownedByWith custom rule: the owner sees their own docs, listed admins see // everyone's, and the platform controller sees all (#controllerOrScoped). // `owner` is the field's Value — a Principal column arrives as #text(principal). docs.toEntity("doc", "Doc", "id") .ownedByWith("owner", func (caller, owner) = admins.get(caller) != null or owner == #text(caller.toText())) .controllerOrScoped() .build()
.ownedBy(f) is exactly .ownedByWith(f, OQL.Entity.ownerIsCaller). At most one owner column; it must be a real field, not also .edge / .hidden. For owner-keyed storage (Map<Principal, List<T>>) use OQL.Entity.newScoped(name, scopedIter, typeName, primaryKey) so the scan is O(user rows): scopedIter(?p) returns only p's rows, scopedIter(null) all (schema seeding).
Two modes, picked by the row type T.
.toEntityFor records whose fields are all primitives with a built-in _toRow (Nat, Int, Float, Text, Bool, the sized Nat/Int widths, Principal):
mocustomers.toEntity(name, typeName, primaryKey) .sample(template) // REQUIRED if the collection may be empty at build .edge(field, targetEntity) // tag an existing field as a foreign key .ownedBy(field) // (or .ownedByWith(field, canSee)) per-user scoping .scopedPerUser() // auth level: .public_ / .controllerOnly (default) / .scopedPerUser / .controllerOrScoped .hidden(field) // drop a field from schema + default projection .build()
.toEntity is sugar for OQL.Entity.new<T>(name, func () = coll.values(), …);it exists on Map, Set, List, [T], and [var T]. It iterates values only — if a row's identity (PK or owner) lives in the Map key, it is not a field: promote it via manual mode over .entries(), or OQL.Entity.newScoped when it's the owner.
primaryKey, and any .edge / .ownedBy field, must name a real,non-.hidden column of the row.
.edge(name, target) tags an existing field (it does not add one) as an FK,enabling dotted-path traversal "name.targetField" in queries. FK/PK types must be Text, Nat/Int, or Bool (Float keys are rejected), and the target's primary key must not be .hidden.
.sample(template) seeds schema discovery; without it an empty collectionyields an empty schema. Only the shape matters, not the values.
Schema fields are listed in lexicographic order (the __record combiner's canonical form); sort client-side if display order matters.
.toEntityManual / OQL.Entity.manualFor non-record T, computed fields, or records with nested / variant / option / collection fields:
moauthors.toEntityManual<Author>("author", "Author", "id") .payload("name", func a = a.name) // one field; extract returns a _toRow value .flatten(func a = a.address) // splice a nested record's fields as columns .payload("tagCount", func a = a.tags.size()) // .edge / .hidden work as in auto mode, by field name .build()
.payload(name, extract) — name must not contain .. For options/variants,return Text/Nat with a sentinel (see below).
.flatten(extract : T -> S) — S must be flat; each of its fields becomes atop-level column. Drop unwanted ones with .hidden. Name collisions get __1, __2 suffixes (nothing is dropped).
OQL.Entity.manual<T>(name, iter, typeName, primaryKey) for arbitrary rowsources (custom flatteners, filtered iterators).
OQL.Value is { #null_; #bool; #nat; #int; #float; #text }. Numeric variants compare across each other, so a JSON integer threshold matches a Float value.
| Row type T | Mode | |---|---| | All-primitive record | .toEntity | | Record with ? / variant / nested field | .toEntity once you ship <Type>Value.mo (below); else manual | | Record with a collection field | manual — .size() or Text.join into a payload | | Tuple / primitive / computed | manual |
To keep a record on the auto-derive path, give each non-primitive field type a _toRow : T -> OQL.Value: one file per type named <TypeName>Value.mo, a single public func _toRow, imported top-level in the file that declares entities (the resolver does not walk submodules). Parent records then ride .toEntity(...) with no per-field .payload.
mo// OptTextValue.mo — option → sentinel module { public func _toRow(self : ?Text) : OQL.Value = switch self { case null { #text("") }; case (?t) { #text(t) } }; }; // StatusValue.mo — variant → tag text module { public func _toRow(self : Status) : OQL.Value = #text(switch self { case (#draft) "draft"; case (#published) "published" }); }; // DepartmentValue.mo — nested record → child PK (then .edge the field) module { public func _toRow(self : Department) : OQL.Value = #text(self.name); };
Always return ONE Value variant, even for null (sentinel "" / 0 / false) — a _toRow that sometimes returns #null_ makes the reported schema type flip-flop by row order. Sentinels keep the field queryable (eq value "" matches the nulls). For a one-off field, inline the same conversion in a .payload instead of a module; lift to a module only when 2+ entities need it. A record used both as an entity and as a nested field just ships its <Type>Value.mo — the structural Row derivation and your Value collapse are distinct types and coexist.
The same storage can back several entities — pick what the client should see:
Map<K1, Map<K2, V>> into rows; have the flattener emita flat record (not a tuple) so it still auto-derives, then .edge the promoted keys.
Map<Author, …>.keys()) viaOQL.Entity.manual; entries with no rows simply don't appear.
queryable from both sides:
moOQL.Entity.manual<(Article, Text)>("articleTag", func () = flattenTags(articles), "Pair", "pair") .payload("article", func ((a, _)) = a.id) .edge("article", "article") .payload("tag", func ((_, t)) = t) .edge("tag", "tag") .build()
mops add caffeineai-oql@0.4.0 in the same batch as the first import.toEntity (all-primitive) or.toEntityManual / OQL.Entity.manual otherwise
<Type>Value.mo for every non-primitive field reused across entities,imported top-level
.sample(template) whenever the collection may be empty at build time.edge(name, target); filter-only fields .hidden(name)Value variant.ownedBy / .ownedByWith and a scoped level(.scopedPerUser() / .controllerOrScoped()) — never bare .controllerOnly()
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 30,496 | 20,135 | -34% | 1 | 1 | 0% | 4,976 | 7,402 | +49% | 0 | 0 | — |
case-02 | fail→pass | 24,109 | 17,001 | -29% | 1 | 1 | 0% | 3,508 | 6,500 | +85% | 0 | 0 | — |
case-03 | fail→pass | 23,677 | 19,139 | -19% | 1 | 1 | 0% | 3,237 | 7,011 | +117% | 0 | 0 | — |
case-04 | pass→pass | 16,845 | 17,492 | +4% | 1 | 1 | 0% | 2,961 | 6,495 | +119% | 0 | 0 | — |
case-05 | pass→pass | 11,609 | 9,371 | -19% | 1 | 1 | 0% | 1,194 | 4,801 | +302% | 0 | 0 | — |
case-06 | pass→pass | 12,078 | 12,725 | +5% | 1 | 1 | 0% | 1,283 | 5,487 | +328% | 0 | 0 | — |
case-07 | fail→pass | 9,522 | 7,121 | -25% | 1 | 1 | 0% | 727 | 4,522 | +522% | 0 | 0 | — |
case-08 | fail→pass | 13,171 | 8,145 | -38% | 1 | 1 | 0% | 2,163 | 5,717 | +164% | 0 | 0 | — |
case-09 | fail→pass | 11,765 | 10,761 | -9% | 1 | 1 | 0% | 1,982 | 5,208 | +163% | 0 | 0 | — |
case-10 | fail→pass | 26,645 | 27,271 | +2% | 1 | 1 | 0% | 4,881 | 5,695 | +17% | 0 | 0 | — |
case-11 | fail→pass | 18,903 | 11,948 | -37% | 1 | 1 | 0% | 2,338 | 5,502 | +135% | 0 | 0 | — |
case-12 | fail→pass | 12,967 | 12,386 | -4% | 1 | 1 | 0% | 2,178 | 5,483 | +152% | 0 | 0 | — |
case-13 | fail→pass | 19,458 | 2,253 | -88% | 1 | 1 | 0% | 2,349 | 4,488 | +91% | 0 | 0 | — |
case-14 | fail→pass | 15,928 | 6,089 | -62% | 1 | 1 | 0% | 2,412 | 5,184 | +115% | 0 | 0 | — |
case-15 | fail→pass | 19,050 | 16,107 | -15% | 1 | 1 | 0% | 2,414 | 7,280 | +202% | 0 | 0 | — |
case-16 | fail→pass | 18,247 | 8,834 | -52% | 1 | 1 | 0% | 1,973 | 4,791 | +143% | 0 | 0 | — |
case-17 | fail→pass | 21,536 | 17,389 | -19% | 1 | 1 | 0% | 2,909 | 6,700 | +130% | 0 | 0 | — |
case-18 | fail→pass | 17,817 | 8,839 | -50% | 1 | 1 | 0% | 1,883 | 4,732 | +151% | 0 | 0 | — |
case-19 | fail→pass | 16,691 | 3,667 | -78% | 1 | 1 | 0% | 1,916 | 4,771 | +149% | 0 | 0 | — |
case-20 | fail→pass | 25,935 | 5,904 | -77% | 1 | 1 | 0% | 3,235 | 5,162 | +60% | 0 | 0 | — |
case-21 | fail→pass | 19,770 | 11,470 | -42% | 1 | 1 | 0% | 2,449 | 6,095 | +149% | 0 | 0 | — |
case-22 | fail→pass | 19,476 | 11,887 | -39% | 1 | 1 | 0% | 2,430 | 5,474 | +125% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +86 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.