Skip to content

REUSE

Skills: work you write once

A Skill is a piece of work somebody already wrote, published under a name, so a plan can call it instead of spelling the steps out again.

If you have written two plans that both read a Slack channel, you have written those steps twice. Change your mind about them and you have to remember both. A skill is the fix: publish slack.read once, and every plan names it.

The one idea to hold onto

A published version is never rewritten. Publishing the same name again creates the next version. A plan that names slack.read@1 runs exactly what it ran the day you wrote it, whatever anybody publishes later.


What a skill is made of

Two things: the steps, and the ports.

The steps are ordinary plan steps. The ports are the small contract around them: what the skill takes in, and what it hands back:

{
  "name": "slack.read",
  "display_name": "Read recent messages in a Slack channel",
  "description": "Reads a channel's recent history. Read-only: it can never post.",
  "visibility": "private",
  "body": {
    "ports": {
      "inputs": {
        "type": "object",
        "required": ["gate", "channel"],
        "properties": {
          "gate": { "type": "string", "description": "your api gate, from `ekka gate list`" },
          "channel": { "type": "string", "description": "the channel id, like C0123ABCD" }
        }
      },
      "input_map": {
        "gate": "call.target",
        "channel": "call.inputs.channel"
      },
      "output_map": {
        "messages": "call.output.rows"
      }
    },
    "steps": [ { "id": "call", "action_ref": "ekka.gate.api.v1", "…": "…" } ]
  }
}

input_map says where each port lands inside the steps. output_map says which step output the caller receives, and under what name. Between them, a caller never has to know what is inside.

@self., not @op.

Inside a skill body, one step reads an earlier one with @self.<step>.output.<field>. A skill cannot name the operation that will call it, because it does not know who that will be. If you are editing a body and see @self., leave it alone.

Calling one from a plan

A plan step names the skill and fills its ports:

{
  "id": "history",
  "skill": "slack.read@1",
  "display_name": "Read recent messages",
  "inputs": {
    "gate": "enclave8e1d5a6aApi",
    "channel": "@input.channel"
  }
}

That is the whole step. No action ref, no contracts, no request shape. The skill carries all of it.

Name the version (slack.read@1) when you want the plan pinned, which is almost always. Without @1 you get the newest at the moment the plan is created, and plans are frozen when they are created, so it will not drift afterwards either.

Four ways to make one

1 · Promote a plan you already wrote

The common one. You have a plan that works; make it reusable:

ekka skill promote my-plan.json --name slack.read --out slack.read.skill.json

It copies the steps exactly as you wrote them, and turns every @input.<name> into a port. If your plan has more than one operation, name the one you mean with --op <id>.

It writes a file. Nothing is published until you say so.

Check the gate before you publish

A plan names a concrete gate, like enclave8e1d5a6aApi, because it runs on one machine. A skill is published for whoever has one. promote tells you when it copied a pinned gate, and the edit is small: set target to "placeholder" and add "gate": "<step>.target" to input_map.

2 · Generate one from a connected API

If the work is one call to an API you have cataloged:

ekka skill from-api slack --call channelHistory

The generated step names the operation rather than spelling out its URL, so the address stays where it is maintained (on the catalog row) and cannot go stale in your skill.

3 · Copy somebody else's

ekka skill fork file.summarize@2 --out my-summarize.skill.json

Change the name in the file before publishing, or you will be adding a version to the skill you copied from.

4 · Write the file by hand

Everything above produces the same kind of file. You can also just write one.

Whichever route you took, check it and publish it:

ekka skill test slack.read.skill.json      # every check publish makes, creates nothing
ekka skill publish slack.read.skill.json   # EKKA assigns the version

Who can see it, and who can read it

Two separate settings, and they answer two different questions.

visibility: who can FIND it.

private your organization only. The default.
public anyone on EKKA can find it.

openness: who can READ the steps inside.

open the body is served, and ekka skill fork will copy it.
source_available the body is served so you can read it, and fork refuses.
closed the steps are never served. The ports, the permissions it needs, and its digest stay public.

A closed skill still runs perfectly well: the Enclave receives the body it needs to execute. What closed withholds is the body from a reader.

What the label always tells you

Openness is always public, whatever it is set to. You can always see that a skill is closed before you decide to depend on it, along with its ports and the permissions it asks for. You are never asked to call something whose requirements are hidden.

Reading what is there

ekka skill list                 # every skill this organization can use
ekka skill list --all           # including archived ones
ekka skill show slack.read      # its ports, its size, its versions
ekka skill show slack.read@1    # an older version

show tells you what it takes, what it hands back, how many steps it has, and the digest of the body, enough to decide whether to depend on it without reading the steps.

Withdrawing one

A published version is never rewritten, so fixing a skill means publishing the next version. That leaves the old one still sitting in the list, still offered. Archiving is how you stop offering it.

ekka skill archive file.summarize@1
✓ Archived file.summarize@1

  It is out of `ekka skill list` and the next plan naming it will be refused.

Nothing that already runs stops. That is worth being precise about, because "archive" in other tools often means the thing breaks. When a plan is created, the skill's steps are copied into it. The plan does not look the skill up again at run time, so a schedule firing tonight runs exactly what it ran last night.

What archiving changes is the future: the skill leaves ekka skill list, and the next plan that names it is refused when you try to create it.

Nothing is deleted, and you can undo it:

ekka skill list --all                  # including the archived ones
ekka skill unarchive file.summarize@1

A version is always required. file.summarize@1 and file.summarize@2 are different bodies and a plan pins one of them, so the name on its own does not say which to withdraw.

You can only archive skills your own organization published. A skill someone else made public stays theirs.

Permissions do not change

A skill is a body of work, not an authority. When a plan calls one, the steps run as the agent that owns the plan, and every gate call inside still needs a Grant.

Publishing a skill that calls Slack does not give anybody Slack. They still need the grant, on their own connection, in their own organization. skill from-api prints which grant its output will need, for exactly this reason.

What's next