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.
Listen to an action
- 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.specificationandaction: 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 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:
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:
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:
- 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. Default500withexecution_failed.on_timeout: how long the trigger waits (timeout, default15s) and what it answers then (default504). 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:
- 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.
Next steps
- Triggers: everything else that can start a workflow
- Secrets and variables: store the API key the trigger uses
- Runs and versions: activation and aliases