> ## Documentation Index
> Fetch the complete documentation index at: https://developer.kodexa.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Script Steps in Activity Plans

> Use SCRIPT steps in Activity Plans to run JavaScript for routing, enrichment, and document logic that is too specific for declarative steps.

A `SCRIPT` step runs JavaScript inside an Activity. It is the right tool when the workflow needs custom logic that is too specific for a declarative step.

Script steps are commonly used to inspect documents, normalize extracted data, assign knowledge features, call Service Bridges, and decide which branch of the Activity Plan should run next.

## When to Use a SCRIPT Step

Use a `SCRIPT` step when you need to:

* Route the Activity based on document content, metadata, extraction results, or task status
* Make several checks before deciding whether human review is needed
* Update document metadata, labels, tags, data objects, or attributes
* Assign knowledge features to document families
* Call one or more Service Bridges and combine their responses
* Emit a named action for downstream dependencies

Do not use a script just to make one external API call. Use a [Service Bridge step](/guides/activity-plans/service-bridge-steps) for that so the external call has its own step status, logs, retry behavior, and result.

## Step Configuration

```json theme={null}
{
  "slug": "route-invoice",
  "kind": "SCRIPT",
  "dependsOn": ["extract"],
  "config": {
    "scriptActions": [
      { "name": "review" },
      { "name": "post" },
      { "name": "reject" }
    ],
    "scriptSidecars": ["acme-finance/invoice-script-helpers"],
    "scriptBody": "return { action: 'review' };"
  }
}
```

| Config key       | Description                                          |
| ---------------- | ---------------------------------------------------- |
| `scriptBody`     | Inline JavaScript source                             |
| `scriptActions`  | Named actions the script can return                  |
| `scriptSidecars` | JavaScript module refs loaded before the script runs |

The script must return an object with an `action` property when downstream routing depends on the step.

```javascript theme={null}
return { action: "review" };
```

Downstream steps can depend on that action:

```json theme={null}
{
  "slug": "analyst-review",
  "kind": "CREATE_TASK",
  "dependsOn": ["route-invoice:review"]
}
```

## Return Value

The full return value can carry three optional pieces alongside `action`:

```javascript theme={null}
return {
  action: "approve",
  features: [
    { documentFamilyId: families[0].id, featureId: "fc_billing_ready" }
  ],
  nextActivity: {
    activityPlanRef: "activity-plan://acme-finance/billing-extraction",
    inputs: { reviewedBy: org.userEmail }
  }
};
```

| Field          | Description                                                                                |
| -------------- | ------------------------------------------------------------------------------------------ |
| `action`       | Required. Must match one of the `scriptActions` names declared on the step.                |
| `features`     | Optional. Attaches knowledge features to document families on the current Activity.        |
| `nextActivity` | Optional. Requests a follow-up Activity Plan to start when the current Activity completes. |

`features` and `nextActivity` are independent — return either, both, or neither.

## Spawning a Follow-Up Activity

A SCRIPT step can request another Activity Plan to start automatically when the current Activity completes. Use this to chain related work, e.g. "after intake classifies an invoice, kick off the extraction plan."

The spawned Activity:

* Starts only after the **current Activity** reaches `COMPLETED` (not on individual step completion).
* Inherits the current Activity's project. The target plan must already be bound to that project.
* Inherits the current Activity's document families when `documentFamilyIds` is omitted.
* Records its source in `triggerMetadata` (`sourceActivityId`, `sourceStepId`, `sourceActionUuid`, `sourceProjectId`) so audit trails work in both directions.
* Has `triggerKind` set to `ACTIVITY_COMPLETED`.

### nextActivity shape

