NEXT · MULTI-STEP PLANS AND SCHEDULES
Write multi-step plans, and put them on a schedule¶
You have an agent with a plan. Now wire several gate operations into one plan: read a database, ask a model, write a file, post a message, with every step authorized before it acts and signed after. Then give the plan a schedule so it runs without you.
New here? Start with Build a governed AI application instead; this page assumes you finished it.
A plan is where governance pays off across many steps: read a database, ask a model, write a file, post a message, and every step is gated the same way. You wire the steps, EKKA governs each one.
What you'll do
Set up the CLI, run ekka demo as a zero-authoring governed run, then author
your own plan with ekka plan, grant it what it needs, run it, and verify the
receipts. The by-hand quickstart teaches the deny, grant,
allow, receipt loop this guide builds on, one command at a time.
Before you start¶
You do not need data or API keys of your own to follow this guide: the demo uses a hosted database and a hosted AI model. The installer runs on macOS and Linux, and on Windows inside WSL.
Setup (one time)¶
You run these three steps once per machine. They are the same as the by-hand quickstart, so skip ahead if you have already done them.
1 · Install the ekka CLI¶
This installs a single signed binary, ekka, onto your PATH. That one binary
is both the CLI you type and the Enclave it starts in step 3. Re-running the
install line upgrades in place.
2 · Sign in and create your organization¶
ekka login --email [email protected]
ekka org create jane-labs
Signing in emails you a link, so there is no password to choose; a new address
creates the account. org create takes an organization code of your own
(lowercase letters, digits and hyphens), creates the organization, and makes it
the one your commands act in. Your local config, including EKKA's pinned key,
lands in ~/.ekka/. Already invited to somebody else's organization? Skip
org create: you joined it when you signed in.
3 · Start your Enclave¶
Wait for authenticated; entering run loop, then leave it running. The Enclave is the
worker that carries out each governed step on your side, so your data and keys
never leave your machine. Only short, signed summaries travel back to EKKA.
Confirm what you are signed in as:
This prints your organization, your environment, and your Enclave identity.
Your first governed run, with zero authoring¶
Before you author anything, watch a real governed plan run end to end:
This runs a real governed plan (a Postgres read plus an AI-model call) through the full loop: allow, deny, grant, revoke, every outcome signed. Then verify the audit chain yourself, offline:
It answers with how many records checked out and that nothing has been changed, added or removed since the first one.
What just happened
You ran a governed plan without authoring a single line, and then proved it was governed by re-checking the hash chain and every signature offline, back to the GENESIS entry, without trusting any dashboard.
Author and run your own plan¶
A plan is a small file that wires gate operations into ordered steps. The file
is the whole plan: a plan block at the top carries its name, and a
definition block carries the steps. You create it from the file alone, and
then run the versioned identity it registers.
(You do not have to start from a blank file: ekka plan template <file> --gate
<type> --op <op> writes a complete one-step starter you can grow. Here we write
one by hand to show the shape.)
1 · Create the plan¶
A minimal plan: a File gate round-trip that writes a note and then reads it back. The File gate is built into your Enclave, so this plan needs no external gate connected. The shape is two steps in one operation:
{
"plan": { "code": "myapp.filenote", "name": "File note" },
"definition": {
"schema_version": "ekka.plan.v2",
"operations": [
{
"id": "fileRoundTrip",
"steps": [
{
"id": "writeNote",
"action_ref": "ekka.local.file.write.v1",
"executor_type": "function",
"feature": "file.fs.write",
"inputs": {
"resource": "quickstart-note.txt",
"content": "Hello from my first governed plan."
}
},
{
"id": "readNote",
"action_ref": "ekka.local.file.read.v1",
"executor_type": "function",
"feature": "file.fs.read",
"inputs": { "resource": "quickstart-note.txt" }
}
]
}
]
}
}
Every step also carries a step_input_contract and a step_output_contract
describing its shape, and the operation carries an execution block. Save the
whole file, contracts included, as plan.json:
The full plan
{
"plan": {
"code": "myapp.filenote",
"name": "File note"
},
"definition": {
"schema_version": "ekka.plan.v2",
"run_context": [],
"inputs": {},
"operations": [
{
"id": "fileRoundTrip",
"display_name": "Write a note, then read it back",
"execution": {
"preferred": {
"mode": "async",
"runtime": "node"
},
"allowed": []
},
"steps": [
{
"id": "writeNote",
"action_ref": "ekka.local.file.write.v1",
"executor_type": "function",
"feature": "file.fs.write",
"identity": {
"requires_user_context": false
},
"inputs": {
"resource": "quickstart-note.txt",
"content": "Hello from my first governed plan."
},
"action_input": {},
"step_input_contract": {
"type": "object",
"required": [
"resource",
"content"
],
"properties": {
"resource": {
"type": "string"
},
"content": {
"type": "string"
}
}
},
"step_output_contract": {
"type": "object",
"required": [],
"properties": {
"op": {
"type": "string"
},
"resource": {
"type": "string"
},
"bytes": {
"type": "number"
},
"sha256_b64": {
"type": "string"
}
}
}
},
{
"id": "readNote",
"action_ref": "ekka.local.file.read.v1",
"executor_type": "function",
"feature": "file.fs.read",
"identity": {
"requires_user_context": false
},
"inputs": {
"resource": "quickstart-note.txt"
},
"action_input": {},
"step_input_contract": {
"type": "object",
"required": [
"resource"
],
"properties": {
"resource": {
"type": "string"
}
}
},
"step_output_contract": {
"type": "object",
"required": [],
"properties": {
"op": {
"type": "string"
},
"resource": {
"type": "string"
},
"bytes": {
"type": "number"
},
"sha256_b64": {
"type": "string"
},
"content": {
"type": "string"
}
}
}
}
]
}
]
}
}
Each step names an action_ref (which gate operation to run), its inputs, and
an input/output contract describing its shape. content is the file body, as a
string. A read returns content too, which is what lets a later step use what an
earlier one wrote: bind it as @<operation>.<step>.output.content. To use a different
gate, swap the step: a Postgres read is ekka.gate.postgres.v1, an AI call is an
llm step, a Slack post is ekka.gate.slack.v1 with resource: channels/<name>.
The plan block is the plan's registration: code is the part after ekka.
in its identity, and name is what ekka plan list shows. (It can also carry
an optional version, status, or agent; left out, EKKA defaults them. The
plan runs as your organization's core agent.)
Check the file first, then point ekka plan create at it. validate runs
every check create would (the plan schema, each step's contracts, whether the
name is already taken) while creating nothing, so it is also the command your CI
can run on plan files:
ekka plan create prints the plan's full identity, which looks like:
Copy that identity exactly. You will need all of it to run the plan.
2 · Grant the plan what it needs¶
The File gate plan above needs no Grant: the File gate runs inside your Enclave, which owns the folder it may touch, so you can skip straight to step 3 and run it now. Grants come in as soon as your plan calls an external gate.
A plan is governed like everything else on EKKA: default-deny. Each external gate operation needs a standing Grant first. Add one Grant per operation:
# Let the plan read a Postgres resource
ekka gate grant add --type postgres --instance <alias> --resource customers \
--capability knowledge.postgres.read
# Let the plan call the AI model
ekka gate grant add --type llm --instance <alias> --resource anthropic/sonnet-4 \
--capability llm.infer
You do not have to guess every Grant up front. If a run is denied, the error
prints the exact ekka gate grant add command that would authorize the step. Run
that, then run the plan again.
3 · Run the plan¶
Run the full identity that plan create printed:
ekka plan run "[email protected]"
If your plan declares inputs, pass them with repeated --input flags, for
example --input question="what is the time in London?". (The File example
above takes none.)
4 · Verify the receipts¶
Every step that ran produced a signed Receipt, chained so tampering is
obvious. receipts verify re-checks the whole chain offline.
Run your plan on a schedule¶
You do not have to be at the keyboard for a plan to run. A schedule runs a plan on a cadence, for your organization only.
Schedule the plan you created above:
# Every weekday at 9am, New York time
ekka plan schedule "[email protected]" \
--cron "0 9 * * 1-5" --tz America/New_York
The cadence is a standard 5-field cron expression, an alias (@hourly,
@daily, @weekly, @monthly), or a friendly interval:
ekka plan schedule "[email protected]" --every 1h
--every accepts 1m, 5m, 15m, 30m, 1h, 6h, 12h, and 1d.
Timezones are IANA names (like America/New_York); the default is UTC. If the
plan declares inputs, attach them with the same repeatable --input key=value
flag you use on ekka plan run, and every fire carries them.
You can also create and schedule in one command:
Every scheduled fire is a normal governed run. Grants are enforced the same
way, every step produces a signed Receipt, and ekka receipts verify covers
scheduled runs exactly like the ones you start by hand. There is no separate,
less-governed path for timers.
Check what is scheduled and whether it is firing:
The listing shows each schedule's cron, its active state, its NEXT-RUN, and its LAST-RUN. A populated LAST-RUN means the schedule is firing.
Pause, resume, or remove a schedule by the id the listing shows:
ekka plan unschedule <schedule-id> --pause # keep it, stop it firing
ekka plan unschedule <schedule-id> --resume # start again at the next real slot
ekka plan unschedule <schedule-id> # remove it
A paused schedule never back-fires the runs it missed: resuming realigns it to the next real slot. Downtime works the same way, so missed fires are skipped, not bursted, and an hourly plan will not fire five times to catch up.
Two requirements: scheduling needs an admin role in your organization, and the plan must be active.
Rules the plan surface enforces¶
ekka plan run takes the full identity, not the bare code. Run the whole
thing plan create printed, for example [email protected], not just the
myapp.summary part of it.
Plan inputs are supplied on the run command. Pass each one as
--input key=value, repeating the flag. A missing input is a common cause of a
step failing.
A Slack channel is addressed as channels/<name>. The resource grammar
allows no hyphens in the first path segment, and channel names often contain
hyphens, so the channels/ prefix carries the name:
ekka gate grant add --type slack --instance <alias> \
--resource channels/team-alerts --capability messaging.slack.send
executor_type comes from a fixed list: llm, data_call, function,
diagnostic, or control_plane. File gate steps are not a separate type.
Declare them as "executor_type": "function" and let the action_ref
(ekka.local.file.read.v1, ekka.local.file.write.v1) route the step to the
File gate, exactly as the example above does. A type outside that list is
rejected.
The plan file is strict JSON, and unknown keys are rejected. A plan file has
exactly two top-level keys: plan (the registration: code, name, and
optionally version, status, agent) and definition (the steps). Inside
definition the keys are fixed too: schema_version, run_context,
operations, and optionally resources, inputs, variables,
output_mapping, and chat_output_mode. The same strictness applies inside
operations and steps, so keep authoring notes outside the plan file: JSON has no
comment syntax, and an extra "comment" key is rejected.
Make it reusable¶
The second time you write those steps, stop and publish them instead.
A skill is a body of work published under a name. Take the plan you just wrote and promote one of its operations:
Every @input.<name> in those steps becomes a port the caller fills in. Read
the file, then check and publish it:
Now any plan calls it in one step:
Name the version. A published version is never rewritten, so @1 runs the same
work next year.
Two things to check in the generated file before you publish, and promote tells
you about both:
output_mapis blank. Only you can say which step's output the caller receives, and under what name.- A pinned gate. Your plan named a concrete gate because it runs on your machine. A skill is for whoever has one, so make it a port.
Skills has the whole picture, including how to generate one straight from a connected API and who is allowed to read the steps.
What you just built¶
You wired multiple governed steps into one plan, authorized each with a Grant, ran it under your own Enclave, and verified a signed, offline-checkable receipt chain for the whole thing. The same grammar (deny, grant, allow, receipt) governs every gate a step touches: a database, an AI model, a file, a message.
Next steps¶
- Understand the pieces → Core concepts (gates, grants, the Enclave, receipts, and plans)
- Read a real audit chain → Governance receipts
- Every command →
ekkaCLI reference - Point it at your resources → Add your own API