Writing packs and skills

Last updated October 5, 2026

On this page

Draft for review

A pack is one YAML file, pack.yaml, that describes a part of a company: the records it keeps, the agents that work on them, what those agents may do, the rules that hold them back and the pages people see. Every department in the demo (CRM, Support, Support operations and the rest) is a pack that ships with OrchKernel. You can write your own, have another admin review it, and turn it on like any bundled module.

This page walks through what a pack holds, using the bundled crm and support-ops packs as examples. Then you write a small pack, upload it as a company module, have a second admin approve it, and see its agent and page appear in the demo. What is not possible yet is listed at the end.

This guide describes packs as they work today on the golive branch.

The steps below were run in the demo company with no model configured. In that case the demo's stub planner answers instead of a model: it knows only the walkthrough's asks and answers anything else with a "No model is configured" note (see Models). Steps that need a real model say so.

Packs, modules and skills#

Word What it is
Pack The pack.yaml file: the definition.
Module A pack a company has turned on and configured: bound to its own teams, people and connections. See Modules.
Catalog The list of modules a company can turn on, on the Catalog tab of Modules: the bundled ones and the company's own.
Company module A pack your company wrote, uploaded, had reviewed by a second admin and added to the catalog.
Skill One thing an agent knows how to do, with its permissions, its playbook and its eval cases. Skills live inside a pack.
Playbook A skill's instructions in fixed sections (goal, steps, guardrails and so on). See Agents.
Eval A test case for a skill: a request and the plan the agent must make for it. See Changing a skill or playbook.
Proposal A change waiting in the change pipeline under Governance > Changes. An uploaded pack becomes one.
Promote The last step of the change pipeline: the reviewed change takes effect. For an uploaded pack, it adds the module to the catalog.

The usual path is: write a pack, upload it, a second admin reviews and promotes it into the catalog, then an admin enables it. A pack can also be installed directly without becoming a module; see Installing a pack directly.

What a pack holds#

The bundled packs are in packs/<id>/pack.yaml in the source tree. Every section except id and name is optional.

Section What it holds Example in the bundled packs
id, name, version, description Who the pack is. version starts at 1. id: crm, name: CRM
requires Other catalog ids this pack builds on. It cannot be enabled until they are. support-ops has requires: [support]
collections The kinds of records it keeps, with fields, who may read and write them, and how sensitive they are. CRM's leads and contacts
tools Calls to outside systems, each a contract with argument and result schemas. CRM's gmail.send, Support operations' support-ops.billing.refund
skills What agents can do: instructions, a playbook, permissions and evals. CRM's lead-management
rules Policy that holds or refuses actions. crm-agent-email-needs-rep
agents The agents, each with its skills, team, budget and trust. Sales Assistant
triggers Schedules, webhooks and record changes that start work. crm-morning-followups, weekdays at 08:00
views The pages in the sidebar: lists, record pages and dashboards. CRM's Leads, Lead and Pipeline pages
thread_templates Thread templates offered under New thread. Product operations' Release

Collections#

A collection is a kind of record, like a table. Each one has a schema with a base type and fields, a policy and a data_class.

yaml
collections:
  - name: leads
    data_class: { label: pii.customer, sensitivity: restricted }
    schema:
      base: organization
      fields:
        - { name: name, type: string, required: true, synonyms: [company, account] }
        - { name: stage, type: enum, values: [new, contacted, qualified, negotiation, won, lost] }
        - { name: owner, type: actor_ref, description: Rep who owns the lead }
        - { name: value, type: money }
    policy:
      read_teams: ["team:sales"]
      write_teams: ["team:sales"]
      read_rows: { rule: owner_or_role, owner_field: owner, roles: [sales_lead, admin] }
      write_rows: { rule: owner_only, owner_field: owner }
    semantics:
      description: Sales leads and where each one is in the pipeline
      example_questions: ["what are my follow ups today"]
Setting Values
base person, organization, work_item, event, document, decision, place, money, or thing
Field type string, text, int, float, bool, date, date_time, money, enum (with values), ref (with collection), refs, actor_ref, list (with of), json
read_rows, write_rows any; owner_only (with owner_field); owner_or_role (with owner_field and roles); owner_or_team_field
sensitivity public, internal, confidential, restricted