```javascript theme={null}
return {
  action: "approve",
  nextActivity: {
    activityPlanRef: "activity-plan://acme-finance/billing-extraction",
    inputs: { reviewedBy: org.userEmail, source: "intake" },
    title: "Billing for " + families[0].name,
    description: "Auto-spawned from invoice intake",
    documentFamilyIds: [families[0].id],
    features: [
      { documentFamilyId: families[0].id, featureId: "fc_billing_ready" }
    ],
    priority: 5,
    triggerMetadata: { batchId: task.data.batchId }
  }
};
```

| Field               | Required | Description                                                                                                                   |
| ------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `activityPlanRef`   | Yes      | Slug or `activity-plan://orgSlug/planSlug` of the plan to start. Must be bound to the current project.                        |
| `inputs`            | No       | Object validated against the target plan's `inputOptions` at spawn time.                                                      |
| `title`             | No       | Override the spawned Activity's title. Defaults to the plan's `defaultTitleTemplate`.                                         |
| `description`       | No       | Override the description. Defaults to the plan's `defaultDescriptionTemplate`.                                                |
| `documentFamilyIds` | No       | Document families to link to the spawned Activity. Defaults to inheriting the current Activity's families.                    |
| `features`          | No       | Knowledge features to attach to document families *before* the spawn fires, so the new plan's templates and scripts see them. |
| `priority`          | No       | Priority for the spawned Activity.                                                                                            |
| `triggerMetadata`   | No       | Free-form metadata. Server-controlled fields (`sourceActivityId`, etc.) always win over anything you set here.                |

### Failure handling

Spawn failures are **soft**. If the target plan doesn't exist, FGAC denies, or inputs fail validation, the source Activity still finishes cleanly. The failure is recorded on the source step in `script_result.nextActivityError`. On success, `script_result.nextActivityId` is set to the new Activity's ID.

### Multiple spawns

Each SCRIPT step in a plan may emit its own `nextActivity`. They fan out at the source Activity's completion in step insertion order. Within a single script return, only one `nextActivity` is supported.

### Same-project only

A script can only spawn Activity Plans bound to the **current project**. Cross-project spawns are rejected. If you need fan-out across projects, model it through a Service Bridge call instead.

## Activity SCRIPT Runtime

Activity Plan `SCRIPT` steps run in the server-side JavaScript runtime. The runtime exposes the same business objects the Activity is working on, plus helper namespaces for task state, document families, document content, Service Bridges, LLM calls, and knowledge features.

| Object                                                | Use                                                                                                                                                                                                                                  |
| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `task`                                                | Snapshot of the Task that started the Activity, when the Activity is task-backed                                                                                                                                                     |
| `families`                                            | Snapshot array of document families attached to the Task                                                                                                                                                                             |
| `org`                                                 | Current organization `{ id, slug }`                                                                                                                                                                                                  |
| `inputs`                                              | The Activity's materialized inputs object — the same values `BRIDGE_CALL` `requestBody` templates resolve against. Defaults to `{}` when the Activity has no inputs, so you can read `inputs.foo` directly without a `typeof` guard. |
| `tasks`                                               | Query and mutate task records in the current project                                                                                                                                                                                 |
| `documents`                                           | Query and mutate document family records in the current project                                                                                                                                                                      |
| `loadDocument(familyId)` / `documents.load(familyId)` | Load a KDDB document for content nodes, tags, data objects, metadata, labels, validations, and exceptions                                                                                                                            |
| `lookupFeature*`                                      | Find or create knowledge features for classification and routing                                                                                                                                                                     |
| `serviceBridge`                                       | Call project-scoped Service Bridges from custom decision logic                                                                                                                                                                       |
| `llm`                                                 | Invoke configured LLM services with platform cost tracking                                                                                                                                                                           |
| `log` / `console`                                     | Write script logs visible from the Activity step details                                                                                                                                                                             |

The current Activity Plan script context is centered on the Task and its document families. The Activity's materialized inputs are also available through the read-only `inputs` global, which defaults to `{}` when the Activity has no inputs — so you can read values such as `inputs.invoiceNumber`, `inputs.routeId`, or `inputs.correlationId` directly. This `inputs` global is a snapshot for reading and is distinct from the `nextActivity.inputs` spawn payload above. For prior step outputs, use declarative step mappings and pass the needed values into document metadata, task data, or downstream step configuration.

