Skip to main content
The knowledge global exposes the knowledge features and items that have been attached to a document family. Scripts can read what the knowledge engine has resolved for a family without re-running resolution, then dispatch downstream behaviour from a single source of truth.
knowledge is available in script steps only — not in intake scripts, event subscriptions, or selection-option formulas. It is read-only: every returned object is Object.freeze-d, and there is no setter or mutate API.

How to think about it

Knowledge in Kodexa is a declarative way to describe what a document is and what should happen to it — kept separate from the script’s runtime state. Two pieces work together:
  • Knowledge features are facts about a document family (vendor ID, language, document classification). They are attached to the family early in processing — typically by an upstream script step that reads upstream metadata, or by an intake module.
  • Knowledge sets are org-level rules that match on features and produce knowledge items with rich, type-checked properties: configuration, prompts, validation rules, processing models, anything you want a downstream script to consume.
When a script step runs, the knowledge engine has already evaluated the project-scoped sets and materialised the resulting items into the document family. Your script reads those items via the knowledge global — it does not re-implement the matching logic. The mental model is declare configuration as knowledge, dispatch in the script:

Why we built it this way

Without knowledge, a script that needs vendor- or document-type-specific behaviour has to do one of:
  • Hard-code lookup tables inside the script body.
  • Round-trip to a service bridge for every document.
  • Stash configuration in environment variables or module options.
  • Re-implement set-matching logic by reading raw features and writing JS conditionals.
All of these put both logic and data in the script — making it harder to update one without redeploying the other. The knowledge global is designed so you can keep configuration in org metadata (versioned, reviewable, separately deployed) and keep the script thin enough that it rarely changes.

What knowledge is and isn’t

If the answer to “where does this value come from?” is “an org admin configures it in the UI”, knowledge is probably the right home. If the answer is “this script computes it for this document”, keep it in the script.

API surface

The global has four explicit accessors and four bare-form conveniences.

Bare accessors (single-family scripts)

When exactly one document family is in scope, scripts can use the bare forms. These are the 80% path for activity-plan script steps that operate on one family at a time.

Explicit accessors (always available)

The explicit forms are required when the script’s families slice has anything other than exactly one entry. The familyId you pass MUST be in the script’s families slice (see Family-access scoping).

Object shapes

Annotations below use TypeScript-style notation. Each returned array element is a frozen plain JS object with exactly these properties.

KnowledgeFeature

KnowledgeFeatureType (sub-record on each feature)

KnowledgeItem

KnowledgeItemType (sub-record on each item)

KnowledgeOption (schema entries inside options / extendedOptions)

properties is free-form. It is whatever shape the type author defined in options. If a type defines instructionMarkdown, domain, pipeline, etc., those keys live on properties — they are NOT separate top-level fields on the feature or item. To find what keys a type uses, inspect featureType.options (or itemType.options) and look at each option’s name.

Single-family rule

The bare accessors (knowledge.features, knowledge.items, knowledge.featuresByType, knowledge.itemsByType) auto-bind to families[0] when exactly one family is in scope. Anything else throws:
For multi-family script steps, you MUST iterate over families and use the explicit forms:

Family-access scoping

Even with the explicit accessors, the familyId you pass must be present in the script’s families slice (the families attached to the script’s task). Calling with a foreign family throws:
This is the primary tenant-isolation defence: the underlying KDDB loader and feature query are keyed by document_family_id only, so this check prevents a script from reading another tenant’s data by guessing UUIDs.

Write protection

Every object returned by knowledge.* is Object.freeze-d, recursively. Nested arrays and sub-records (e.g. featureType, itemType, properties) are also frozen. In both cases Object.isFrozen(obj) returns true. The contract is: knowledge is read-only from scripts. If you need to compute a derived value, build a new object instead of mutating the returned one:

Performance characteristics

Per-script-execution caches keyed by family ID make repeated calls cheap. Calling knowledge.featuresByType("vendor") ten times in the same script runs one query.
The KDDB load behind knowledge.items is a separate loader from loadDocument(familyId). If a script calls both, the KDDB is loaded twice (once for each cache). This is acceptable today but may be consolidated in a future revision.