synonyms, description and semantics are what agents read to understand the collection, so write them in the words people use. The team names in read_teams and write_teams are the pack's own; when the pack is enabled, the admin binds each to a real team. See The brain for how row rules play out.

A skill that reads a collection must also list the collection's data class label under data_classes in its permissions when the collection is confidential or restricted (CRM's skill lists pii.customer to read leads). Without it the read is refused.

Tools and their contracts#

OrchKernel ships no connectors of its own. A pack declares each tool as a contract against a named server: the tool name on that server, the arguments it takes and the result it returns. Any server that meets the contract works, which is how the demo's local fakes stand in for Gmail or Stripe.

yaml
tools:
  - id: support-ops.billing.refund
    name: Issue a refund or credit
    provider: { type: mcp, server: billing, tool: refund }
    risk_tier: critical
    data_class: { label: financial, sensitivity: confidential }
    external_side_effect: true
    rate_limit_per_min: 5
    arg_rules:
      - { rule: in_read_results, path: invoice }
    args_schema:
      type: object
      properties:
        invoice: { type: string }
        amount: { type: number, minimum: 0 }
        reason: { type: string }
      required: [invoice, amount, reason]
      additionalProperties: false
    result_schema:
      type: object
      properties: { id: { type: string } }
      required: [id]

Name new tools <pack>.<server>.<tool> so two modules on one server never collide. arg_rules are checked before anyone is asked to approve a call:

Rule The argument must
in_read_results Appear in a record or tool result the run has read (an invoice id, an email address).
company_domain Be an address on one of the company's domains.
listed_in_connection Be listed under key in the module's connection settings.
not_repeated Not repeat an earlier call's value (one refund per invoice). message sets the refusal for a call that went through, pending for one still waiting for approval.

When a tool has more than one of the first three rules on the same argument, passing any one is enough. not_repeated must pass as well.

How a server is bound to a tool is in Connections.

Skills and playbooks#

A skill says what an agent may touch and how it should work.

Field What it does
instructions Plain instructions, used when there is no playbook.
playbook The structured instructions: goal, when, inputs, steps, guardrails, approval_points, outputs, measures and run.
tools The pack's tools this skill may call.
permissions read_collections, write_collections, data_classes, memory_write_scopes, may_delegate. A skill can name collections another pack owns; that pack's row rules still apply.
risk_tier low, medium, high or critical.
handles Words that route a request to this skill.
code { type: declarative }: a model plans each run. Or a sandboxed skill, below.
evals Test cases for the skill, below.

CRM's lead-management skill is a good model: one step per thing the rep asks for, guardrails such as "Never change a stage to lost without an explicit instruction from the rep", and an approval point for every email. The playbook's sections, and how single and loop runs differ, are explained in Agents.

Evals#

An eval case gives the skill a request and says which operations its plan should start with. The eval gate runs them whenever the skill or its playbook is changed; see Changing a skill or playbook.

yaml
evals:
  - name: follow-ups-query-is-scoped-by-date
    input: { ask: "what are my follow ups today" }
    expect: { ops: [ { op: query, collection: leads } ] }

By default the expected operations must be the first ones, in order. Add match: contains when they may come anywhere in the first round. Give every skill at least one eval: a later change to its playbook is refused without one. A model must answer for an eval to pass, so with the demo's stub planner evals show as "not run".

Rules#

Rules are the pack's policy. In a company module they can only tighten.

yaml
rules:
  - id: crm-agent-email-needs-rep
    description: Agents never send email without the rep approving the draft
    effect: require_approval
    approver: { approver: acting_for }
    matches: { tools: [gmail.send], agents_only: true }
    priority: 10

effect is deny, require_approval or allow. approver is acting_for (the person the agent works for), owner, manager, role (with role) or actor (with id). matches can name tools, actors, collections, actions (such as brain.upsert(leads)), agents_only, min_risk and an amount cap (amount_over, in cents). See Policy for how rules are weighed.

Agents#

