Skip to content

decorations/ — scheduled KG node enrichment

A realm drives enrichment of nodes already in the knowledge graph by declaring decoration manifests. Each manifest binds a realm-declared action to a set of Neo4j labels and a cadence; the host’s decoration scheduler walks candidate rows, invokes the action per node, and persists the result.

This is the right shape for “keep these rows fresh”, “add my realm’s structured data to entities the user already has”, and “re-summarise on a TTL” — without the realm needing to write any host code, manage scheduling, or implement dedup.

realm-hubspot/decorations/contact-enrich.yml
name: hubspot-contact-enrich # required; stable kebab-case id (used as stamp key)
targetLabels: [HubSpotContact] # required; ≥ 1 Neo4j label this decoration targets
action: enrichHubSpotContact # required; an action declared in this realm's actions/
tickInterval: PT6H # ISO-8601 duration; how often the scheduler checks (default PT6H)
batchSize: 25 # candidate rows per tick (default 25)
concurrency: 4 # parallel decorate calls within a tick (default 4)
refreshAfter: P7D # ISO-8601 duration; null/omitted = one-shot

A file may carry a single manifest or a YAML list of them (- name: ... per entry).

The referenced action receives one node snapshot and returns a structured DecorationResult describing what to write. The host owns persistence and binds world, context, principal, and execution out of band; none is serialized into the action input. The action stays pure.

Input bindings (the action’s inputs: block in actions/<name>.yml must accept these):

Binding Type Meaning
nodeId string The candidate node’s stable id
nodeName string The node’s display name
nodeLabels list<string> Every Neo4j label on the node
nodeProperties object Map of realm-visible properties on the node. Reserved host metadata is redacted.

Output type: DecorationResult (a host-provided domain type). Action declarations set outputType: com.embabel.world.kg.decoration.DecorationResult.

interface DecorationResult {
// Property keys to MERGE onto the node. Setting `description`
// here updates the node's display description. Pre-existing
// properties are preserved unless an entry here overrides them.
propsToSet?: Record<string, unknown>
// Relationships to MERGE from the node to other entities.
// Idempotent — re-running doesn't duplicate edges.
edgesToMerge?: Array<{
type: string // relationship type, e.g. "HAS_HUBSPOT_DEAL"
targetNodeId: string // the other end's stable id
targetLabel?: string // optional; helps some graph stores
properties?: Record<string, unknown>
}>
}

Reserved host metadata includes scope, identity, policy, lease, provenance-control, and credential fields such as worldId, contextId, principalId, executionId, and worldEpoch. Retired or legacy host identity fields remain reserved so old data cannot be forged. Reserved metadata is never included in nodeProperties. The host rejects propsToSet, relationship targets, or any other guest result that attempts to set, remove, or substitute a reserved field.

Empty result is fine — the scheduler stamps the row regardless, so the decorator doesn’t re-run before its refreshAfter (or never, if one-shot). Return empty when the action surveyed the row and decided there was nothing to add (e.g. “no signature in this email”, “no Wikipedia article for this person”).

  1. Discovery. At host startup, the decoration loader walks every installed realm for decorations/*.yml (or *.yaml).
  2. Wiring. Each manifest becomes a scheduled decorator on the host’s TaskScheduler, ticking at tickInterval. The decorator’s stamp property is <name>DecoratedAt.
  3. Per tick. A label-filtered Cypher predicate selects up to batchSize rows whose stamp is missing OR (when refreshAfter is set) older than the refresh window. Rows are dispatched to decorate(...) calls in parallel up to concurrency.
  4. Per node. The host invokes the referenced action with the input bindings. On success, the host persists propsToSet and edgesToMerge, then stamps the row. On exception, the row stays un-stamped — next tick retries.

Single-shot enrichment from external API:

realm-wikipedia/decorations/topic-wiki.yml
name: wikipedia-topic
targetLabels: [Topic]
action: fetchWikipediaTopic
tickInterval: PT12H
batchSize: 50
concurrency: 4
# no refreshAfter — Wikipedia summaries change so slowly that one-shot is right

Periodic resummarisation:

realm-hubspot/decorations/deal-summary.yml
name: hubspot-deal-summary
targetLabels: [HubSpotDeal]
action: summariseHubSpotDeal
tickInterval: PT2H
batchSize: 25
concurrency: 4
refreshAfter: P14D

Second-order entity discovery (Bills from Billers):

realm-finance/decorations/bills-from-biller.yml
name: bills-from-biller
targetLabels: [Biller]
action: scanBillsForBiller
tickInterval: PT6H
batchSize: 25
concurrency: 4
refreshAfter: P1D

The action body queries email threads from the biller’s domain, extracts :Bill rows, and returns them as edgesToMerge entries with type: "ISSUED_BILL".

Knob Picking it
tickInterval How quickly new rows of targetLabels should get decorated. For freshly-arriving entities, ~1h. For one-shot enrichments, can be 24h+ — the predicate empties after first pass.
batchSize Throttle per-tick LLM / HTTP cost. 25 is generous; drop to 10 for expensive actions.
concurrency Parallel calls within a tick. 1 for pure-Cypher actions (parallelism = contention). 4-8 for LLM / HTTP-bound actions. Honor your provider’s rate limits.
refreshAfter Set when re-decoration earns its rent (re-summarise after activity, re-fetch slowly-changing facts). Omit for genuinely one-shot enrichments.
  • Self-persisting actions. Don’t have the action call gateway methods to write graph properties directly. Return them via DecorationResult — the host’s persistence + stamping is one atomic-ish step that keeps “what this decorator changed” auditable.
  • Cross-realm dependencies. A manifest references one action from its own realm. If you need behaviour from another realm, call it through the gateway from inside the action body, don’t pull it via manifest composition.
  • Sweep when an event works. If the entity gets an enrichable signal on creation (a webhook fires, an EmailSignal lands), prefer the event path. Decorations are for what can’t be done event-driven — refreshes, federations, cross-source joins, slow-moving fact maintenance.