Skip to content

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:

mkdir -p ~/ekka-analyst
ekka enclave start <enclave-id> --data ~/ekka-analyst
ekka enclave start: data folder set to /Users/you/ekka-analyst.

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
  EKKA_DATA   /Users/you/ekka-analyst  [file]

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:

ekka gate list
  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:

ekka plan template entry.json --gate file --op write

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:

ekka plan create daily.json
✓ 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).
ls ~/ekka-analyst
28542ef2-476c-4779-9988-47c853bb10a9
3c352fb0-44d6-429d-9ab2-34097d60aed5
ledger.jsonl
thesis.md

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:

ekka plan schedules
  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

ekka receipts verify

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. verify recomputes 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:

ekka receipts show <run-id>
  #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.v1 returns the path, the byte count and a SHA-256, so the memo above is written from the ledger's size and fingerprint. Declaring a content field 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_id does 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 --input values. Read what the fires did with ekka 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