yaml
agents:
  - id: sales-assistant
    name: Sales Assistant
    kind: agent
    role: sales_assistant
    description: Works each rep's pipeline on their behalf
    skills: [lead-management]
    tools: [gmail.send, calendar.create]
    budget: { limit: 5000 }
    concurrency: 2
    memory_scopes: [team]
    team: team:sales
    trust_tier: standard

budget.limit is in cents (5000 shows as $50.00). In a module, who the agent acts for comes from the Enable sheet, not from the pack. In a pack installed directly, acts_for: [alice] sets it.

Triggers#

kind Starts work when Extra field
cron A schedule comes round schedule: every:30m, every:2h, hourly, daily@08:00, weekdays@08:00
webhook An outside system posts an event source, named <pack>.<source>
data_change A record in a collection is written collection

Each trigger has an id, a name and a template for the task it creates: title, owner (required), assignment, priority, skill_hint, labels and sla_hours. Support operations' churn watcher has a cron sweep and a data_change trigger on tickets; Founder's hiring coordinator has a webhook trigger with source founder.careers. In the demo, module triggers start off; see Modules.

Views#

A view is a page. kind is list, detail (one record) or dashboard. section and order place it in the sidebar of a pack installed directly; a module's pages go in the section the Enable sheet sets. The spec is a list of blocks:

Block Shows
header Title, subtitle, tags, facts, and buttons such as ask: { agent: sales-assistant, text: "..." }
table, list Rows from a query, optionally linking to a detail view
fields A record's fields
refs Everything that refers to a record: messages, runs, approvals, decisions
tiles Numbers from aggregates (count, sum, avg)
chart Bar or line charts over a grouped aggregate
text, actions Text and buttons

A view only shows to people who can read its collection. A detail view's blocks can use $record.<field>, and filters can use $today. CRM's crm.pipeline dashboard uses most of them.

Thread templates#

yaml
thread_templates:
  - name: release
    label: Release
    description: One release, from drafted notes to publication
    slots:
      - { name: pm, role: owner, kind: person, label: Product manager }
      - { name: eng_lead, role: approver, kind: person, label: Engineering lead }
      - { name: release_notes_writer, role: contributor, kind: agent, default: release-notes-writer, label: Release-notes writer }
    artifacts: [[release_notes, release_notes]]
    requires_approval_to_close: true
    agent_chatter_cap: 3

What each part means is in Threads. A module's template appears under New thread while the module is enabled.

The JSON Schemas#

ok schemas prints the JSON Schema of every definition type as one JSON object, and GET /api/schemas returns the same. Point your editor's YAML support at the relevant part to get completion and checks while you write.

sh
ok schemas > schemas.json

Run it in a scratch folder: it also creates an empty ./.orchkernel/state.db where you run it (a bug, see Known problems).

Key Covers
collection A collection: schema, policy, semantics, data class
skill A skill, its permissions, code, playbook and eval cases
playbook A playbook on its own
test_case One eval case
tool A tool, its provider and argument rules
rule A rule, its match and approver
actor People, teams and agents
others Tasks, threads, events, delegations, policy bundles and the gateway's types

There is no schema for the pack file as a whole, for views, triggers or thread templates. The upload checks those and lists every problem it finds.

Write a small pack#

This pack adds an Equipment requests collection for the Engineering team, an agent that logs requests, a rule and a page. Save it as pack.yaml in a folder of its own, for example equipment/pack.yaml.

yaml
id: equipment
name: Equipment requests
version: 1
description: Laptops, monitors and other kit people ask for, with an agent that logs and sorts the requests.

collections:
  - name: equipment_requests
    storage: native
    data_class: { label: internal, sensitivity: internal }
    schema:
      base: work_item
      fields:
        - { name: item, type: string, required: true, synonyms: [kit, device], description: What is being asked for }
        - { name: requester, type: actor_ref, description: Who asked }
        - { name: status, type: enum, values: [new, approved, ordered, delivered, declined] }
        - { name: cost, type: money, synonyms: [price] }
        - { name: needed_by, type: date }
    policy:
      read_teams: ["team:eng"]
      write_teams: ["team:eng"]
      read_rows: { rule: any }
      write_rows: { rule: any }
    semantics:
      description: Equipment people have asked for and where each request stands
      synonyms: [kit requests, hardware]
      example_questions: ["what equipment is waiting", "log a monitor for eli"]
      searchable: true

