Skip to main content
Org-scoped webhooks let you subscribe to workflow and workflow step lifecycle events for your Nuon Org. Whenever a workflow runs (for example, an Install provision, a Sandbox reprovision, or a component deploy), Nuon POSTs a CloudEvents v1.0 envelope to every webhook URL registered on the Org. Webhooks are scoped to the current Org. Manage them with the Nuon CLI or the dashboard.

Manage webhooks

A few constraints to be aware of:
  • The --url must be an absolute http or https URL with a host.
  • Webhook URLs are unique per Org per scope. The uniqueness key is (org_id, webhook_url, match) — the same URL can be registered multiple times in one Org as long as each registration uses a different match predicate (for example, once for “all installs” and again for “components in env=prod”). Registering the exact same (url, match) twice returns a conflict.
  • The --secret is write-only. The API never returns it; responses include has_secret: true|false so you can tell whether one is configured.
  • The webhook URL is part of the (org_id, webhook_url, match) unique index and cannot be changed in place. To rename it, delete the webhook and create a new one. Use the dashboard’s edit action (or PATCH /v1/orgs/current/webhooks/{webhook_id}) to rotate the signing secret or replace the interests filter / match predicate without recreating the webhook.

Filtering events with interests

By default a new webhook receives every supported workflow, deploy, sandbox, runner, and approval event for the Org. To narrow that down, attach an interests filter on create or update. The filter is a structured object stored on each webhook and persisted as JSONB on the webhook row. If you omit interests entirely (and on legacy webhooks created before this filter shipped), Nuon treats the configuration as {"all_events": true} so deliveries don’t silently stop. The list endpoint surfaces this same effective shape so the CLI/dashboard/SDK stay in sync with what’s actually delivered.

Top-level shape

A configuration is one of two shapes:
all_events: true is the new-subscription default and short-circuits everything else. To opt into a per-resource configuration, omit all_events (or set it to false) and populate resources. A resource missing from resources is off — no events of that kind are delivered.

Resource vocabulary

Each resource accepts a list of sub-ops drawn from a fixed vocabulary: Drift workflow lifecycle events (drift_run, drift_run_reprovision_sandbox) are intentionally not part of the ops vocabulary — every cron tick fires a started/completed pair, which is pure noise on a clean scan. To subscribe to drift, set drift_detected: true on components and/or sandboxes (see below).

Per-resource fields

outcome semantics:
  • all — every started + terminal event for the matched ops.
  • completion — terminal events only (succeeded, failed, cancelled); suppress started.
  • failures — only failed and cancelled terminal events.
  • none — mutes lifecycle events entirely for this resource. Drift and approval events are gated independently and remain functional, so none is the right choice when you want a drift-only or approvals-only subscription for this resource.
Approval events do not have a started/succeeded/failed lifecycle, only a requested / approved / rejected handshake. They are gated independently of outcome by the approval_requests and approval_responses booleans.

Drift detection

To receive drift notifications, set drift_detected: true on components and/or sandboxes. The notification fires once per component (inside drift_run) or once per sandbox (inside drift_run_reprovision_sandbox) whenever the plan-only check finds non-no-op changes. It works for both manually triggered and scheduled drift scans, and is gated independently of outcome — the same way approval_requests / approval_responses are.

Runner inactivity

Runners emit a runners.inactive lifecycle event when the control plane gives up on a runner-process — currently this fires after roughly 5 minutes of silence on the heartbeat channel for that process. It does not distinguish a true outage from routine churn (process restart, version upgrade, deliberate shutdown), so subscribers should expect to see it on normal operations as well. Subscribe by including inactive in the runners ops list (or by leaving ops empty to receive every runner sub-op).

Examples

Subscribe to every supported event (this is the default if interests is omitted):
Per-resource opt-in matching the dashboard’s “opted-out-of-AllEvents” baseline — terminal events plus approval notifications for the four most common resources:
Narrowly scoped — only component deploy failures:
Drift only — components, no lifecycle, no approvals:

Setting interests via the CLI

nuon orgs webhooks create and nuon orgs webhooks update take the full subscription — both the interests filter and the optional match predicate — as a single JSON document via --subscription-json (inline) or --subscription-file (path on disk). The two flags are mutually exclusive. Omit both to fall back to the default: every supported event in the Org, no scope.
The wire shape is {"interests": {...}, "match": {...}} — both keys are optional. A missing interests defaults to {"all_events": true} so you can scope a webhook with --subscription-json '{"match":{...}}' without restating the events filter. A missing or null match is the org-wide (“every entity”) subscription. update replaces both interests and match wholesale — it is not a deep merge. Pass --secret <new> to rotate the signing secret at the same time; omit --secret to leave the existing secret unchanged. The webhook URL plus its match predicate together form the (org_id, webhook_url, match) unique index — delete and recreate to rename, or register a sibling row with a different match to deliver the same URL with a different scope.

Setting interests via the dashboard