### Runtime Limits

| Limit                                      | Value                  |
| ------------------------------------------ | ---------------------- |
| Script timeout                             | 60 seconds             |
| Document loads                             | 5 per script execution |
| Task reads / document family reads         | 50 per namespace       |
| Task mutations / document family mutations | 20 per namespace       |
| Task/document mutation payload             | 256 KB per call        |

The 60 second script timeout is the maximum wall-clock time the `SCRIPT` body's JavaScript may run before it is aborted. Other step kinds use their own distinct timeouts: `BRIDGE_CALL` steps default to a 30 second HTTP timeout (author-overridable via the step's `timeout`, capped at 120 seconds), and `AI_PROMPT` steps allow up to 120 seconds for the LLM call.

Document changes are saved after the script completes. Modified KDDB documents are persisted as new content object versions with the `SCRIPT_MODIFICATION` transition type.

### Task Namespace

Use `tasks` when a script needs to inspect or update human review work inside the current project.

| Method                                        | Description                                                                        |
| --------------------------------------------- | ---------------------------------------------------------------------------------- |
| `tasks.current()`                             | Load the current Task                                                              |
| `tasks.parent()`                              | Load the current Task's parent, if any                                             |
| `tasks.subTasks()`                            | List subtasks of the current Task                                                  |
| `tasks.subTasksOf(taskId)`                    | List subtasks for another Task in the same project                                 |
| `tasks.get(taskId)`                           | Load a Task by ID                                                                  |
| `tasks.query(filter)`                         | Query Tasks by `status`, `statusType`, `parentTaskId`, `templateSlug`, and `limit` |
| `tasks.lookupStatus(slug)`                    | Resolve a project Task status                                                      |
| `tasks.listStatuses()`                        | List project Task statuses                                                         |
| `tasks.setStatus(taskId, statusSlug)`         | Change Task status and trigger Activity advancement when the status type is `DONE` |
| `tasks.setProperties(taskId, props)`          | Merge properties into Task data                                                    |
| `tasks.setMetadata(taskId, metadata)`         | Merge Task metadata                                                                |
| `tasks.setTitle(taskId, title)`               | Update Task title                                                                  |
| `tasks.setDescription(taskId, description)`   | Update Task description                                                            |
| `tasks.lock(taskId)` / `tasks.unlock(taskId)` | Lock or unlock a Task                                                              |

### Document Family Namespace

Use `documents` when a script needs to manage document family state without loading the full KDDB document.

| Method                                                                                      | Description                                                                                                             |
| ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `documents.taskFamilies()`                                                                  | List document families attached to the current Task                                                                     |
| `documents.get(familyId)`                                                                   | Load a document family summary                                                                                          |
| `documents.getByPath(storeSlug, path)`                                                      | Find a document family by store and path                                                                                |
| `documents.query(filter)`                                                                   | Query families by `storeSlug`, `path`, `pathContains`, `label`, `mixin`, `status`, `featureSlug`, `locked`, and `limit` |
| `documents.contentObjects(familyId)`                                                        | List content object versions                                                                                            |
| `documents.latestContentObject(familyId, contentType?)`                                     | Get the latest content object, optionally `DOCUMENT` or `NATIVE`                                                        |
| `documents.load(familyId)`                                                                  | Load the KDDB document, equivalent to `loadDocument(familyId)`                                                          |
| `documents.lookupStatus(slug)` / `documents.listStatuses()`                                 | Resolve document statuses                                                                                               |
| `documents.setMetadata(familyId, metadata)`                                                 | Merge metadata into the family                                                                                          |
| `documents.addLabel(familyId, label)` / `documents.removeLabel(familyId, label)`            | Manage family labels                                                                                                    |
| `documents.setStatus(familyId, statusSlug)`                                                 | Update document status                                                                                                  |
| `documents.lock(familyId)` / `documents.unlock(familyId)`                                   | Lock or unlock the family                                                                                               |
| `documents.attachToTask(familyId, taskId?)` / `documents.detachFromTask(familyId, taskId?)` | Manage Task-family links                                                                                                |