skills:
  - id: equipment-intake
    name: Equipment intake
    version: 1
    description: Log equipment requests and list the ones still waiting.
    instructions: |
      Log each equipment request in equipment_requests with status new.
      List open requests when asked. Never set a request to approved.
    permissions:
      read_collections: [equipment_requests]
      write_collections: [equipment_requests]
      memory_write_scopes: [team]
    risk_tier: low
    handles: [equipment, laptop, monitor, kit]
    code: { type: declarative }
    playbook:
      goal: Every equipment request is logged once, with who asked and by when, so nothing is lost in chat.
      when:
        prose: Someone asks for kit or asks what is waiting.
        triggers: [mention]
      inputs:
        - The open equipment_requests
        - What the person asked, in their words
      steps:
        - name: Check
          do: Query equipment_requests for the same item and requester so a request is not logged twice.
        - name: Log
          do: Upsert the request with status new, the requester and needed_by.
        - name: Report
          do: Output the request, or the list of open requests when that was the question.
      guardrails:
        - Never set a request to approved, ordered or declined; a person does that.
      approval_points:
        - Approving a purchase is a person's decision, made on the record.
      outputs:
        - A logged request or the list of open requests
      measures:
        - name: waiting
          description: Requests not yet approved or declined
          source: { collection: equipment_requests, filter: { op: eq, field: status, value: new }, metrics: [{ op: count, as: n }] }
          value: n
      run: { mode: single }
    evals:
      - name: a-request-is-checked-first
        input: { ask: "Eli needs a second monitor by Friday" }
        expect: { ops: [ { op: query, collection: equipment_requests } ] }

rules:
  - id: equipment-helper-never-deletes
    description: The equipment helper never deletes a request
    effect: deny
    matches: { actors: [equipment-helper], actions: ["brain.delete"] }
    priority: 10

agents:
  - id: equipment-helper
    name: Equipment helper
    kind: agent
    role: equipment_helper
    description: Logs equipment requests and lists what is waiting
    skills: [equipment-intake]
    budget: { limit: 1000 }
    concurrency: 1
    memory_scopes: [team]
    team: team:eng
    trust_tier: sandboxed

views:
  - id: equipment.requests
    name: Equipment requests
    kind: list
    collection: equipment_requests
    section: Engineering
    order: 50
    icon: list
    spec:
      blocks:
        - type: header
          title: Equipment requests
          subtitle: What people have asked for, oldest need first
          actions:
            - { label: Ask Equipment helper, ask: { agent: equipment-helper } }
        - type: table
          source: { collection: equipment_requests, order: { field: needed_by, ascending: true } }
          columns: [item, requester, status, cost, needed_by]

Two things a first pack often gets wrong:

  • A rule in a company module must name one of the module's own agents, tools or collections in matches. A rule that only names an action such as brain.upsert(equipment_requests) is refused with "match names none of the module's agents, tools or collections".
  • The agent's team must be a team the pack's collections or rules name (team:eng here). At enable time an admin binds it to a real team.

Turn it into a company module#

A company module needs two human admins: one uploads it, another reviews and promotes it. The demo has only Ada, so add a second admin first.

Add a second admin in the demo#

While ok demo serve runs, add people through the API. A command-line change to the state file is overwritten by the running server; see Setting up a company.

  1. Copy Ada's token from the list ok demo serve printed, and the port from its Workspace line (8080 unless you started it with --bind). In a terminal:

    sh
    ADA=orchk_...        # Ada's token
    OK=http://127.0.0.1:8080
    
  2. Add Dana as an admin and give her a token:

    sh
    curl -X POST $OK/api/actors \
      -H "Authorization: Bearer $ADA" -H 'content-type: application/json' \
      -d '{"actor":{"id":"dana","name":"Dana Ruiz","kind":"human","role":"admin"}}'
    curl -X POST $OK/api/tokens \
      -H "Authorization: Bearer $ADA" -H 'content-type: application/json' \
      -d '{"actor":"dana","label":"docs"}'
    

    The first call answers with Dana's record ("role":"admin"). The second answers with her token in "token":"orchk_...", valid 30 days. Keep it for step 3 below.

