RECIPE · 20 MINUTES
The financial analyst clone¶
A chat session forgets you. This one keeps a folder on your own disk, adds to it on a schedule you set, and hands you a signed record of every touch.
You are going to give an agent three things a chat window cannot have: a memory that lives on your machine, a heartbeat that fires without you, and a leash you can read.
Every command and every output below is copied from a real run.
What you'll have at the end
A folder you own that an agent writes into. A plan that adds one dated entry per run and counts what is there. A second plan that asks a model for a Monday memo using a prompt EKKA never sees. A schedule that fires both without you. And a receipt chain that proves each step, which you can check offline.
Before you start, finish Build a governed AI application. This page assumes you have the CLI, an organization, and an Enclave.
Why a folder is the whole idea¶
An analyst is not valuable because they can summarize a document. They are valuable because of what they have written down over two years: the positions they hold, the thesis behind each one, what they got wrong in March, the counterparty who never replies before noon.
That is a file on their machine. Not a context window, not a chat history you cannot export.
So the clone starts from the file. The file gate is built into the Enclave
you already started. You point it at exactly one folder with --data, and the
Enclave refuses every operation that tries to leave it. Reads and writes are
authorized before they happen and receipted after, like every other gate.
Three things about --data¶
It is optional, and most clones never need it. An Enclave started without
--data has no filesystem reach at all, which is the right default. You are
reading the one recipe that is entirely about a folder, so you want it here.
It points at your own disk. Not a copy, not a sync, not a bucket we manage.
--data names data you already own, where it already lives, so you can keep
working in it while your agent does. Open it in Excel, edit it in your editor,
commit it. Nothing about --data changes how you use your own files.
EKKA governs access and receipts every operation. It does not encrypt your working files. Every read and write is authorized before it happens and signed after, and the receipts are yours to check offline. The bytes on disk stay readable, because you have to be able to read them. EKKA never receives the file, the contents, or the path.
The vault is what your agent holds. --data is what you already own.
Your agent's secrets and its own identity live in a sealed store the Enclave
owns: the vault. Everything you deposit with ekka secret put, and the
key the Enclave signs receipts with, is encrypted at rest, with no folder to
choose and no key for you to hold. On macOS and Windows the sealing key lives
in the OS keychain, so the files stay unreadable if they are copied off your
machine; on Linux the key is a permission-protected file next to them, so
turn on full-disk encryption. The vault is part of every edition: see
What's included.
--data is the other half: the folder your agent works in alongside you.
The clearest case is a code review agent. Your repository cannot live in a
vault, because you need to edit it, build it, commit it and open it in your
editor, so you point the agent at it where it already is. Your source tree,
your spreadsheets and your documents stay where you keep them, because you
are still using them.
1 · Point it at your folder¶
Create the folder first, then start the Enclave pointed at it. Pass --data
with your Enclave id, including on a machine that already holds that Enclave,
which is how you add or move the folder later:
It is recorded in this machine's config, so every later plain ekka enclave
start keeps it. Leave it running and use a second terminal for everything
below. Check that the setting took:
ekka enclave config --schema prints every key this binary understands, with
what each one does. --data is what wrote that EKKA_DATA line into
<EKKA_HOME>/enclave.env; exporting EKKA_DATA in your shell overrides the file
for one run, and shows as [env] instead of [file]. The path must be
absolute, and a folder that does not exist is refused before anything is
recorded, not a warning at startup.
Now confirm your organization can address the gate:
file/enclave-4597bcaa-fs
runs inside this org's Enclave (reported at start)
contract ekka.gate.file.v1
serves paths under the granted folder (list, read, write)
Write down that instance name. It is derived from your Enclave, so yours will differ, and every grant below needs it.
The part you write, not the agent¶
A clone is only worth having if some of what it knows came from you. Put something real in the folder before the agent touches it:
cat > ~/ekka-analyst/thesis.md <<'EOF'
# Investment thesis (written by me, not the agent)
Horizon: 3 years. I am overweight energy transition and underweight
consumer discretionary. I want to be told when a position moves more
than 5 percent against the thesis, not every day.
EOF
echo '{"date":"2026-08-08","note":"seed entry, written by hand"}' > ~/ekka-analyst/ledger.jsonl
2 · The daily plan: add an entry, then count the space¶
ekka plan template writes a runnable plan rather than making you start from
an empty file:
We want two steps in one plan, so open the generated file and use it as the
shape for this one. The plan block at the top is the plan's name; the
definition is the steps. This is the part that carries the meaning:
{
"plan": { "code": "analyst.dailyEntry.v1", "name": "Analyst daily entry" },
"definition": {
"schema_version": "ekka.plan.v2",
"inputs": {
"content": "The entry body.",
"folder": "The folder inside the analyst space to count."
},
"operations": [
{ "id": "daily", "steps": [
{ "id": "writeEntry",
"action_ref": "ekka.local.file.write.v1",
"feature": "file.fs.write",
"inputs": { "resource": "@ekka.run_id", "content": "@input.content" } },
{ "id": "countSpace",
"action_ref": "ekka.local.file.list.v1",
"feature": "file.fs.list",
"inputs": { "resource": "@input.folder" } }
] }
]
}
}
Each step also carries an input and an output contract, which is what the
platform checks the plan against when you save it. Open the block below for the
whole file and save it as daily.json:
Full plan JSON, ready to save as daily.json
{
"plan": {
"code": "analyst.dailyEntry.v1",
"name": "Analyst daily entry"
},
"definition": {
"schema_version": "ekka.plan.v2",
"run_context": [],
"inputs": {
"content": "The entry body.",
"folder": "The folder inside the analyst space to count."
},
"operations": [
{
"id": "daily",
"display_name": "Analyst daily: one entry per run, then count the space",
"execution": {
"preferred": {
"mode": "async",
"runtime": "node"
},
"allowed": []
},
"steps": [
{
"id": "writeEntry",
"action_ref": "ekka.local.file.write.v1",
"executor_type": "function",
"feature": "file.fs.write",
"identity": {
"requires_user_context": false
},
"inputs": {
"resource": "@ekka.run_id",
"content": "@input.content"
},
"action_input": {},
"step_input_contract": {
"type": "object",
"required": [
"resource",
"content"
],
"properties": {
"resource": {
"type": "string"
},
"content": {
"type": "string"
}
}
},
"step_output_contract": {
"type": "object",
"required": [
"op",
"resource"
],
"properties": {
"op": {
"type": "string"
},
"resource": {
"type": "string"
},
"bytes": {
"type": "number"
},
"sha256_b64": {
"type": "string"
}
}
}
},
{
"id": "countSpace",
"action_ref": "ekka.local.file.list.v1",
"executor_type": "function",
"feature": "file.fs.list",
"identity": {
"requires_user_context": false
},
"inputs": {
"resource": "@input.folder"
},
"action_input": {},
"step_input_contract": {
"type": "object",
"required": [
"resource"
],
"properties": {
"resource": {
"type": "string"
}
}
},
"step_output_contract": {
"type": "object",
"required": [
"op",
"resource"
],
"properties": {
"op": {
"type": "string"
},
"resource": {
"type": "string"
},
"entry_count": {
"type": "number"
}
}
}
}
]
}
]
}
}
Three things in there are worth knowing before you copy it.
| What | Why it looks like that |
|---|---|
"resource": "@ekka.run_id" |
Each run names its entry after its own run id, so runs do not overwrite each other. A write in EKKA replaces the file it names, so a fixed filename would leave you with only the newest run. |
@input.content |
The body arrives as a run input, so the same plan writes whatever you hand it. |
| Two steps, one operation | Steps pass values to each other inside a single operation. Splitting them into two operations breaks the wiring. |
Save it, and note the agent it runs as. The file carries the plan's name, so
create takes nothing else:
✓ Plan created: [email protected]
This plan runs as agent 063b953a-5bfc-4466-92f7-7052f7e8a69f.
Authorize it¶
Two grants: one to write, one to list. Use the instance name from gate list.
ekka gate grant add --type file --instance enclave-4597bcaa-fs \
--resource "*" --capability file.fs.write --no-fingerprint
ekka gate grant add --type file --instance enclave-4597bcaa-fs \
--resource "." --capability file.fs.list --no-fingerprint
Why --no-fingerprint, and why * is a decision
Leave --no-fingerprint off and the grant is refused, on purpose:
ekka gate grant: `file/enclave-4597bcaa-fs` runs inside your Enclave and
keeps no alias directory, so use --no-fingerprint: its resources are plain
paths, not aliases.
Gates that address the outside world through aliases pin each grant to the alias it was issued against. The file gate has no aliases, so you state that rather than letting the tool guess.
--resource "*" grants write to every path in the folder, which is
what this plan needs because the filename is a run id nobody can predict.
A grant for one named file is narrower and better where you can use it:
--resource ledger.jsonl. Grants take exact paths, prefix/ and a
single-level *, so scope this to the smallest thing that works for you.
Run it, twice¶
CONTENT='{"fx":"EURUSD 1.0871","note":"morning mark"}'
ekka plan run [email protected] \
--input folder=. --input content="$CONTENT"
ekka plan run [email protected] \
--input folder=. --input content="$CONTENT"
✓ Plan completed (run 28542ef2-476c-4779-9988-47c853bb10a9).
✓ Plan completed (run 3c352fb0-44d6-429d-9ab2-34097d60aed5).
Two runs, two entries, and the hand-written files untouched. The countSpace
step saw the folder grow from four entries to five. That is the difference
between a clone and a chat session: nothing here was in a context window, and
tomorrow's run starts from a folder that already has today's in it.
3 · The weekly plan: a memo, using your own prompt¶
Now a model. The interesting part is not the completion, it is that the instructions are yours and EKKA never reads them.
Write the prompt to your Enclave's home, mode 0600. Placeholders are
@{name}, lowercase with underscores:
# EKKA_HOME comes from `ekka enclave config`; ORG_ID from `ekka whoami`.
PROMPTS="$EKKA_HOME/tenant_prompts/$ORG_ID"
mkdir -p "$PROMPTS"
cat > "$PROMPTS/[email protected]" <<'EOF'
You are a financial analyst's assistant writing a short Monday memo.
ledger size: @{ledger_bytes} bytes
ledger fingerprint: @{ledger_hash}
entries in the working folder: @{entry_count}
The analyst asked: @{user_message}
Write at most five sentences. Be explicit about what you cannot see.
EOF
chmod 600 "$PROMPTS/[email protected]"
The file name is <slug>@<version>.txt, and the slug is lowercase with dots.
The plan names that prompt by slug and version, and lists the placeholders it fills. Three steps this time: read the ledger, count the folder, then ask the model. The meaning is in the last one:
{
"id": "memo",
"action_ref": "ekka.attest.auditedLlmCall.v1",
"executor_type": "llm",
"feature": "llm.infer",
"prompt": {
"source": "tenant",
"slug": "analyst.weekly.memo",
"version": 1,
"expected_variables": [
"ledger_bytes", "ledger_hash", "entry_count", "user_message"
]
},
"inputs": {
"model": "anthropic/sonnet-4",
"user_message": "@input.user_message",
"ledger_bytes": "@weekly.readLedger.output.bytes",
"ledger_hash": "@weekly.readLedger.output.sha256_b64",
"entry_count": "@weekly.countSpace.output.entry_count"
}
}
@weekly.readLedger.output.bytes is how one step reads another step's output:
the operation id, the step id, then the field. Open the block below for the
whole file and save it as weekly.json:
Full plan JSON, ready to save as weekly.json
{
"plan": {
"code": "analyst.weeklyMemo.v1",
"name": "Analyst weekly memo"
},
"definition": {
"schema_version": "ekka.plan.v2",
"run_context": [],
"inputs": {
"resource": "The ledger file inside the analyst folder.",
"folder": "The folder inside the analyst space to count.",
"user_message": "What you want the memo to answer."
},
"operations": [
{
"id": "weekly",
"display_name": "Analyst weekly: look at the space, then write a memo",
"execution": {
"preferred": {
"mode": "async",
"runtime": "node",
"ai": {
"model": "anthropic/sonnet-4"
}
},
"allowed": []
},
"steps": [
{
"id": "readLedger",
"action_ref": "ekka.local.file.read.v1",
"executor_type": "function",
"feature": "file.fs.read",
"identity": {
"requires_user_context": false
},
"inputs": {
"resource": "@input.resource"
},
"action_input": {},
"step_input_contract": {
"type": "object",
"required": [
"resource"
],
"properties": {
"resource": {
"type": "string"
}
}
},
"step_output_contract": {
"type": "object",
"required": [
"op",
"resource"
],
"properties": {
"op": {
"type": "string"
},
"resource": {
"type": "string"
},
"bytes": {
"type": "number"
},
"sha256_b64": {
"type": "string"
}
}
}
},
{
"id": "countSpace",
"action_ref": "ekka.local.file.list.v1",
"executor_type": "function",
"feature": "file.fs.list",
"identity": {
"requires_user_context": false
},
"inputs": {
"resource": "@input.folder"
},
"action_input": {},
"step_input_contract": {
"type": "object",
"required": [
"resource"
],
"properties": {
"resource": {
"type": "string"
}
}
},
"step_output_contract": {
"type": "object",
"required": [
"op",
"resource"
],
"properties": {
"op": {
"type": "string"
},
"resource": {
"type": "string"
},
"entry_count": {
"type": "number"
}
}
}
},
{
"id": "memo",
"action_ref": "ekka.attest.auditedLlmCall.v1",
"executor_type": "llm",
"feature": "llm.infer",
"identity": {
"requires_user_context": false
},
"prompt": {
"source": "tenant",
"slug": "analyst.weekly.memo",
"version": 1,
"expected_variables": [
"ledger_bytes",
"ledger_hash",
"entry_count",
"user_message"
]
},
"inputs": {
"model": "anthropic/sonnet-4",
"user_message": "@input.user_message",
"ledger_bytes": "@weekly.readLedger.output.bytes",
"ledger_hash": "@weekly.readLedger.output.sha256_b64",
"entry_count": "@weekly.countSpace.output.entry_count"
},
"action_input": {},
"step_input_contract": {
"type": "object",
"required": [
"model",
"user_message",
"ledger_bytes",
"ledger_hash",
"entry_count"
],
"properties": {
"model": {
"type": "string"
},
"user_message": {
"type": "string"
},
"ledger_bytes": {
"type": "number"
},
"ledger_hash": {
"type": "string"
},
"entry_count": {
"type": "number"
}
}
},
"step_output_contract": {
"type": "object",
"required": [
"text"
],
"properties": {
"text": {
"type": "string"
},
"finish_reason": {
"type": "string"
},
"usage": {
"type": "object"
}
}
}
}
]
}
]
}
}
Use a model id your llm gate advertises in ekka gate list. Then save,
authorize, and run:
ekka plan create weekly.json
ekka gate grant add --type llm --instance <your-llm-gate> \
--resource anthropic/sonnet-4 --capability llm.infer
ekka plan run [email protected] \
--input resource=ledger.jsonl --input folder=. \
--input user_message="Is my ledger growing, and what can you actually tell me about it?"
The memo we got back:
**Monday Memo: Ledger Status**
Your ledger currently stands at 56 bytes with 5 entries in your working
folder. However, I cannot determine if your ledger is growing because I
don't have access to historical size data from previous weeks. I can
confirm the ledger's current integrity via its fingerprint
(5lqpUkq0rpRv83k0ZqWLejOqdkOGGSHC4XRZue2aBBs=), but I cannot see the
actual contents, entries, or transactions within the file.
Read the last sentence again. The model is describing the platform correctly: a plan sees facts about a file, not its contents. Limits has the rest.
The completion is on your machine, under <EKKA_HOME>/artifacts/<run-id>/. The
receipt that travels back to EKKA carries digests and token counts, never the
prompt and never the answer.
Your prompt stays yours
With "source": "tenant", the Enclave loads the body from your disk and
EKKA is sent the slug and version only, plus a hash of the bytes that ran.
So the audit chain records which instructions ran without EKKA holding
them. If you would rather EKKA manage a prompt for you, that is what
"source": "platform" is for.
4 · Give it a heartbeat¶
Nothing above needed you except to type it. A schedule removes that too.
Start with a once-a-minute cron so you can watch it fire instead of waiting until Monday:
ekka plan schedule [email protected] \
--cron "* * * * *" --tz America/New_York \
--input folder=. --input content="$CONTENT"
--every is the shorthand form and takes a fixed set of intervals: 1m, 5m,
15m, 30m, 1h, 6h, 12h, 1d. Check it:
PLAN CRON ACTIVE NEXT-RUN LAST-RUN
[email protected] * * * * * yes 2026-08-09 15:50:00 2026-08-09 15:49:03
A populated LAST-RUN means it is firing. Ours fired three times in three minutes while nobody was at the keyboard, and each fire left a new entry in the folder:
28542ef2-476c-4779-9988-47c853bb10a9 (run by hand)
3c352fb0-44d6-429d-9ab2-34097d60aed5 (run by hand)
80ba4759-4185-4929-aa68-e2d3b9693258 (fired at 15:49)
29736ef3-16c2-4301-86c5-a1638456e44d (fired at 15:50)
c805cc7b-253c-40e6-b069-8b94b339905f (fired at 15:51)
Once you have seen it fire, swap the minute cron for the one you actually want.
Schedule ids come from ekka plan schedules:
ekka plan unschedule <schedule-id> # remove it
ekka plan schedule [email protected] \
--cron "0 7 * * 1-5" --tz America/New_York \
--input folder=. --input content="$CONTENT"
ekka plan unschedule <schedule-id> --pause keeps the schedule and stops it
firing, and --resume starts it again.
A scheduled fire is an ordinary governed run. Same grants, same refusals, same receipts. There is no quieter path for timers: revoke the write grant and the 7am run is refused exactly the way your keyboard run would be, and the refusal is recorded. A paused schedule never back-fires the runs it missed, so a laptop that was shut for a week does not wake up and fire seven times.
The record of who started it is explicit. A run you type is attributed to you; a fire is attributed to the schedule itself, not borrowed from your session:
plan.dispatched principal system schedule:db7c6f0d-e376-4a7d-9f90-9cff27183236
plan.dispatched principal user d1d42bfb-23e9-475b-bf5d-05dd3768e17f
5 · What the receipts prove¶
It answers two separate claims, and the second is the one that matters for a folder.
- The chain is intact. Every entry covers the one before it, so changing any
entry breaks every signature after it.
verifyrecomputes it offline. - The file operations were signed by this Enclave's own key. A read or write on your disk is not something a remote service can witness. So the Enclave attests it with the identity it enrolled with, and that attestation is what lands in the chain. Nobody has to take EKKA's word for what happened in your folder, including you.
Read one run instead of the whole chain:
#311 local_op_receipt run=80ba4759-… step=writeEntry
#312 local_op_receipt run=80ba4759-… step=countSpace
#313 run_attestation run=80ba4759-…
Each file receipt records the operation, the path relative to your folder, the byte count and a SHA-256. It does not record the contents, so the receipt is shareable with someone you would never show the file to.
Limits¶
- A plan sees facts about a file, not its contents.
ekka.local.file.read.v1returns the path, the byte count and a SHA-256, so the memo above is written from the ledger's size and fingerprint. Declaring acontentfield the action does not produce is refused when you save the plan, not at 7am. - A write replaces the file it names. There is no append mode and no
revision history. Name entries so they do not collide, the way
@ekka.run_iddoes above, and keep your own backups. - One folder per Enclave, not one per agent. Grants narrow which paths a given agent may touch and they are enforced. For two agents that cannot reach each other's files at all, run two Enclaves with two folders.
- Step outputs under
<EKKA_HOME>/artifacts/are ordinary files, not encrypted. So are the files in your working folder, which is the folder you open in your own editor. Secrets live in the Enclave's encrypted vault. Turn on full-disk encryption. - A schedule's inputs are fixed when you create it. Every fire sends the
same
--inputvalues. Read what the fires did withekka run list.
Your version of this¶
Nothing above is about finance. Read ~/ekka-analyst as whatever folder holds
the state your work actually accumulates in, and read the two plans as
"add to it" and "look at it and write something".
-
Sales
The folder is your deal notes. The daily plan drops in what changed in the CRM. The weekly plan asks a model which accounts went quiet.
-
Operations
The folder is your incident journal. The daily plan records what the ERP reported. The weekly plan drafts the Monday summary your team already writes by hand.
-
Trading
The folder is your trading journal and your thesis. The clone adds a mark per session and never edits the thesis, because you did not grant it write access to that path.
-
Engineering
The folder is your release log. The daily plan records what shipped. The weekly plan asks for the changes worth telling customers about.
The swap is a path and a prompt. The grants, the refusals and the receipts do not change, and neither does the fact that the folder is on your disk.
Next¶
- Bring your own API into a plan → Act 4 of the quickstart
- More on plans and schedules → Multi-step plans and schedules
- Read a real audit chain → Governance receipts
- Every command →
ekkaCLI reference