What it does
The first three layers made the graph remember decisions, reason over them, and govern them. The action layer lets it act on them — turning a system you reason from into one you operate from.
An action is a named, typed operation bound to an ontology object type — ApproveOrder,
CancelShipment, EscalateTicket. Invoking one runs a short, strict pipeline: validate the
typed arguments, authorize and record the execution through the business-rules gate, and — only if the gate
didn't reject it — run an optional side effect. The audit edge written to the graph is the record of the
executed action.
emit_decision_trace path as any decision, so it flows through the same
rules gate and leaves the same kind of auditable edge. You don't bolt governance
onto actions — actions are governed decisions that happen to also do something.
Anatomy of an action
Actions live in a per-workspace catalog. Each definition names the operation, the object type it acts on, the relationship it writes, its typed parameters, and how its side effect executes:
{
"name": "sales",
"actions": [
{
"name": "ApproveOrder",
"object_type": "Order", // the ontology type it acts on
"relation_type": "APPROVED", // edge keyword written on invoke
"effect": "approval", // human label of the side effect
"policy_ref": "DiscountPolicy",
"params": [
{ "name": "discount", "kind": "percent", "required": true }
],
"handler": { "kind": "none" } // none | webhook
}
]
}
| Field | Meaning |
|---|---|
name | The operation's name. Also the default relationship keyword (upper-cased) if relation_type is omitted. |
object_type | The ontology object type the action operates on. Empty means "any". |
relation_type | The keyword written to the graph edge on invoke — the same field a rule matches with sim(). |
params | Typed arguments, using the same property kinds as the ontology. Validated + coerced on invoke. |
handler | How the side effect runs: none (record-only) or webhook. |
policy_ref | Recorded onto the audit edge's RelationContext. |
The catalog is persisted per workspace, one atomic JSON file, versioned on every save — exactly like the rules policy and the ontology.
Typed arguments
An action's parameters reuse the ontology's property kinds, so they validate and coerce identically — the same pass that checks an argument normalises it.
Invoke ApproveOrder with a human-shaped discount and it becomes a real number:
| Argument in | Kind | Coerced value |
|---|---|---|
"20%" | percent | 0.20 |
"$25,000" | money | 25000.0 |
"slack" | enum [slack, email] | "slack" (or an error if not a member) |
Missing a required argument, or a value that won't coerce or falls outside an enum/range, comes back as a precise error before anything is written:
validate_args({ "amount": "not money" })
→ errors: [ "missing required argument 'discount'",
"amount: could not parse money from 'not money'" ]
quantitative_data in a form the rules gate can read — so a rule like percent > 0.15
fires on an action's arguments, not just on text extracted from documents.
The invoke pipeline
One call, four steps, in this order — the ordering is the whole point:
The response tells you exactly what happened — the outcome, the audit, the edge that was written, and the handler result:
POST /actions/invoke → 200 {
"ok": true,
"outcome": "FLAG",
"edge": { "src": "Sarah", "tgt": "Order#123", "relation_type": "APPROVED" },
"coerced": { "discount": 0.2 },
"audit": { "rule": "large discount review", "matched_concept": "APPROVAL",
"score": 1.0, "threshold": 0.4 },
"handler": { "kind": "none", "executed": false }
}
Handlers & side effects
A handler is how an authorized action reaches the outside world. Two kinds ship today:
The audited decision trace is the effect. Perfect for capturing that an approval happened, integrating with systems that watch the graph, or a first, safe rollout.
POSTs the invocation payload (action, actor, object, coerced args, outcome) to a URL. This is where an action actually fires a downstream system.
http(s)
schemes — unless the action's handler explicitly sets allow_internal: true for trusted internal
automation. A blocked or failing webhook is reported in the response, never crashes the invoke, and never
un-writes the audit edge.
The /actions API
Workspace-scoped via LIGHTRAG-WORKSPACE; returns 503 unless the server runs in Context
Graph mode (USE_CONTEXT_GRAPH=true).
| Method · Path | Purpose | Returns |
|---|---|---|
GET /actions | Catalog summary | Actions + params + versions |
POST /actions | Save the catalog (validated) | Summary (400 if malformed) |
DELETE /actions | Delete the catalog | {deleted, workspace} |
GET /actions/{name} | One action's definition | Definition (404 if unknown) |
POST /actions/invoke | Run an action | Result — unknown 404, bad args 400, REJECT 422 |
# Register a catalog, then invoke an action
curl -X POST http://localhost:9621/actions \
-H "LIGHTRAG-WORKSPACE: sales" -H "Content-Type: application/json" \
-d '{"catalog":{"name":"sales","actions":[
{"name":"ApproveOrder","object_type":"Order","relation_type":"APPROVED",
"params":[{"name":"discount","kind":"percent","required":true}]}]}}'
curl -X POST http://localhost:9621/actions/invoke \
-H "LIGHTRAG-WORKSPACE: sales" -H "Content-Type: application/json" \
-d '{"action":"ApproveOrder","actor":"Sarah","object_ref":"Order#123","args":{"discount":"20%"}}'
# -> outcome FLAG · audit rule "large discount review" · edge Sarah -APPROVED-> Order#123
Where it sits
The action layer is the top of a deliberate arc — each layer standing on the one below.
| Layer | What it added |
|---|---|
| Facts | The triple (h, r, t) — what is true. |
| Decisions | The rc — why, who, when, under which policy. |
| Policy | The gate — whether a decision is allowed. |
| Ontology | Typed vocabulary — the object types actions bind to. |
| Actions | Executable, governed operations — turning memory into doing. |