Omnigraph
Concepts

Hooks

React to graph changes or query-result changes without a separate event layer.

Hooks let you react to changes in the graph without polling. They turn Omnigraph from a passive data store into an active coordination surface where agents can trigger each other through shared state.

Two kinds of hooks

Graph-change hooks

A graph-change hook fires when a specific kind of mutation happens: a node is created, updated, or deleted.

{
  "name": "new_critical_ticket",
  "trigger": "on_create",
  "node_type": "Ticket",
  "filter": { "priority": "critical" },
  "action": "notify_oncall_agent"
}

This hook fires every time a Ticket node with priority: critical is created. The action can be a webhook, a command, or an agent invocation.

Graph-change hooks are simple and fast. They watch one node type and one operation.

Query-result hooks

A query-result hook fires when the result of a stored query changes. It watches a query, not a single entity.

{
  "name": "unreviewed_work_available",
  "query": "pending_reviews",
  "condition": "result_count > 0",
  "action": "wake_review_agent"
}

Where pending_reviews is a stored query:

query pending_reviews() {
    match {
        $t: Ticket { status: "pending" }
        not { $t hasReview $r }
    }
    return { $t.slug, $t.title }
}

The hook evaluates the query after every merge to main. When the result set goes from empty to non-empty, the hook fires.

Why query-result hooks are different

Graph-change hooks are event-driven: "something was created." Query-result hooks are condition-driven: "a query that used to return nothing now returns something."

This distinction matters because many interesting conditions involve multiple entity types and relationships:

  • "There is a ticket that is open, has no assignee, and has a message from a customer in the last hour."
  • "There are more than 5 accounts with no activity in the past 30 days."
  • "A deal moved to 'closed-won' but has no signed contract attached."

None of these are single-entity events. They are emergent conditions that arise from the combination of multiple nodes and edges. Query-result hooks watch for these conditions directly.

Patterns

Work discovery

An agent wakes up when new work appears in the graph. The hook watches for the condition that defines "there is work to do."

query unprocessed_signals() {
    match {
        $s: Signal
        not { $s processedBy $a: Agent }
    }
    return { $s.id, $s.type, $s.payload }
}
{
  "name": "signals_to_process",
  "query": "unprocessed_signals",
  "condition": "result_count > 0",
  "action": "wake_signal_processor"
}

Review queues

A review agent wakes up when items need human or automated review.

query needs_review() {
    match {
        $d: Decision { status: "proposed" }
        $d madeBy $agent
        not { $d reviewedBy $reviewer }
    }
    return { $d.id, $d.outcome, $d.rationale, $agent.name }
}
{
  "name": "review_queue",
  "query": "needs_review",
  "condition": "result_count > 0",
  "action": "wake_review_agent"
}

Follow-up processing

After one agent finishes work, downstream agents pick up the results.

query enriched_but_unscored() {
    match {
        $a: Account
        $a hasSignal $s: Signal { processed: true }
        not { $a hasScore $sc }
    }
    return { $a.name }
}
{
  "name": "score_enriched_accounts",
  "query": "enriched_but_unscored",
  "condition": "result_count > 0",
  "action": "wake_scoring_agent"
}

The enrichment agent writes signals. The scoring agent is triggered when accounts have signals but no scores. No direct communication between agents.

Hooks and branches

Hooks evaluate against the main branch by default. They fire after a merge to main, not during work on a feature branch. This means:

  • An agent working on a branch doesn't trigger hooks for other agents.
  • Hooks fire only when validated work lands on main.
  • This prevents cascading reactions from speculative or incomplete work.

If you need hooks on a specific branch (e.g., for testing), you can configure the hook to watch a named branch instead.

On this page