Upload, review, enable#

  1. Sign in as ada. Open Modules and choose Upload a module. The sheet says "A pack.yaml of at most 256 KB. Its skills must be declarative, its ids new, and its rules may only tighten. Trust and ownership in the file are ignored." Choose your pack.yaml with the file picker, or paste the YAML into the box below it, then choose Upload for review.

    The sheet now says "Filed module equipment v1 (uploaded): 1 collections, 1 agents, 0 tools as proposal . Another admin reviews the card below and promotes it from Governance › Changes; enabling it is a separate step." Below it is the Review card: the collection and its data class ("equipment_requests: internal, internal"), the tools ("none"), the skill's reads and writes, the agent and its team, the triggers ("none"), the rule ("deny on agents equipment-helper; actions brain.delete"), the page and the section it asks for ("in Engineering"), what it requires ("nothing"), and the file's sha256 fingerprint, so the reviewer can tell exactly which file they approved.

  2. Still as Ada, open Governance, tab Changes. The card module equipment v1 (uploaded), Proposed, "proposed by Ada Park", shows Approve and Reject. Ada cannot approve her own upload: choose Approve, then Approve in the sheet, and the sheet shows "policy denied change.review: ada proposed this module; another admin reviews and promotes it". Choose Cancel.

  3. In a private browser window, open the workspace and sign in with Dana's token. Open Governance, tab Changes. The same card is there, with a content section that opens to the full pack. Choose Approve. The sheet "Approve this change" asks for an optional Note; type one if you like and choose Approve. Within a few seconds the card reads Reviewed, shows "Dana Ruiz approved: ", and offers Promote.

  4. Choose Promote. The card reads Promoted. The module is now in the catalog. Nothing is turned on yet.

  5. Back as ada, open Modules. The Catalog tab shows Equipment requests with the badges Uploaded and Reviewed by your company, "1 collection · 1 agent · 1 page", and Enable.

  6. Choose Enable. The sheet Enable Equipment requests has four parts:

    Part What you see
    Teams A drop-down "Pick a team…" for team:eng. A company module's teams are not picked for you.
    Agents "Equipment helper · acts for nobody · $10.00 · sandboxed · on"
    Connections "This module calls no tool servers."
    Pages A tick box for Equipment requests and a box for its sidebar section, filled in with the module's name.

    Enable is greyed out and the sheet says "Bind team:eng to enable." Under Teams, pick Engineering (team:eng). The sheet then says "equipment-helper joins Engineering and can read dependencies, incidents, pull_requests, equipment_requests", and the agent line changes to "Equipment helper · acts for Engineering · $10.00 · sandboxed · on".

  7. Choose Enable. The module page opens: state Enabled, health Ok, version v1, set up by Ada Park.

See the agent and the page#

  1. Sign in as eli (or erin), who are on Engineering. The sidebar has a new section, Equipment requests, with the page Equipment requests. The section comes from the Enable sheet's Pages part; the pack's section: Engineering is not used for a module.

    Ada does not see the page: she is not on Engineering, and a page shows only to people who can read its collection.

  2. Open the page. It shows the header, the subtitle, the Ask Equipment helper button and an empty table ("No rows.").

  3. Choose Ask Equipment helper. The page New conversation with Equipment helper opens. Type "I need a second monitor by Friday" and choose Ask agent.

    With the stub planner, the agent answers "No model is configured, so the demo's stub planner answered. ..." and the run shows Done. With a real model the agent plans from the playbook: it queries the collection, then logs the request.

  4. To see the agent write without a model, add ?dev=1 to the end of the conversation's address and reload. Choose Explicit plan next to the agent's name; a second box appears. Type a message in the message box ("Log a second monitor for me"), paste this plan into the second box, and choose Ask agent:

    json
    {"ops":[{"op":"query","collection":"equipment_requests","bind":"existing"},
            {"op":"upsert","collection":"equipment_requests","fields":{"item":"Second monitor","requester":"eli","status":"new","needed_by":"2026-10-09"}},
            {"op":"output","value":"Logged: second monitor for Eli"}]}
    

    The agent answers "Logged: second monitor for Eli" and the run shows Done. Open the page again: the table lists "Second monitor", "Eli Novak", "New", no cost, "Fri, Oct 9". At phone width the table scrolls sideways inside its card.

  5. Open Directory, click Equipment helper and choose Open agent page. The header shows the module Equipment requests and Active. The Overview lists trust Sandboxed, the skill "Equipment intake v1", acts for Eli Novak and Erin Walsh, "$0.00 of $10.00" spent, and the playbook's goal. Acts under shows one delegation per person, "Via Equipment requests". The Playbook tab shows the sections. See Agents.

From here the module behaves like a bundled one: pause it, change who the agent acts for, or roll back its record on its page. See Modules.

Describe a module instead#

Describe a module on the Modules page asks the Architect (a built-in agent) to draft a pack from a sentence and file it as a proposal; any human admin, including you, may review an Architect draft. It needs a real model. In the demo it fails today: after Draft it the sheet says "The Architect did not draft a module: no native skill registered as architect", with or without a model (a bug, see Known problems).

When an upload is refused#

The upload lists every problem at once. The ones you are most likely to see:

Message Fix
version_not_higher: equipment v1 is not higher than the catalog's v1 Raise version to upload a new version.
skill ...: wasm code is refused; company modules run declarative skills only Use code: { type: declarative }.
rule ...: match names none of the module's agents, tools or collections Name one of the pack's own objects in matches.
rule ...: approver role ...: module rules may name only the admin role, the owner or the manager Use role: admin, owner or manager.
agent ... acts for ...; delegations come only from the modules record Remove acts_for; choose who it acts for on the Enable sheet.
agent ... is on team ..., which none of the module's collections or rules name Use a team the collections or rules name.
collection ... already exists (or tool, skill, rule, actor, trigger, thread template, page) Choose ids no other module uses. Prefix them with the pack id.
trigger ...: webhook source ... is not namespaced <pack>.<source> Name the source <pack>.<source>.
yaml: ...: missing field ... at line ... column ... The file does not match the pack format; the message names the field and line.
too_large: the module is ... KB of YAML; the limit is 256 KB Keep the YAML under 256 KB.

What the kernel changes on upload, whatever the file says:

In the file Becomes
trust Verified ("Reviewed by your company") once promoted
Every tool An external side effect, at least high risk. On the review card the reviewer can pick a lower risk and choose Lower before promoting.
Every agent's trust_tier sandboxed; an admin changes it on the Enable sheet or the module's Agents tab
Each collection's owner The pack

A collection may be restricted; the card marks it so the reviewer sees it.

Installing a pack directly#

An admin can install a pack without making it a module. This skips the second admin's review, so use it for development and for sandboxed skills, and use a company module for anything people depend on.

Company module Direct install
Reviewed by a second admin Yes No: any human admin installs it
Listed on the Modules page, pausable, configurable Yes No
Who an agent acts for Chosen on the Enable sheet The agent's acts_for in the file
Sandboxed (WASM) skills Refused Allowed
Pack trust Verified As written; unverified (the default) may not declare a confidential or restricted collection
A collection id already taken Refused Depends on --on-collision

While the server runs, install over the API: POST /api/packs with { "path": "<folder holding pack.yaml, on the server's machine>" } or { "yaml": "<pack.yaml text>" }. It answers {"pack":"equipment","collections":[...],"skills":1,"agents":1}.

From the command line, with the server stopped (the CLI writes the state file directly):

sh
ok --as ada pack install path/to/equipment    # the folder holding pack.yaml
ok --as ada pack list                         # id, name and version of every installed pack

Use your company's admin in --as (ada in the demo), and --state for a state file other than ./.orchkernel/state.db (the demo's is orchkernel-demo/state.db). It prints installed pack equipment (1 collections, 1 skills, 1 agents); pack list prints lines such as equipment Equipment requests v1.