Item ordering

knowledge.items and knowledge.itemsByType always return items sorted by:
  1. sequenceOrder ASC
  2. ties broken by slug ASC
This is a documented contract — scripts that index by position (e.g. items[0]) may rely on it. Without this, scripts would be at the mercy of whatever order the KDDB happens to return. Features have no documented ordering — they come back in whatever order the SQL join returns them.

Active-features policy

The bindings return all features and items regardless of the active flag, with the boolean exposed on each instance. If you want only-active records, filter in JS:
The rationale: scripts that audit, log, or report on inactive records need to see them too. Filtering up-front would have been a footgun.

Empty-result handling

Filtered accessors return [] when nothing matches. Indexing [0] on an empty array yields undefined, and any property access after that throws. Always guard:

Null featureType / itemType

In pathological cases (e.g. the type was deleted via direct DB access despite the immutability claims) the enrichment lookup returns no row. The binding surfaces this as featureType: null (or itemType: null) on the affected instance rather than throwing. Null-check before accessing nested fields:
itemType may also be null when a knowledge item references a type from a different organisation than the script’s context, since the type lookup is org-scoped.

Type-slug case sensitivity

featuresByType / itemsByType perform an exact string match against the type’s slug. Slugs are lowercase by convention — featuresByType("Vendor") will not match a type whose slug is "vendor".

Kill switch

There is no runtime feature flag for the knowledge global. Emergency disable requires commenting out the registration in kodexa-orchestrator/internal/service/planner_script_adapters.go (the kb.Register(vm) call near the bottom of plannerContextBindings.Register) and redeploying.

What’s NOT here (deferred to v2)

The following are not available in v1:
  • Resolution metadata (knowledge.matches, ResolutionMatch objects). The engine does not currently persist per-set resolution decisions; surfacing them requires engine changes.
  • Per-item source / clauses. Which knowledge clause produced an item is not exposed.
  • Attachment-fetch accessors. itemType.supportsAttachment is exposed, but there is no accessor to fetch the underlying attachment (presigned URL, content, etc.).
  • Mid-script re-resolution. The bindings read what is already attached to the family; they do not re-run the knowledge engine. If you need fresh resolution, run a separate engine invocation upstream of the script.
  • Feature provenance read fields (feature.attachedAt, feature.attachedBy). The schema and write-side population land in v1’s parallel track so the data accumulates; the read API ships in v2.

Worked example: vendor routing

A common cass-analysis pattern: a single document family carries a vendor feature emitted upstream, the knowledge engine resolves the family’s processing-model knowledge set, and a downstream script step dispatches based on the resolved item’s properties.
The same pattern applied to multi-family steps:

Common patterns

Pattern 1: Two-step feature emission then read

The canonical use of knowledge is a two-step activity-plan flow: an upstream script step emits a feature using the planner’s {features: [...]} return contract; the orchestrator runs AssessAndEnrich between steps; a downstream script reads the resolved items via knowledge.itemsByType(...).
The split keeps each step focused: step 1 sources data from upstream, step 2 dispatches based on org-level configuration.

Pattern 2: Per-vendor configuration without a lookup table

Older code commonly contains hard-coded tables like:
Knowledge replaces this with a declarative item per vendor:
Now adding a new vendor is a knowledge-set edit (UI or YAML), not a code change.

Pattern 3: Conditional behaviour gated on feature presence

Sometimes you don’t need a knowledge item at all — just the existence of a feature. Use featuresByType:

Pattern 4: Multi-vendor / batch script steps

When the step’s families slice has more than one entry, the bare accessors throw. Loop and use the explicit forms:

Pattern 5: Audit / inspection

A read-only inspection script can summarise what the engine has done:
This is useful in test pipelines, before-merge checks, and triage activity-plans where you want visibility into what was resolved.

Choosing knowledge vs. alternatives

A short decision guide for “where should this configuration live?”: The recurring test: if the org admin needs to change this value tomorrow, do they have to redeploy code, redeploy the project template, or just edit a knowledge item? The last is the cheapest — favour it when the value is genuinely org-scoped.

See also