---
sidebar_label: Actions
toc_max_heading_level: 3
doc_id: 89955a24-2133-422b-a509-85764c2b43a1
description: >-
  Run a workflow when someone executes an action, and answer the caller with the
  result: right away for synchronous actions, or reported back when the run
  ends.
keywords:
  - workflows
  - actions
  - synchronous actions
  - np-action-trigger
  - catalog actions
  - nullplatform
title: Answer actions with a workflow
canonical: 'https://docs.nullplatform.com/docs/workflows/building-blocks/actions'
---
# Answer actions with a workflow

An action is something people run on a catalog entity, like `summarize` on a record. A workflow can be what runs it. The `np-action-trigger` receives the action, your steps do the work, and the result goes back to the action, either to a caller that is waiting for it or as a report when the run ends.

```mermaid
flowchart LR
    CALL["Caller runs the action"] --> API["Catalog API<br/>records the execution"]
    API -->|notification| TRIG["np-action-trigger"]
    TRIG --> STEPS["Your steps"]
    STEPS -->|"sync: HTTP answer"| API
    STEPS -.->|"async: PATCH callback_url"| API

    classDef np fill:#e6faf4,stroke:#00b894,color:#0b3d2e;
    classDef infra fill:#eef3fb,stroke:#274a86,color:#14243d;
    class TRIG,STEPS np;
    class CALL,API infra;
    linkStyle default stroke:#94a3b8,stroke-width:1.5px;
```

## Listen to an action

```yaml
- id: trigger
  type: trigger
  plugin_type: np-action-trigger
  name: "On summarize"
  config:
    source: catalog
    specification: my-entity-type
    action: summarize
    nrn: "organization=1255165411"
    npApiKey: "${{ secrets.np_api_token }}"
```

- `source`: `catalog`, for actions on catalog entities.
- `specification` and `action`: only actions with this action slug on entities of this specification start the workflow. Leave them out to listen to every action of the source.
- `nrn`: where the notification channel is created. Actions on entities below it reach the workflow.
- `npApiKey`: a [secret](/docs/workflows/building-blocks/secrets-and-variables) holding an API key, used to report results back.

On activation the trigger creates its own notification channel, with filters for the source, specification, and action. You don't create the channel by hand, and deactivating removes it. Activate with an identity that's allowed to manage notification channels.

A catalog action reaches the workflow only when its specification has an executor for that action with the `notification` transport. Without one, the catalog doesn't send the action anywhere.

### What the steps receive

| Output | What it holds |
|---|---|
| `action` | The execution: `id`, `slug`, `status`, `specification` |
| `entity` | The entity the action ran on: `id`, `specification`, and its attributes |
| `input` | What the caller sent, validated against the action's input schema |
| `nrn` | The entity's NRN |
| `user` | Who ran the action |
| `callback_url` | Where an async run reports back |

Read them as `${{ steps.trigger.outputs.entity }}`, `${{ steps.trigger.outputs.input }}`, and so on.

## Answer the caller

The action's definition decides whether the caller waits. A catalog action declared with `execution: sync` holds the request until someone answers; an action without it (the default, `async`) answers `202` right away and gets its result later. The trigger's `response.mode` matches that:

:::note

Only catalog actions can be synchronous. Service actions are always asynchronous, so a trigger with `source: service` uses `response.mode: immediate`.

:::

| `response.mode` | Use it for | What the caller gets |
|---|---|---|
| `immediate` (default) | Async actions | `202` at once. When the run ends, its outputs are reported to the action's `callback_url` |
| `last_node` | Sync actions | The workflow `outputs`, when the run ends |
| `respond_node` | Sync actions that can answer before the run ends | The `status` and `body` of the `np-action-respond` node you place |

A run that answers the caller doesn't report to `callback_url` as well: the API that held the request stores the answer as the action's output.

### Answer with the workflow outputs

With `last_node`, whatever the workflow declares in `outputs` is the answer:

```yaml
id: np_action_summarize
name: "Action: summarize"
path: "/actions/summarize"

steps:
  - id: trigger
    type: trigger
    plugin_type: np-action-trigger
    config:
      source: catalog
      specification: my-entity-type
      action: summarize
      nrn: "organization=1255165411"
      npApiKey: "${{ secrets.np_api_token }}"
      response:
        mode: last_node
        on_error:
          status: 500
          body:
            error: "${{ error.message }}"
        on_timeout:
          timeout: 15s
          status: 504

  - id: summarize
    type: module
    plugin_type: code-exec
    inputs:
      entity: "${{ steps.trigger.outputs.entity }}"
      input: "${{ steps.trigger.outputs.input }}"
    config:
      code: |
        return {
          entity_id: inputs.entity.id,
          greeting: "hello " + ((inputs.input && inputs.input.name) || "anonymous"),
        };

connections:
  - { id: c1, from: trigger, to: summarize }

outputs:
  entity_id: "${{ steps.summarize.outputs.entity_id }}"
  greeting: "${{ steps.summarize.outputs.greeting }}"
```

### Answer before the run ends

With `respond_node`, place an `np-action-respond` node where the answer is ready. The caller gets its `status` and `body`, and the run keeps going with the steps after it:

```yaml
- id: respond
  type: module
  plugin_type: np-action-respond
  config:
    status: 200
    body:
      id: "${{ steps.create.outputs.id }}"
      accepted: true
```

Only the first answer of a run counts; a second `np-action-respond` is ignored.

### Errors and timeouts

- `on_error`: what the caller gets when the run fails. Default `500` with `execution_failed`.
- `on_timeout`: how long the trigger waits (`timeout`, default `15s`) and what it answers then (default `504`). The run is not cancelled; it keeps going in the background.

The API that holds the request has its own limit, 20 seconds by default. Keep `on_timeout.timeout` below it, so the caller gets your timeout answer instead of a generic one.

### Report progress from an async run

An `immediate` run reports its outputs when it ends. For progress before that, or results halfway through, use `np-action-update`:

```yaml
- id: progress
  type: module
  plugin_type: np-action-update
  config:
    callback_url: "${{ steps.trigger.outputs.callback_url }}"
    target: catalog
    auth:
      bearer: "${{ secrets.np_api_token }}"
    status: in_progress
```

It updates the action's `status` and, when there is one, its `output`.

## Things to know

- **One run per execution.** The trigger keys each run on the action execution ID, so a redelivered notification doesn't start a second run.
- **Re-activating changes the webhook URL.** The trigger's notification channel follows it on its own, so don't point anything at the webhook URL by hand.
- **Answers must be JSON objects.** The calling API stores the answer as the action's output.
- **Keep the answer out of the workflow's storage.** Ephemeral runs work with any workflow, not only actions. With `execution: { mode: ephemeral }` the caller still gets the answer, but the run doesn't keep its inputs, step outputs, or output. See [Keep a run's data out of storage](/docs/workflows/building-blocks/runs-and-versions#keep-a-runs-data-out-of-storage).

## Next steps

- [Triggers](/docs/workflows/building-blocks/triggers): everything else that can start a workflow
- [Secrets and variables](/docs/workflows/building-blocks/secrets-and-variables): store the API key the trigger uses
- [Runs and versions](/docs/workflows/building-blocks/runs-and-versions): activation and aliases