## Routing Example

This script loads the first Task document, inspects its text, and routes the Activity to review, rejection, or straight-through posting.

```javascript theme={null}
var family = families[0];
var doc = loadDocument(family.id);
var text = doc.getRootNode().getAllContent(" ", true).toLowerCase();

if (text.indexOf("duplicate invoice") !== -1) {
  log.warn("Duplicate invoice language detected");
  return { action: "reject" };
}

var invoice = doc.findFirstDataObjectByPath("invoice");
var amount = invoice ? Number(invoice.getFirstAttributeValue("total_amount")) : 0;

if (!amount || amount > 10000) {
  return { action: "review" };
}

return { action: "post" };
```

## Updating Documents

Document changes made by a script are persisted after the step completes.

```javascript theme={null}
var family = families[0];
var doc = loadDocument(family.id);
doc.setMetadata("scriptStep", "tag-total-lines");
doc.addLabel("invoice-intake");

var root = doc.getRootNode();
var matches = root.select("//line[contains(@content, 'TOTAL')]");

for (var i = 0; i < matches.length; i++) {
  matches[i].tag("invoice/total-line", {
    confidence: 0.95,
    value: matches[i].getContent()
  });
}

return { action: "tagged" };
```

<Warning>
  Do not call `doc.close()` in Activity Plan scripts. Kodexa persists document changes after the script completes.
</Warning>

## Calling Service Bridges from a Script

Use `serviceBridge.call()` inside a SCRIPT step when the decision requires more than a single declarative bridge call.

```javascript theme={null}
var supplier = serviceBridge.call(
  "finance-reference-data",
  "lookup-supplier",
  { supplierId: task.data.supplierId }
);

if (!supplier || supplier.status !== "active") {
  log.warn("Supplier is not active:", task.data.supplierId);
  return { action: "review" };
}

var duplicate = serviceBridge.call(
  "finance-reference-data",
  "check-duplicate-invoice",
  {
    supplierId: task.data.supplierId,
    invoiceNumber: task.data.invoiceNumber
  }
);

return { action: duplicate && duplicate.found ? "review" : "post" };
```

Service bridge credentials stay in the platform. Scripts reference the configured bridge and endpoint; they do not handle secrets directly.

## Shared Script Sidecars

Use `scriptSidecars` to pre-load reusable JavaScript helpers.

```json theme={null}
{
  "slug": "normalize-fields",
  "kind": "SCRIPT",
  "config": {
    "scriptSidecars": ["acme-finance/invoice-script-helpers"],
    "scriptActions": [{ "name": "normalized" }],
    "scriptBody": "normalizeInvoiceFields(families[0].id); return { action: 'normalized' };"
  }
}
```

Sidecars are useful for shared validation functions, string cleanup, request mapping, and common routing decisions. Keep sidecars small and version them like any other project resource.

## Design Guidance

* Declare every returned action in `scriptActions`.
* Keep scripts focused on one decision or one transformation.
* Prefer `BRIDGE_CALL` for a single external API call that operators should see as its own step.
* Use logs at important decision points.
* Make scripts idempotent when they can be retried.
* Avoid long loops and repeated document loads.

## Next Steps

<CardGroup cols={2}>
  <Card title="Script API Reference" icon="code" href="/guides/script-steps">
    See the full JavaScript context, document helpers, LLM calls, and logging reference.
  </Card>

  <Card title="Service Bridge Steps" icon="bridge" href="/guides/activity-plans/service-bridge-steps">
    Learn when to model an external API call as its own Activity Plan step.
  </Card>
</CardGroup>
