---
sidebar_label: Nodes
toc_max_heading_level: 3
doc_id: c6c89044-1e86-4ebf-9513-03130fc1458b
description: >-
  The units of work inside a nullplatform workflow: platform operations, data
  transforms, control flow, waits, and external integrations.
keywords:
  - workflows
  - nodes
  - steps
  - plugins
  - control flow
  - integrations
  - nullplatform
---

# Nodes

Nodes are the steps that do the work between the trigger and the result. Each node runs a plugin. The node library in the editor is the complete, up-to-date reference; this page covers how a node is configured and the families you'll use most.

## How a node is configured

Every node has the same fields: an `id`, a `type`, the `plugin_type` it runs, `inputs` wired with [expressions](/docs/workflows/building-blocks/expressions), and a plugin-specific `config`. Retries are declared per node under `error_handling`:

```yaml
- id: create_item
  type: module
  plugin_type: np-action-item-create
  error_handling:
    retry_policy:
      max_attempts: 3
      initial_interval: "2s"
      backoff_strategy: exponential
      jitter: 0.3
  config:
    apiKey: "${{ secrets.NP_API_KEY }}"
    categorySlug: "${{ workflow.inputs.category_slug }}"
    nrn: "${{ workflow.inputs.finding.scope_nrn }}"
    title: "Deprecated AMI on scope ${{ workflow.inputs.finding.scope_name }}"
    priority: medium
```

To run a node once per element of an array (one action item per finding, one API call per path), add a `forEach` block:

```yaml
- id: ensure_items
  type: module
  plugin_type: sub-workflow
  forEach:
    expression: "${{ steps.detect_drift.outputs.findings }}"
    itemVariable: finding
    parallel: true
    maxConcurrency: 5
  config:
    workflowId: wf_ensure_action_item
    waitForCompletion: true
```

Inside the node, each element is available as `inputs.finding`. Iterations retry independently, and in parallel mode a failed iteration doesn't stop the rest. [Loops and iteration](/docs/workflows/building-blocks/loops) covers the other ways to repeat work, and when each one fits.

## Nullplatform nodes

These nodes talk to your organization. The ones you'll use most:

- **`np-api-call`**: an authenticated request to any nullplatform REST endpoint, with optional pagination. Prefer a dedicated node when one exists for the operation.
- **`np-lake-query`**: read-only SQL against the [data lake](/docs/data-lake/). Each row becomes one item for downstream steps, and organization scoping is automatic.
- **Action items family**: create, find, update, comment on, and wait for [action items](/docs/action-items/), plus their suggestions. The find node searches by a metadata key, which is how a scanner avoids opening the same item twice.
- **Checklists and deployments**: create checklists and wait for their outcome, or wait for deployment follow-up actions like a traffic switch.
- **`np-agent-command`**: dispatch a command to a nullplatform agent and optionally wait for the result.

## Branch, wait, and call other workflows

Deciders branch the run. `conditional` evaluates a boolean expression and sends each item out its `true` or `false` port; `case` does the same with several ports. Write decider expressions without the `${{ }}` wrapper:

```yaml
- id: missing
  type: decider
  plugin_type: conditional
  config:
    expression: "variables.found.count == 0"
```

Four more nodes control the flow. `sub-workflow` calls another workflow by id, either waiting for it to finish or not. `split-in-batches` processes a large array in chunks through a loop. `delay` pauses for a fixed duration. `signal-wait` parks the run until an external signal or a timeout arrives; a parked run costs nothing while it waits and survives restarts, which is what makes waiting for Jira or for a person practical.

## Transform data with code and variables

The `code-exec` node runs inline JavaScript in isolation. The code receives the step's resolved `inputs` as read-only values, returns an object, and anything it prints with `log.*` lands in the run's log view. By default there's no `require`, no `fetch`, and no filesystem. If the step declares network hosts or npm libraries in its config, the engine runs it in an isolated runtime with exactly those capabilities. What goes into and comes out of the code is capped at 1 MB each way, tighter than any other step, so trim a large result before it reaches the code rather than inside it.

For simpler needs, `set-variable` writes per-run variables that later steps read as `${{ variables.* }}`, and `log` writes a message to the run's log and never fails.

## Call external systems

For systems outside nullplatform: `http-request` for any HTTP call, `paginated-fetch` for APIs that page their results, and `webhook-wait` to create a one-time callback URL and pause until it's called. Jira nodes create, find, and wait for issues to reach a status. Slack nodes post messages, and two of them bring people into the loop: `slack-ask` posts buttons and waits for a click, `slack-wait-message` waits for a reply in a thread.

All of them take their credentials from [secrets and variables](/docs/workflows/building-blocks/secrets-and-variables), never inline.

## Next steps

- [AI nodes](/docs/workflows/building-blocks/ai-nodes): agents as workflow steps
- [Expressions](/docs/workflows/building-blocks/expressions): wiring data between nodes
- [Items and data flow](/docs/workflows/building-blocks/items-and-data-flow): why a node runs once or once per item
- [Error handling](/docs/workflows/building-blocks/error-handling): what `error_handling` can do beyond retries
- [Limits](/docs/workflows/building-blocks/limits): what a step may return, how long it may run, and how to stay under both
- [Detect and roll out AMI updates](/docs/tutorials/ami-drift): lake queries, deciders, and sub-workflows in a real suite
