Context Graph · Feature Guide

The Action Layer

A graph that only records decisions is a filing cabinet. Bind executable operations to your object types and it becomes a control plane — an agent can do things (approve, cancel, escalate), each one validated, authorized by your rules, and written back as its own audit record.

typed operations bound to object types validate → gate → do PASS · FLAG · REJECT audited by design SSRF-guarded handlers
The purpose

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.

◆ The one idea
Execution reuses the decision machinery you already have. An action is emitted through the same 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.
The definition

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
    }
  ]
}
FieldMeaning
nameThe operation's name. Also the default relationship keyword (upper-cased) if relation_type is omitted.
object_typeThe ontology object type the action operates on. Empty means "any".
relation_typeThe keyword written to the graph edge on invoke — the same field a rule matches with sim().
paramsTyped arguments, using the same property kinds as the ontology. Validated + coerced on invoke.
handlerHow the side effect runs: none (record-only) or webhook.
policy_refRecorded 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.

Type safety

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 inKindCoerced value
"20%"percent0.20
"$25,000"money25000.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'" ]
Why coercion matters here
Because money/percent arguments are coerced, they're also rendered into the decision's 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.
Execution

The invoke pipeline

One call, four steps, in this order — the ordering is the whole point:

1 · validate args coerce · required · enum 2 · rules gate emit_decision_trace REJECT → stop 3 · side effect handler (PASS/FLAG only) 4 · audit edge on the graph provenance = action:ApproveOrder
The gate runs before the side effect. A REJECTed action writes nothing and never fires its handler; a PASS/FLAG persists the audit edge, then runs the effect.

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 }
}
◆ Authorize before you act
Running the gate first is what makes actions safe: a decision your rules would reject — a discount over policy, an out-of-hours cancellation — never reaches the side effect. REJECT surfaces as HTTP 422 and nothing is written or done.
Side effects

Handlers & side effects

A handler is how an authorized action reaches the outside world. Two kinds ship today:

none — record-only.
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.
webhook — call out.
POSTs the invocation payload (action, actor, object, coerced args, outcome) to a URL. This is where an action actually fires a downstream system.
SSRF-guarded by default
A server that POSTs to operator-supplied URLs is an SSRF risk, so the webhook handler resolves the target host and refuses loopback, private, link-local, reserved, and multicast addresses — and non-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 interface

The /actions API

Workspace-scoped via LIGHTRAG-WORKSPACE; returns 503 unless the server runs in Context Graph mode (USE_CONTEXT_GRAPH=true).

Method · PathPurposeReturns
GET /actionsCatalog summaryActions + params + versions
POST /actionsSave the catalog (validated)Summary (400 if malformed)
DELETE /actionsDelete the catalog{deleted, workspace}
GET /actions/{name}One action's definitionDefinition (404 if unknown)
POST /actions/invokeRun an actionResult — 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
The stack

Where it sits

The action layer is the top of a deliberate arc — each layer standing on the one below.

LayerWhat it added
FactsThe triple (h, r, t) — what is true.
DecisionsThe rc — why, who, when, under which policy.
PolicyThe gate — whether a decision is allowed.
OntologyTyped vocabulary — the object types actions bind to.
ActionsExecutable, governed operations — turning memory into doing.
◆ What's next
This is the first slice of the P3 "action / kinetic layer." The sequenced follow-ons: a plain-English action author (like the rules and ontology guides describe), object-level access control over who may invoke what, exposing the catalogue over MCP for agent action-discovery, and a planner that assembles multi-step workflows over the typed graph. Fill the graph with the Web Ingester; start with The Fourth Element.
↑ Top