--on-collision is fail (the default: refused with "collection ... already exists (owned by ...)" if another pack or the company owns the collection), merge (adds the new fields), replace, or namespace:<prefix>. Reinstalling the same pack replaces its own collections. An unverified pack with a restricted collection is refused with "pack invalid: unverified pack declares collection ... with data class ...".

Sandboxed WASM skills#

A skill can run code instead of asking a model to plan. The code is compiled to WebAssembly (WASM) and runs inside the kernel with a fuel limit, a memory limit and a cap on calls. It reaches the world only through one host function, and every call it makes goes through the same policy as an agent's plan: the skill's permissions, delegations, rules and approvals. It needs no model, so it works the same in the demo.

yaml
skills:
  - id: hello-wasm
    name: Hello in WASM
    version: 1
    description: Says hello and counts the leads it can read.
    permissions:
      read_collections: [leads]
      data_classes: [pii.customer]
    risk_tier: low
    handles: [hello]
    code: { type: wasm, path: hello.wasm }   # relative to the pack folder
    evals:
      - name: greets-by-name
        input: { name: "Ada", fixtures: { brain.query: [ { id: "l1" }, { id: "l2" } ] } }
        expect: { greeting: "hello Ada", existing: 2 }

code can also be { type: wat, source: "..." }, WebAssembly text inline, for tests and tiny skills. A missing .wasm file is refused at install ("skill hello-wasm references missing wasm ...").

