REUSE
Skills: work you write once¶
A skill is a piece of agent work published under a name, so any plan can call it in one step instead of spelling the steps out again.
This page is for whoever is about to write the same steps into a second plan.
The property that makes a skill safe to depend on: a version is never
rewritten. Creating the same name again makes the next version, so a plan
naming slack.read@1.0.0 runs exactly what it ran the day you wrote it, whatever
anybody creates afterward.
Two plans that both read a Slack channel means those steps
are written twice, and changing your mind means remembering both. Publish
slack.read once and every plan names it.
See what your organization can already call:
What is a skill 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.",
"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.
How does a plan call one?¶
A plan step names the skill and fills its ports:
{
"id": "history",
"skill": "slack.read@1.0.0",
"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.0.0) when you want the plan pinned, which is almost
always. Without a version 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.
How do I make one?¶
1 · Promote a plan you already wrote¶
The common one. You have a plan that works; make it reusable:
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 exists in EKKA until you run ekka skill create.
Check the gate before you create it
A plan names a concrete gate, like enclave8e1d5a6aApi, because it runs on
one machine. A skill is written 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:
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¶
Change the name in the file before you create it, 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 create it:
ekka skill validate slack.read.skill.json # every check create makes, creates nothing
ekka skill create slack.read.skill.json # EKKA assigns the version number
create prints what you got:
✓ Created slack.read@1.0.0
visibility unlisted
steps 2
body sha256 sha256:3b1f8a4c…
Version 1.0.0 is fixed. Creating this name again makes the next version, so a
plan that names slack.read@1.0.0 keeps running exactly this body.
It is yours alone. Only your own plans can call it until you offer it.
That last line is the one to read twice. It is yours: nobody else's plans can call it, and it is not in anybody else's list. When you want the organization to have it, offer it:
That asks. An administrator accepts it with ekka skill approve, or
declines it with a reason you will read. Nothing you write reaches your
colleagues until somebody agrees to it.
publish takes a name and version, not a file
ekka skill publish slack.read.skill.json is refused, and the refusal tells
you to run ekka skill create. The two are genuinely different acts: one
makes a frozen version, the other offers an existing one to more people.
Who picks the version number?¶
EKKA does, from what changed against the latest version of the same name. A new required port or a new permission is a new major, a widening is a minor, and a body that promises the same things is a patch. The reason is printed under the identity, so a number you did not choose is still a number you can check.
Plans are numbered the same way when you create them. The number tells you whether your schedules, grants and callers still fit. It does not tell you the plan still behaves the same. Read the diff for that.
Who can see it, and who can read it?¶
Two separate things, and they answer two different questions.
The rung: who can FIND it and build on it. You do not write this in the file. You reach it by asking, and somebody agrees.
unlisted |
you. Where every skill starts. Your own plans can call it; nobody else's can, and it is in nobody else's list. |
org |
everyone in your organization, once an administrator accepted it. |
public |
every organization on this install, once EKKA accepted it. |
Each step up is a request: ekka skill publish asks, and the person who
decides accepts or declines with a reason.
A skill file must not name a rung
A visibility key in the file is refused whatever its value, and the
refusal (skill_visibility_is_a_ladder) says what to do instead: remove
the key, because a skill is created for you alone, and you offer it with
ekka skill publish <name>@<version> for an administrator to accept. If
the key were merely ignored you would read a success line and believe your
whole organization could use the skill, when nobody had agreed to anything.
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.
How do I see what exists?¶
ekka skill list # every skill you can use
ekka skill list --all # plus colleagues' unlisted skills, with who wrote each
ekka skill list --requested # only what waits for an administrator
ekka skill list --archived # include archived versions
ekka skill show slack.read # its ports, its size, its versions
ekka skill show slack.read@1.0.0 # 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.
How do I withdraw one?¶
A version is never rewritten, so fixing a skill means creating the next version. That leaves the old one still sitting in the list, still offered. Archiving is how you stop offering it.
✓ Archived file.summarize@1.0.0
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:
A version is always required. file.summarize@1.0.0 and file.summarize@2.0.0 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.
Do permissions change when a plan calls a skill?¶
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¶
- Multi-step plans and schedules: where a skill call fits in a larger plan
- Core concepts: agents, plans, gates, grants and receipts
- CLI reference: every
ekka skillcommand