Use when creating or editing Kodexa activity plans — org-scoped YAML defining a step graph (CREATE_TASK, EXECUTION, BRIDGE_CALL, SCRIPT, LLM, APPROVAL, AGENT) plus inputs schema and default title/description templates. Replaces the old plan_template inside task-templates.
How this skill is triggered — by the user, by Claude, or both
Slash command
/kodexa-metadata-skills:activity-planThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
An **ActivityPlan** is the org-level definition of automated work — a graph of steps, an inputs schema, and default templates for title/description rendering. Activities are *runtime instances* of a plan; the plan itself is metadata, bound to projects via `project_resources`.
An ActivityPlan is the org-level definition of automated work — a graph of steps, an inputs schema, and default templates for title/description rendering. Activities are runtime instances of a plan; the plan itself is metadata, bound to projects via project_resources.
Introduced by the activity refactor (2026-05-02). Activity plans replace the inline planTemplate that used to live inside task templates. They are first-class resources, syncable via kdx-cli (push order 50), and resolvable via activity-plan://orgSlug/slug.
task_template.planTemplate block into its new homeBRIDGE_CALL step)CREATE_TASK with waitForCompletion)┌─────────────────┐ ┌──────────────────┐ ┌───────────────┐
│ Trigger │ │ ActivityPlan │ │ TaskTemplate │
│ (project) │ → │ (org) │ → │ (org) │
│ eventKind + │ │ steps[].kind= │ │ shape of the │
│ filter │ │ CREATE_TASK │ │ human task │
└─────────────────┘ └──────────────────┘ └───────────────┘
fires on event runs steps in order materialized by
CREATE_TASK steps
A Trigger (project-scoped) listens for an event (task_created, task_status_changed, activity_completed, manual), then starts the referenced ActivityPlan as an Activity. Each CREATE_TASK step in the plan materializes a Task from a TaskTemplate.
slug: invoice-processing # Required — unique within (orgSlug, slug)
name: "Invoice Processing Workflow" # Required — display name (from AbstractMetadata)
organizationId: ${orgSlug} # Required
description: "Extract → review → approve invoices"
inputsSchema: # Optional — JSON-Schema; validated at start time
type: object
required: [documentId]
properties:
documentId: { type: string }
priority: { type: string, enum: [low, medium, high] }
defaultTitleTemplate: "Process invoice {{ .inputs.documentId }}"
defaultDescriptionTemplate: |
Automated processing of {{ .inputs.documentId }} (priority: {{ .inputs.priority }})
steps: # Required — ordered graph of step nodes
- slug: extract
kind: EXECUTION
config:
moduleRef: "${orgSlug}/invoice-extractor"
options: {}
- slug: review
kind: CREATE_TASK
dependsOn: [extract]
config:
taskTemplateRef: invoice-review
taskStatusSlug: todo
waitForCompletion: true
metadata: # Optional — description, icon, provider, etc.
description: "Standard invoice intake → review pipeline"
icon: "file-invoice"
defaultTitleTemplate and defaultDescriptionTemplate use Go text/template syntax. Context root: { inputs, org, project, caller, now }. Missing keys render as empty strings. No HTML escaping (titles are plain text).
inputsSchema is JSON-Schema-shaped. Validation runs at activity start time; failures return HTTP 400 with field-level errors.
Every step in steps shares the same envelope:
- slug: "string" # Unique within the activity plan
kind: "CREATE_TASK | EXECUTION | BRIDGE_CALL | SCRIPT | LLM | APPROVAL | AGENT"
conditionExpr: "string" # Optional — predicate; step skipped if false
badges: # Optional — UI-only
- { icon: "...", color: "...", label: "..." }
dependsOn: ["other-slug", "other-slug:OUTCOME"] # Optional
config: { ... } # Per-kind, see below
dependsOn notes. Each entry is either a step slug (run after that step succeeds) or slug:OUTCOME to gate on a specific action outcome (relevant for SCRIPT, APPROVAL, LLM).
conditionExpr. Single string predicate. (Migrated from the old condition: { expr: "..." } shape, which is now flattened.)
- slug: review
kind: CREATE_TASK
config:
taskTemplateRef: invoice-review # Required — TaskTemplate slug (org-scoped)
taskStatusSlug: todo # Optional — defaults to template's initialStatusSlug
taskData: # Optional — overrides for the materialized task
title: "Review invoice {{ .inputs.documentId }}"
description: ""
priority: 2
properties: {}
waitForCompletion: true # Optional — default true; step blocks until task reaches a terminal status
- slug: extract
kind: EXECUTION
config:
moduleRef: "${orgSlug}/invoice-extractor" # Required — module URI or slug
options: # Optional — module-specific config
confidence_threshold: 0.85
bypass: false # Optional — skip execution and mark COMPLETED
perDocument: true # Optional — iterate over the activity's document family
maxParallel: 4 # Optional — parallelism cap when perDocument=true
joinPolicy: all_complete # Optional — all_complete | any_complete
- slug: post-to-erp
kind: BRIDGE_CALL
config:
serviceBridgeRef: "${orgSlug}/erp-bridge" # Required
endpointName: createInvoice # Required — named endpoint within the bridge
requestBody: # JSONata expressions or static body
number: "$.inputs.invoiceNumber"
amount: "$.context.extracted.total"
requestQuery: # Optional — query-string template
mode: "auto"
requestPath: # Optional — path-segment substitution
vendorId: "$.inputs.vendorId"
requestHeaders: # Optional
X-Idempotency-Key: "$.context.idempotencyKey"
requestScript: "" # Optional — GoJA preprocessor; mutex with the four request_* maps
treatAsError: '$.status >= "400"' # Optional — JSONata predicate that marks response as error
timeoutSeconds: 30
disableCache: false
bridgeActions: # Optional — action descriptors emitted by the call
- { uuid: "ok", name: "submitted" }
- slug: classify
kind: SCRIPT
config:
scriptBody: | # Required — JS code
const total = context.extracted.total;
action(total > 10000 ? 'high' : 'normal');
scriptActions: # Optional — declared action outcomes
- { uuid: "high", name: "high" }
- { uuid: "normal", name: "normal" }
scriptSidecars: # Optional — module refs whose JS is pre-loaded
- "${orgSlug}/script-helpers"
Migration note. Old
planTemplateitems used the keyscript:for the body. The activity-plan canonical key isscriptBody:— Step A migrations rename automatically; new authoring should usescriptBody.
scriptBodyThe script runs in a GoJA (Go-embedded JS) VM with these globals:
| Symbol | Purpose |
|---|---|
context | Activity context (context.extracted, context.params, ...) |
families | Document families attached to the activity (array) |
loadDocument(familyId) → Document | Materialise a document for navigation/mutation |
serviceBridge.call(ref, op, body) | Synchronous bridge call. ref = '${org}/<bridge-slug>' |
log.info / .warn / .error / .debug | Structured logging — surfaces in the run log |
action('outcome') | Declare the chosen scriptActions outcome (drives dependsOn) |
The Document exposes navigation + mutation methods that are thin wrappers
over the Go bindings in kodexa-document/lib/go/pkg/scripting/:
doc.findFirstDataObjectByPath(path) — fetch the first data object whose
data path matches.doc.getOrCreate(path) — fetch-or-create at a top-level path.obj.getOrCreateChild(path) — fetch-or-create a child data object.obj.setAttribute(name, value) / obj.addAttribute({...}) — write
values.obj.getFirstAttributeValue(name) / obj.getAttributeByName(name) —
read values.The Go bindings validate every path argument against the document's
taxonomies by exact match on the taxon's Path field
(Taxonomy.FindTaxonByPath in kodexa-document/lib/go/internal/domain/document/taxonomy.go).
The taxon's Path is the full slash-separated hierarchy, e.g.
BankStatement/borrower_match.
This means:
// ❌ WRONG — validator looks for a taxon with Path == "borrower_match"
// (does not exist) and panics:
// GoError: taxon path "borrower_match" does not exist in taxonomy "..."
const bm = bs.getOrCreateChild('borrower_match');
// ✅ RIGHT — full path matches BankStatement/borrower_match in the taxonomy
const bm = bs.getOrCreateChild('BankStatement/borrower_match');
The same rule applies to doc.getOrCreate('BankStatement'),
obj.addChild({path: 'BankStatement/transactions'}), and any other
method that takes a path argument.
Exception — setAttribute and addAttribute combine the parent's
path with the attribute name internally, so they take a bare attribute
name:
bm.setAttribute('match_status', 'MATCHED');
// validator checks parent.path + "/" + name
// = "BankStatement/borrower_match" + "/" + "match_status"
// = "BankStatement/borrower_match/match_status" ✓
To debug a taxon path … does not exist error:
kdx run data-definitions list-taxonomies --filter "slug:'...'" -o json | jq '.. | objects | select(.path?)').path argument the script passes matches that
taxon's Path field byte-for-byte.name, externalName, label, and path. Only path
matters for these helpers — name and externalName are display
concerns.- slug: extract-fields
kind: LLM
config:
promptBody: "Extract invoice number and total from the document." # Either promptBody…
promptTemplateRef: "${orgSlug}/invoice-extract-prompt" # …or promptTemplateRef
llmModelName: claude-sonnet-4-6 # Optional — overrides default
enrichment: # Optional — service-bridge calls run before the prompt
- serviceBridgeRef: "${orgSlug}/vendor-lookup"
operation: lookupVendor
inputMapping: { vendorName: "$.inputs.vendor" }
outputKey: vendorRecord
includeDocument: true # Attach activity's document family content
outputMapping: # JSONata mapping LLM output → step outputs
invoiceNumber: "$.number"
amount: "$.total"
promptVariables: # JSONata expressions providing variables
vendor: "$.context.vendorRecord.name"
promptActions: # Optional — declared action outcomes for outputMapping
- { uuid: "ok", name: "extracted" }
Was named
AI_PROMPT/AIPLANNERin the old plan_template schema. The kind value is nowLLM; migrations remap automatically.
- slug: approval
kind: APPROVAL
config:
approverRole: finance-manager # Required — role slug
approvalCriteria: # Optional — predicate over the activity context
condition: "$.context.extracted.total > 10000"
Outcomes are written to the runtime approval_outcome column.
Provisional. Column wiring not yet finalized. Verify against the orchestrator's agent-step handler before authoring.
- slug: agent-review
kind: AGENT
config:
assistantRef: "${orgSlug}/review-agent" # Assistant slug, resolved within the project
agentInputs:
context: "$.inputs"
| Kind | Required config keys | Common optional keys |
|---|---|---|
CREATE_TASK | taskTemplateRef | taskStatusSlug, taskData, waitForCompletion |
EXECUTION | moduleRef | options, perDocument, maxParallel, joinPolicy, bypass |
BRIDGE_CALL | serviceBridgeRef, endpointName | requestBody, requestQuery, requestPath, requestHeaders, requestScript, treatAsError, bridgeActions, timeoutSeconds, disableCache |
SCRIPT | scriptBody | scriptActions, scriptSidecars |
LLM | promptBody or promptTemplateRef | llmModelName, enrichment, includeDocument, outputMapping, promptVariables, promptActions |
APPROVAL | approverRole | approvalCriteria |
AGENT (provisional) | assistantRef | agentInputs |
Old item_type | New kind |
|---|---|
TASK | CREATE_TASK |
EXECUTION (no service_bridge_ref) | EXECUTION |
EXECUTION (with service_bridge_ref) | BRIDGE_CALL |
BRIDGE_CALL | BRIDGE_CALL |
SCRIPT | SCRIPT |
AI_PROMPT / AIPLANNER | LLM |
AGENT | AGENT |
APPROVAL | APPROVAL |
slug: invoice-processing
name: "Invoice Processing Workflow"
organizationId: ${orgSlug}
description: "Extract invoice fields, run rules, optionally route to human review."
inputsSchema:
type: object
required: [documentId]
properties:
documentId: { type: string }
priority: { type: string, enum: [low, medium, high] }
defaultTitleTemplate: "Process invoice {{ .inputs.documentId }}"
defaultDescriptionTemplate: "Automated processing of {{ .inputs.documentId }}"
steps:
- slug: extract
kind: EXECUTION
config:
moduleRef: "${orgSlug}/invoice-extractor"
options:
confidence_threshold: 0.85
- slug: classify
kind: SCRIPT
dependsOn: [extract]
config:
scriptBody: |
const total = context.extracted.total;
action(total > 10000 ? 'high' : 'normal');
scriptActions:
- { uuid: "high", name: "high" }
- { uuid: "normal", name: "normal" }
- slug: post-to-erp
kind: BRIDGE_CALL
dependsOn: ["classify:normal"]
config:
serviceBridgeRef: "${orgSlug}/erp-bridge"
endpointName: createInvoice
requestBody:
number: "$.context.extracted.number"
amount: "$.context.extracted.total"
treatAsError: '$.status >= "400"'
- slug: review
kind: CREATE_TASK
dependsOn: ["classify:high"]
config:
taskTemplateRef: invoice-review
taskStatusSlug: todo
waitForCompletion: true
taskData:
title: "High-value invoice — {{ .inputs.documentId }}"
- slug: post-after-review
kind: BRIDGE_CALL
dependsOn: [review]
config:
serviceBridgeRef: "${orgSlug}/erp-bridge"
endpointName: createInvoice
requestBody:
number: "$.context.extracted.number"
amount: "$.context.extracted.total"
metadata:
description: "Auto-process invoices; route high-value to human review."
icon: "file-invoice"
Activity plans don't run on their own. They are launched by:
Trigger (project-scoped) firing on a project event — see the project-template skill (Triggers section).CREATE_TASK step in another activity plan, when its child task is configured to spawn one.POST /api/activities with the plan slug.activityPlanId from its GoJA script (replaces old taskTemplateId).For an ActivityPlan to be usable in a project, its URI must be bound in kdxa_project_resources. Three ways to bind:
If a project tries to start an ActivityPlan that isn't bound, the orchestrator returns 422 Unprocessable Entity.
| Mistake | Fix |
|---|---|
Using old kind values (TASK, AI_PROMPT, AIPLANNER) | Use CREATE_TASK, LLM. |
Putting plan steps inside a task-template's planTemplate: | Move to a separate activity-plan resource and reference via taskTemplateRef/Trigger. |
Using script: instead of scriptBody: in a SCRIPT step | Authoritative key is scriptBody. |
condition: { expr: "..." } on a step | Flatten to a top-level conditionExpr: "..." string. |
dependencies: on a step | Source key is dependsOn — dependencies is silently ignored. |
EXECUTION step with serviceBridgeRef set | Use kind: BRIDGE_CALL instead. EXECUTION is module-only. |
| Authoring an activity plan directly under a project | Plans are org-scoped. Bind to projects via project resources. |
| Starting an unbound plan in a project | Bind the plan to the project first (UI / template / kdx). The orchestrator returns 422 otherwise. |
Forgetting waitForCompletion on a CREATE_TASK you intend to gate on | Default is true, but be explicit when the next step depends on the task's outcome. |
npx claudepluginhub kodexa-ai/kodexa-metadata-skills --plugin kodexa-metadata-skillsProvides CDSS development patterns for drug interaction checking, dose validation, clinical scoring (NEWS2, qSOFA), and alert classification integrated into EMR workflows.