Try one in the demo#

  1. Build the example in examples/skills/hello-wasm with cargo build --release --target wasm32-unknown-unknown (this needs Rust with the wasm32-unknown-unknown target). The file is target/wasm32-unknown-unknown/release/hello_wasm.wasm.

  2. Make a folder hello, copy the file into it as hello.wasm, and save this as hello/pack.yaml: the skill above plus an agent that acts for Alice.

    yaml
    id: hello
    name: Hello
    version: 1
    
    skills:
      # the hello-wasm skill above, indented under skills:
    
    agents:
      - id: hello-bot
        name: Hello bot
        kind: agent
        role: hello_bot
        skills: [hello-wasm]
        acts_for: [alice]
        team: team:sales
    
  3. Install it with Ada's token, giving the folder's full path:

    sh
    curl -X POST $OK/api/packs -H "Authorization: Bearer $ADA" \
      -H 'content-type: application/json' -d '{"path":"/full/path/to/hello"}'
    

    It answers {"agents":1,"collections":[],"pack":"hello","skills":1}.

  4. Ask the agent as Alice, with her token in $ALICE:

    sh
    curl -X POST $OK/api/ask -H "Authorization: Bearer $ALICE" \
      -H 'content-type: application/json' -d '{"agent":"hello-bot","text":"hello"}'
    

    The run finishes "outcome":"done" with the output {"existing":4,"greeting":"hello stranger"}. Its steps show the skill hello-wasm selected, a brain_query, and a note "looking up leads" (the guest's log call). existing is how many leads the query returned: Alice's four. The guest greets "stranger" because it reads a top-level name, and a real run's input has none; see the ABI below.

    Leave out data_classes: [pii.customer] and the query is refused, because leads are restricted: the run still finishes, with "existing":0, and nothing in the run says the read was refused.

Evals for a sandboxed skill run the code against a stand-in host: the case's whole input is passed to the guest, brain.query and directory.find answer from input.fixtures, and any call that would change something is refused. The case above passes when the skill is proposed as a change.

Not possible yet#

  • Upgrading an enabled company module to a newer version. Uploading v2 and promoting it works, and the catalog card shows Newer version available, but the module record's version cannot be changed ("equipment version 2 is not the installed version 1") and there is no Upgrade button.
  • Sandboxed (WASM) skills in a company module.
  • A JSON Schema for the whole pack file, or for views, triggers and thread templates.
  • Running a module's skill evals when it is uploaded. The proposal shows no eval results, and a skill with a playbook and no evals is accepted.
  • Retiring a company module that is enabled. It is refused with "catalog_in_use: a modules record uses equipment; pause the module instead".

Known problems on this page#

  • Describe a module always fails in the demo. The Architect's skill is registered only when the company is first created, so after any restart (every ok demo serve is one) Draft it fails with "no native skill registered as architect".
  • ok schemas creates a state file. It writes ./.orchkernel/state.db in the current folder, although it only prints schemas.
  • Plural slips in the upload note. "1 collections, 1 agents, 0 tools", in the UI and in ok changes list.
  • A refused read in a sandboxed skill leaves no trace. The guest gets an error and the run shows no step for it.
  • The unverified-pack refusal names the wrong thing. It gives the data class label ("with data class internal") rather than the sensitivity that caused it.
  • The hello-wasm example always greets "stranger" in a run. Only an eval, which passes the case's input as it is, can give it a name.

For developers#

API#

All under /api, with Authorization: Bearer <token>. Every catalog route is for human admins.

Method and path Body Notes
GET /schemas The same JSON as ok schemas.
GET /packs Installed packs: id, name, version.
POST /packs { path?, yaml?, on_collision? } Direct install. Human admins only. Returns { pack, collections, skills, agents }.
GET /catalog Every module: id, version, versions, source (bundled, uploaded, architect), trust, state, installed, newer_version_available, summary.
POST /catalog/uploads { yaml } Files a module proposal. Returns { proposal, state, summary, card }. 413 too_large, 422 invalid with problems, 409 version_not_higher.
POST /catalog/drafts { text } Asks the Architect to draft a module.
GET /catalog/proposals/:id/card The review card.
POST /catalog/proposals/:id/lower { tool, risk_tier, external_side_effect } Lower a tool's risk on the card before promotion; never above high.
DELETE /catalog/:id Retire an unused company module. 409 catalog_in_use while enabled.
POST /changes/:id/review { approve, note? } A second admin reviews. The proposer is refused with "policy denied change.review".
POST /changes/:id/reject { note? } Reject the proposal.
POST /changes/:id/promote Adds the reviewed module to the catalog.
POST /modules/:pack/enable { record: { teams: { "team:eng": "team:eng" }, ... } } Enable; see Modules.
GET /thread-templates Includes enabled modules' templates.

CLI#

sh
ok schemas                          # every definition's JSON Schema
ok pack install <dir> [--on-collision fail|merge|replace|namespace:<prefix>]
ok pack list
ok catalog list                     # id, version, source, trust, state, name
ok catalog upload <pack.yaml>       # file a module proposal
ok catalog retire <id>
ok changes list | review | promote  # the second admin's review
ok module enable <pack> --team team:eng=team:eng

These work on the state file; stop the server first or use the API.

The WASM ABI#

The guest exports:

memory
alloc(len: i32) -> ptr: i32
run(input_ptr: i32, input_len: i32) -> packed: i64    // (ptr << 32) | len

run receives JSON. In a run it is {"task": {"id", "title", "description", "inputs"}, "me": "<actor acted for>"}; in an eval it is the eval case's input. It returns JSON, which becomes the run's output. Returning {"await_approval": "<id>"}, or an error starting with approval_required:, parks the run; once approved the guest runs again and gets the approved result for the same call.

The guest imports one function from module ok:

call(op_ptr, op_len, arg_ptr, arg_len) -> packed: i64

It answers {"ok": value} or {"err": "message"}.

op Argument
brain.query { collection, query: { filter, order, limit } }
brain.aggregate { collection, filter, group_by, metrics, limit }
brain.upsert { collection, id?, fields }
brain.remember { scope, content, topics, confidence }
tool.call { tool, args }
llm.complete { system, prompt, json }
directory.find { skills: [...] }
thread.post { body }
log Any string; becomes a note on the run

Limits: 50 million fuel, 16 MiB of memory and 200 host calls per run.

Where it lives#

What Where
The bundled packs packs/*/pack.yaml
Pack loading and checks crates/ok-skills/src/pack.rs
The sandbox crates/ok-skills/src/sandbox.rs, host calls in crates/ok-kernel/src/runner.rs
Company module rules and the review card crates/ok-kernel/src/modules/catalog.rs
The JSON Schemas ok_core::json_schemas in crates/ok-core/src/lib.rs
The WASM example examples/skills/hello-wasm