Skip to main content

Workflow building blocks

Every workflow, from a two-node hello world to a full drift-detection suite, is assembled from the same small set of pieces. This section covers what each piece does and when to use it.

How a workflow is put together

A workflow is a graph of connected steps. Each step runs a plugin: query the data lake, call a nullplatform API, run code, send a Slack message, or hand a task to AI. Data flows between steps as items (plain JSON objects), and expressions wire one step's output into the next step's input. A step that receives several items usually runs once per item; that's called fan-out.

Steps come in three kinds. Triggers start a run and feed it inputs. Modules do the work. Deciders look at the data and route it to one of their output ports, which is how a workflow branches.

Two rules apply to every workflow:

  • Saving is always safe. Every save creates a new immutable version. Nothing runs, nothing gets registered.
  • Activation is the moment side effects begin. Activating a workflow registers its triggers: schedules start firing, webhook URLs go live, event subscriptions start listening. Deactivating removes them all.

Example: a scan-and-track workflow

Here's a representative scan-and-track workflow with all three step kinds. Click a node to see the YAML behind it:

Nightly schedule
Find stale scopes
Detect offenders
Anything found?
Open action items
Log summary

In the definition, steps declare what they run and connections declare how data flows between them:

name: "Stale scope scan"
steps:
- id: start_cron # trigger: starts the run
type: trigger
plugin_type: cron
config:
schedule: "0 6 * * *"

- id: lake_query # module: does the work
type: module
plugin_type: np-lake-query
config:
apiKey: "${{ secrets.NP_API_KEY }}" # credentials come from config entries
sql: "SELECT ..."

- id: anything_found # decider: routes to output ports
type: decider
plugin_type: conditional
config:
expression: "steps.detect.outputs.total > 0"

connections:
- { from: start_cron, to: lake_query }
- { from: anything_found, to: open_items, source_port: "true" }
- { from: anything_found, to: log_summary, source_port: "false" }

Notice what's not in the definition: no credentials, no organization ids. Everything tenant-specific comes from secrets and variables, so the same definition runs unchanged in any organization.

You rarely write this YAML from scratch. The visual editor (Platform Settings → Workflow Editor) and the definition are two views of the same thing: build on the canvas and the YAML updates, paste YAML and the canvas redraws. Describing the workflow in plain language, as in Your first workflow, generates it for you.

Next steps