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.
Manifest
Section titled “Manifest”name: hubspot-contact-enrich # required; stable kebab-case id (used as stamp key)targetLabels: [HubSpotContact] # required; ≥ 1 Neo4j label this decoration targetsaction: 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-shotA file may carry a single manifest or a YAML list of them (- name: ... per entry).
The action contract
Section titled “The action contract”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”).
Lifecycle
Section titled “Lifecycle”- Discovery. At host startup, the decoration loader walks every installed realm for
decorations/*.yml(or*.yaml). - Wiring. Each manifest becomes a scheduled decorator on the host’s
TaskScheduler, ticking attickInterval. The decorator’s stamp property is<name>DecoratedAt. - Per tick. A label-filtered Cypher predicate selects up to
batchSizerows whose stamp is missing OR (whenrefreshAfteris set) older than the refresh window. Rows are dispatched todecorate(...)calls in parallel up toconcurrency. - Per node. The host invokes the referenced action with the input bindings. On success, the host persists
propsToSetandedgesToMerge, then stamps the row. On exception, the row stays un-stamped — next tick retries.
Examples
Section titled “Examples”Single-shot enrichment from external API:
name: wikipedia-topictargetLabels: [Topic]action: fetchWikipediaTopictickInterval: PT12HbatchSize: 50concurrency: 4# no refreshAfter — Wikipedia summaries change so slowly that one-shot is rightPeriodic resummarisation:
name: hubspot-deal-summarytargetLabels: [HubSpotDeal]action: summariseHubSpotDealtickInterval: PT2HbatchSize: 25concurrency: 4refreshAfter: P14DSecond-order entity discovery (Bills from Billers):
name: bills-from-billertargetLabels: [Biller]action: scanBillsForBillertickInterval: PT6HbatchSize: 25concurrency: 4refreshAfter: P1DThe action body queries email threads from the biller’s domain, extracts :Bill rows, and returns them as edgesToMerge entries with type: "ISSUED_BILL".
Choosing parameters
Section titled “Choosing parameters”| 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. |
Anti-patterns
Section titled “Anti-patterns”- 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.

