Before you start
A first governed write is a configuration job before it is a coding job. The work is to name one process, the people who own it, and the rules it already follows. Have these ready:
- One process where a person already signs off: month-end journal entries, purchase-invoice posting, a credit-limit change. Pick one where a wrong write is costly and the rule is already written down somewhere.
- A named owner and a named approver. The approver is never the asker. If an agent proposes the write, a person approves it; if a person proposes it, a different person approves it.
- The system of record and who holds its write credential. This single fact decides your mode in step 1.
- Your tenant and residency domain. Today there is one residency domain,
in(Mumbai). Chains never cross tenants.
You will touch four of the six KernelPort calls: define to register the rules, assert to send a write intent, verify to check the record, and indirectly seal, which only the kernel calls. KernelPort v1 documents all six.
Who holds the credential
1 · Choose a mode
The mode is not a setting you pick for convenience. It follows from one fact: who holds the credential that writes to your system of record. It also decides what elan1 may truthfully say about your writes.
| Mode | How you connect | Who writes | What we may say |
|---|---|---|---|
| Mode A · Enforced | In-process SDK. elan1 Work hosted on ERPNext; third parties by agreement. | The single writer in the kernel, with a single-use credential that lasts 10 minutes. | No write reaches the system of record without a gate decision and a proof record. |
| Mode B · Attested advisory | The MCP server (34 tools) or the HTTP API. | Your system, with its own credential, after it has called elan1 first. | Evaluated against your rules and recorded. |
Most integrations start in Mode B: your code calls assert before it writes, and acts on the outcome. Mode B records the write; it cannot stop it, so we never describe a Mode B write as blocked. A write type can move to Mode A once elan1 can hold the write credential, after at least 30 days of recorded Mode B traffic, a bypass test against the real system and a review. Modes and assurance levels covers the difference in full.
define
2 · Declare the rules
A declaration is your process’s rules as data, in seven blocks: identity, shape, permissions, write_types, gates, proof_binding and channels. You register it with define, at an explicit version. Every proof record will later name the declaration and the version it was checked against (fields F06 and F07).
{
"id": "decl_month_end_close",
"version": 7,
"identity": {
"name": "Month-end close",
"owner_role": "financial_controller",
"residency_domain": "in",
"system_of_record": "erpnext"
},
"shape": {
"record_type": "journal_entry",
"fields": { "date": "business", "lines": "business", "narration": "business" }
},
"permissions": {
"propose": ["agent", "human"],
"approve": ["human"],
"approver_is_never_the_asker": true
},
"write_types": [
{
"name": "journal_entry.post",
"blast_radius": { "records": 1 },
"dual_control": false,
"reversal": "journal_entry.reverse"
}
],
"gates": { "rate": { "per_actor_per_hour": 20 }, "schedule": { "window": "business_hours" } },
"proof_binding": { "seal": "elan1.proof.seal.v6" },
"channels": { "api": "B", "mcp": "B" }
}Three things to get right on the first declaration:
- One write type per kind of change. There is no generic write.
journal_entry.postandjournal_entry.reverseare separate write types, and an undo takes the Right Path like any other change. - A bounded blast radius. Rung 5 compares each write with the declared radius, so “update one invoice” cannot arrive as an unbounded filter.
- Only the optional gates are parameters. Six of the nine pre-gates — consent, residency, retention, dual control, counterparty and precondition — always run and can never be declared off. Rate, budget and schedule can be tuned, or switched off with a decision record.
Registering a declaration is itself a governed write under dual control: it takes a control-plane token and a second named person. Changing the rules moves the version; records written under version 7 keep pointing at version 7. See Declarations for every block.
assert
3 · Assert a write intent
A write intent is a typed proposal to change one record. It names the write type, the declaration it claims to fall under, the actor, the record and its expected version, and the payload. Your agent, the elan1 Decide Router or a person in the console may produce one. Only elan1 Govern turns it into an allow.
POST /v1/assert
Authorization: Bearer <tenant token, act scope>
X-Tenant: tnt_specimen_01
Idempotency-Key: wr_specimen_7c2
Content-Type: application/json
{
"write_id": "wr_specimen_7c2",
"write_type": "journal_entry.post",
"declaration": { "id": "decl_month_end_close", "version": 7 },
"actor": { "id": "agt_specimen_gl02", "kind": "agent" },
"record": { "id": "je_specimen_5531", "expected_version": 1 },
"payload": {
"date": "2027-07-14",
"lines": [
{ "account": "5100 Freight inward", "debit": "18400.00" },
{ "account": "2100 Accrued expenses", "credit": "18400.00" }
]
}
}| Name | Type | Description |
|---|---|---|
| write_id | string | Your identifier for this intent. Sent again as Idempotency-Key; replaying it returns the original outcome. Becomes F04 on the record. |
| write_type | string | Must be listed in the declaration's write_types block, or rung 2 refuses. |
| declaration | { id, version } | The declaration and the explicit version the intent claims to fall under. |
| actor | { id, kind } | Who proposes. The kernel sets actor_kind on the record from the token, never from the caller. |
| record | { id, expected_version } | The record that would change, and the version you read. A stale version fails the precondition gate. |
| payload | object | The change itself. The record keeps only its hash (F14), so personal data stays out of evidence. |
assert runs the whole Right Path in order and answers synchronously. You do not call the rungs one by one, and you cannot skip one.
committed · refused · escalated
4 · Handle the outcome
Every assert ends in one of three outcomes, and every outcome writes a proof record. A platform failure is a fourth case, but it is ours, not a rule, and it is never billed.
{
"outcome": "committed",
"rung": 9,
"record": { "id": "je_specimen_5531", "version": 2 },
"proof": { "seq": 1042, "digest": "sha256:9f3c…e41a" },
"meter": { "governed_actions": 1 }
}{
"outcome": "refused",
"rung": 3,
"refusal": {
"code": "GATE.PRECONDITION_FAILED",
"gate": "G9",
"reason": "The accounting period for 2027-07 is closed."
},
"proof": { "seq": 1043, "digest": "sha256:4be1…90c7" },
"meter": { "governed_actions": 0.4 }
}{
"outcome": "escalated",
"rung": 6,
"reason": "Above the autonomy ceiling. A named person, not the asker, decides.",
"obligation": "OB-specimen-0412",
"proof": { "seq": 1039, "digest": "sha256:3b9a51c2…7e04d1f8" },
"meter": { "governed_actions": 0 }
}write_id. If elan1 cannot be reached, do not write: allows stop, and changes wait for a person.// Illustrative. Field names follow the specimens on this site; exact names are in the KernelPort v1 reference.
const res = await fetch(`${BASE_URL}/v1/assert`, {
method: "POST",
headers: {
Authorization: `Bearer ${TENANT_TOKEN}`,
"X-Tenant": TENANT_ID,
"Idempotency-Key": intent.write_id, // the same write_id on every retry
"Content-Type": "application/json",
},
body: JSON.stringify(intent),
});
if (!res.ok) {
// A platform failure, not a rule. Retry with the SAME write_id; never mint a new one.
// If elan1 cannot be reached, do not write: fail toward a human.
return holdForPerson(intent, "elan1 unreachable");
}
const out = await res.json();
switch (out.outcome) {
case "committed":
// Mode A: the single writer has made the change. Store the proof reference.
return saveProofRef(intent.write_id, out.proof.seq, out.proof.digest);
case "refused":
// A typed "no". It was recorded and metered at 0.4 of a governed action. Show the code; do not retry unchanged.
return showRefusal(out.refusal.code, out.rung);
case "escalated":
// A named approver decides. Track the obligation; expiry means refusal.
return awaitApproval(out.obligation);
}In Mode B the same three outcomes come back, and your code is the one that acts on them: write on committed, do not write on refused, wait on escalated. How a Mode B caller reports the write it then made is part of the HTTP API; see HTTP API. Approval results can be delivered on a webhook, HMAC-authenticated and carrying seq, so a gap in delivery is detectable. Errors and refusals sets out the refusal envelope.
verify
5 · Read and verify the record
Each outcome above returned a proof reference: a sequence number and a digest. The record behind it has 16 fields under Seal v6, hashed with SHA-256 over a length-prefixed preimage and chained to the record before it through prev_digest.
At L1, checking a record means recomputing its digest and comparing it with prev_digest on the next record, and confirming that seq only ever goes up by one. A changed or missing record breaks the chain. That is what tamper-evident means at L1: a change shows, to you. It is not signed, and someone who does not trust elan1 cannot check it without us.
# Illustrative. The exact preimage encoding is set out on the Proof record page.
# At L1 this check is tamper-evident to you; it is not a signature.
def chain_is_unbroken(records, seal_digest):
for prev, cur in zip(records, records[1:]):
if cur["seq"] != prev["seq"] + 1:
return False, f"gap after seq {prev['seq']}"
if cur["prev_digest"] != seal_digest(prev):
return False, f"record {prev['seq']} does not match its successor"
return True, "chain unbroken"From L2, each record is also signed with Ed25519 and anchored to two RFC 3161 timestamp authorities, and an evidence bundle can be checked offline with the free verifier. The Proof record page sets out all 16 fields and the exact recompute.
6 · Before production
A first governed write in a sandbox is the start of onboarding, not the end. Before production keys are issued, expect to work through this checklist with us:
- Refusal paths exercised. Show that your code handles a refused and an escalated outcome, including an approval that expires.
- Idempotency proven. Retry a timed-out request with the same
write_idand confirm you receive the original outcome, not a second write. - Fail-closed behaviour. Cut the connection to elan1 and confirm your system stops writing and hands the change to a person.
- Approvers named. Every write type that can escalate has a named approver who is not the asker, and the owner knows who covers when that person is away.
- For Mode A: credential custody. At least 30 days of recorded Mode B traffic, a bypass test against the real system and a review before the kernel takes the credential.
What the customer reference covers
The six call names, the 16-field record and Seal v6 are fixed, and so is the rest of KernelPort v1. The examples on this page are illustrative; the full reference customers receive with their workspace sets out:
- Exact argument and response field names, including the shape of the outcome envelope.
- Refusal codes. Real codes come from one closed register. Codes on this site, such as
GATE.PRECONDITION_FAILED, are illustrative. - The declaration schema’s field-level syntax. The seven block names are stable.
- The languages the in-process SDK supports.
The changelog records each change to these docs.