The dashboard’s webhook create + edit forms expose the interests filter as an inline picker. Toggling Send all events off materializes the per-resource baseline shown above so you land on a sensible starting point instead of an empty config that silently drops every event. Saving the form persists the same JSON shape documented here.
Match in the dashboard. The dashboard’s webhook forms do not yet expose the match predicate, and saving from the dashboard resets the webhook to org-wide. To create or preserve a scoped webhook, manage it from the CLI.

Scoping deliveries with match

By default a webhook is org-wide — it fires for every event the interests filter opts into, regardless of which install, component, or action triggered it. Attach a match predicate to scope deliveries to specific entities or to entities carrying specific labels. match is an object with up to three optional kinds — installs, components, actions. For each kind, list the ids you care about and/or a label selector:
How matching works:
  • An event is delivered if any populated kind matches.
  • Within a kind, the entity matches if its ID is in ids or its labels satisfy selector.
  • A selector requires every match_labels entry to match (AND); use "*" as the value to require only that the key is present.
  • An empty filter ({}) for a kind means “any entity of this kind”.
  • Omit match entirely (or send null) for the org-wide default.

Examples

Every event, but only for two specific installs:
Drift-only on components labelled env=prod:
The same URL can be registered multiple times in one Org with different match predicates — for example, a noisy “all events” subscription to a staging endpoint plus a focused “drift only on env=prod components” subscription to a paging endpoint.

Two primitives: workflows and workflow steps

The webhook surface exposes exactly two primitives:
  1. Workflow lifecycle (com.nuon.workflow.lifecycle.v1) — fires for the workflow as a whole. Carries the workflow’s type (e.g. provision, reprovision, manual_deploy, action_workflow_run) and its owner (installs, apps, app_branches).
  2. Workflow step lifecycle (com.nuon.workflow_step.lifecycle.v1) — fires for each step within a workflow. Carries the step’s name, idx, and target_type (e.g. install_deploys, install_sandbox_runs, install_action_workflow_runs) along with denormalized component_id / sandbox_id when applicable.
You do not need to memorize an operation taxonomy. To know “did the sandbox finish provisioning?”, look at workflow events with data.workflow.type == "provision" and a step whose target_type == "install_sandbox_runs" succeeded. To know “did this component deploy?”, look at a step whose target_type == "install_deploys" and inspect data.step.component_id.

Transitions

Both primitives use the same transition vocabulary: data.transition is the transition name. data.outcome.status mirrors it on *.succeeded, *.failed, and *.cancelled events. Validation failures of the workflow / step wrappers surface as a failed outcome on the following execute event — there is no separate validate event. A typical Install provision produces, in order:
  1. workflow.lifecycle started for the provision workflow.
  2. For each step in the workflow: workflow_step.lifecycle startedworkflow_step.lifecycle succeeded (or failed / cancelled).
  3. workflow.lifecycle succeeded for the provision workflow once every step terminates.

Delivery guarantees and failures

Nuon webhooks are best-effort notifications, not a durable event stream.
  • A final 2xx response is required for a delivery to succeed.
  • Nuon waits up to 5 seconds for a response. Endpoints should acknowledge promptly and perform long-running processing asynchronously.
  • Network errors, timeouts, and non-2xx responses are treated as delivery failures. Nuon does not retry these failures or provide delivery history or replay.
  • A failed webhook does not interrupt the workflow that produced the event or prevent delivery attempts to other webhook subscriptions.
  • Rare infrastructure interruptions can result in duplicate deliveries when Nuon cannot determine whether an earlier request completed. A duplicate logical event may have a different CloudEvents id.
Make webhook handlers idempotent. Persist or enqueue the event before returning a 2xx response, and derive an idempotency key from the relevant workflow, step, resource, and transition fields rather than relying only on the CloudEvents id.

Payload format

The body is a CloudEvents v1.0 JSON envelope sent with Content-Type: application/cloudevents+json; charset=utf-8. Nuon-specific extension attributes (nuonorgid, nuonkind, nuontransition) are mirrored on the envelope for routing.

Workflow lifecycle event

Workflow step lifecycle event

Nested workflows: parent

When a workflow is launched from another workflow’s step (for example, an action workflow run launched from a deploy step), the child workflow’s events include a data.parent block:
parent is omitted for top-level workflows.

Field reference

Verifying signatures

When a webhook is created with --secret, Nuon signs the raw request body with HMAC-SHA256 using your secret and sends the lowercase hex digest in the X-Nuon-Signature header. Reject any request whose signature does not match. When no secret is configured, no signature header is sent.

End-to-end example

  1. Start your receiver locally and expose it (for example with ngrok http 8080).
  2. Register the webhook with your Org:
  3. Trigger a workflow. The smallest reliable trigger is reprovisioning a Sandbox:
  4. Your receiver should record one workflow.lifecycle.started, a sequence of workflow_step.lifecycle.* deliveries (one started + one terminal per step), and finally a workflow.lifecycle.succeeded (or failed / cancelled). A 401 response from your receiver, such as when the secrets do not match, is a delivery failure. See Delivery guarantees and failures.
  5. When you are done, remove the webhook: