# Nullplatform docs > User guides, tutorials, and API reference for nullplatform. This file contains all documentation content in a single document following the llmstxt.org standard. ## Nullplatform Resource Name (NRN) URL: https://docs.nullplatform.com/docs/NRN An NRN is a hierarchical string that uniquely identifies any entity in nullplatform — similar to Amazon ARNs. NRNs encode an entity's position in the organizational hierarchy and appear throughout the platform in authorization grants, notification filters, provider configurations, and API responses. ## Format ``` organization=1:account=2:namespace=3:application=4:scope=5 ``` Each segment represents a level in the hierarchy: | Level | Example | Description | |---|---|---| | Organization | `organization=1` | Top-level tenant | | Account | `organization=1:account=2` | Business unit or team | | Namespace | `organization=1:account=2:namespace=3` | Logical grouping of applications | | Application | `…:namespace=3:application=4` | A single deployable application | | Scope | `…:application=4:scope=5` | A deployment target (environment) | You can use a partial NRN to reference any level. For example, `organization=1:account=2` targets the account and everything beneath it. ## Where NRNs appear - **Authorization grants** — scope permissions to specific levels in the hierarchy. - **Notification filters** — route events from specific resources. - **Provider configurations** — bind providers at any hierarchy level, with inheritance downward. - **Parameters** — attach configuration at any level; child entities inherit parent values. - **Banners** — target announcements to specific resources using NRNs. - **API responses** — most entities include an `nrn` field showing their position. ## NRN API (deprecated) {#nrn-api} :::warning Deprecation notice The NRN API (`/nrn/:id`) is a legacy hierarchical configuration store that may be removed in the future. Use [**Providers**](./providers/overview.md) for infrastructure configuration and [**Parameters**](./parameters/parameters.md) for application runtime settings. ::: The NRN API stored key-value configuration data keyed by NRN strings, with hierarchical merging and profile-based overrides. See the [NRN API reference](./providers/nrn-api.md) for details. --- ## Configure and manage action items URL: https://docs.nullplatform.com/docs/action-items/configure-and-manage At the organization level, you decide what kinds of action items developers can work with and how they move through their lifecycle. This page covers both paths: driving it from the **Platform Settings** UI, or automating it with the **np-governance** agent plugin. :::info You need an **Admin** or **Ops** role to access this section. ::: ## Manage with the np-governance agent plugin The [**np-governance**](https://github.com/nullplatform/ai-plugins/tree/main/marketplace/plugins/np-governance) plugin lets you manage action items, categories, and AI suggestions through natural-language prompts from your coding assistant. It wraps the same API endpoints used throughout this page, but handles authentication, pagination, and deduplication for you. To install it, follow [Set up the AI plugins](/docs/ai-ops/ai-plugins). For a step-by-step example of what you can do with the plugin, see [Detect stale applications with AI](/docs/tutorials/governance-detect-stale-apps): it scans an account for apps that haven't been deployed in over 90 days and creates tracked action items automatically. ## Managing categories Categories are the backbone of action items. They classify what each item tracks, how it appears in the UI, and what governance rules apply when developers triage it. From **Settings > Action Items > Categories**, you see every category available in your organization, the NRN it applies to, and its status: ### Create a category Click **New Category** and fill in the form: Most fields are self-explanatory. A few to note: | Field | Behavior | |---|---| | **Resource (NRN)** | Categories inherit down the NRN hierarchy, so a category defined at the organization level is available to every account and application below it. | | **Color** / **Icon** | Color takes a hex code, icon takes an [iconify](https://iconify.design) name. Both determine how items in the category render in the UI. | | **Unit name / symbol** | Defines what "value" means on items in this category, for example "Dollars" / "$" or "Hours" / "h". The unit combines with priority to compute each item's score. | | **Parent category** | Optional. Supports up to two levels of nesting. | The same action is available via a [POST request](/docs/api/action-item-category-create) to `/action_item_category`. ```bash curl -L -X POST 'https://api.nullplatform.com/action_item_category' \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer ' \ -d '{ "nrn": "organization=1:account=2", "name": "Cost optimization", "description": "Tracks opportunities to reduce cloud spending", "color": "#38A169", "icon": "mdi:cash", "unit_name": "Dollars", "unit_symbol": "$", "config": { "max_deferral_days": 60, "max_deferral_count": 2 } }' ``` ### Deferral limits Each category carries a configuration block with two limits on how items can be deferred: | Setting | Effect | |---|---| | **Max deferral days** | Limits how far into the future an item can be deferred. | | **Max deferral count** | Limits how many times an item can be deferred. | Approval gating for resolve, defer, and reject is no longer part of the category. It now lives in nullplatform's [approvals](/docs/approvals) engine, which gives you the same governance through policies and notification channels you already use for deployments and scopes. The next section walks through it. ### Category inheritance Categories follow the NRN hierarchy. A category defined at `organization=1` is available to every account, namespace, and application below it. If a more specific category with the same name exists at a lower level, the more specific one takes precedence. This means you can set an organization-wide policy once, then override it for a specific account or application when needed. ## Require approval to transition items Resolving, deferring, and rejecting an item are governed by nullplatform's [approvals](/docs/approvals) engine, the same one that gates deployments, scopes, and services. You create an **approval action** for the `action_item` entity, attach a **policy** that decides when a request needs review, and route notifications through your existing channels (Slack, a webhook, or the approval request page). ### Create the approval action The `action_item` entity supports three actions: `action_item:resolve`, `action_item:defer`, and `action_item:reject`. To require review before any item can be rejected at a given NRN, [create an approval action](/docs/approvals/actions): ```bash curl -L -X POST 'https://api.nullplatform.com/approval/action' \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer ' \ -d '{ "nrn": "organization=1:account=2", "entity": "action_item", "action": "action_item:reject", "on_policy_success": "approve", "on_policy_fail": "manual" }' ``` Unlike deployments or scopes, action item approvals don't take dimensions: scope them through the NRN alone. ### Add a policy A [policy](/docs/approvals/policies) decides when a request needs manual review. Action item requests expose the item's fields to policy conditions, including `action_item.priority`, `action_item.category`, and `action_item.status`, so you can auto-approve low-stakes transitions and reserve manual review for the rest. For example, this policy passes (auto-approves) only when the item isn't critical: ```json { "nrn": "organization=1:account=2", "name": "Manual review for rejecting critical items", "conditions": { "action_item.priority": { "$ne": "critical" } } } ``` With `on_policy_success: "approve"` and `on_policy_fail: "manual"`, rejecting a non-critical item goes through automatically, while rejecting a critical one waits for a reviewer. [Associate the policy with the action](/docs/approvals/associate-policy-with-action) to put it into effect. ## Managing action items From **Settings > Action Items > Items**, you see every item across the organization, not just the ones attached to a single application. The list view supports the same filters as the developer list, plus NRN filtering so you can slice by account, namespace, or application. ### Create an item manually Click **New Action Item** to create an item by hand. This is useful when you want to track improvements that aren't surfaced by an agent, such as manual audit findings or planned migrations. The equivalent API call is a [POST request](/docs/api/action-item-create) to `/action_item`. ```bash curl -L -X POST 'https://api.nullplatform.com/action_item' \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer ' \ -d '{ "nrn": "organization=1:account=2:namespace=3:application=4", "title": "Upgrade base image to fix CVE-2026-1234", "description": "The current base image has a critical vulnerability that allows remote code execution.", "category_slug": "security-vulnerabilities", "priority": "critical", "value": 500, "due_date": "2026-05-01T00:00:00Z", "labels": { "source": "container-scan", "cve": "CVE-2026-1234" } }' ``` ### Review pending requests When a developer triggers a transition that an approval action gates, the item lands in a `pending_*` state and an approval request goes out to your reviewers. You don't approve or deny from this list. That happens in the approvals flow: from the [notification channel](/docs/approvals/set-up-notifications) or the [approval request page](/docs/approvals/approval-requests), where you can also see who requested the transition and the reason they gave. Once the request is decided, the item advances to its final state or returns to `open` automatically. You can track its current state from this list or the item's Activity tab. ### Close, reopen, or delete Beyond configuring approvals, you have a few direct actions on items: - **Close**: close an item without resolution. Closed items are terminal. Use this for items that no longer apply, such as agent-generated items whose underlying issue has disappeared. - **Reopen**: bring a closed, rejected, or deferred item back to `open`. - **Delete**: permanently remove an item and its suggestions. ### Admin workflow The admin role is to set the rules, not to triage items. You configure categories, decide which transitions need approval, and create items manually when something isn't surfaced by an agent. Developers do the resolving, deferring, and rejecting; reviewers act on the approval requests those transitions generate. ```mermaid stateDiagram-v2 direction TB open --> pending_verification : Developer resolves open --> pending_deferral : Developer defers open --> pending_rejection : Developer rejects pending_verification --> resolved : Request approved pending_verification --> open : Request denied pending_deferral --> deferred : Request approved pending_deferral --> open : Request denied pending_rejection --> rejected : Request approved pending_rejection --> open : Request denied ``` A transition only passes through a `pending_*` state when an approval action gates it. Otherwise the item moves straight to its final state. Deferred items auto-reopen when their target date passes, and admins can close, reopen, or delete items at any point. ## Next steps - [Action items overview](/docs/action-items): see how the detect-triage-resolve cycle fits together - [Triage and resolve action items](/docs/action-items/triage-and-resolve): understand the workflow your governance rules shape - [Approvals](/docs/approvals): gate item transitions with policies and notification channels - [API reference](/docs/api/action-item-list): endpoints to manage items and categories programmatically --- ## Action items URL: https://docs.nullplatform.com/docs/action-items/ :::info 🚀 Early release AI Ops features are in early release and may not be available in your organization yet. If you'd like to try them or want to know more, [reach out to us](https://docs.nullplatform.com/docs/support) and we'll get you set up. ::: Action items are the tasks your team needs to do to improve the software you run. Think of it as an intelligent ticketing system built into nullplatform: issues worth acting on, such as security vulnerabilities, over-provisioned resources, or configuration drift, become items that developers can triage, prioritize, and resolve. Each item carries enough context to act on it, a priority, and an estimated value, so you can always tell what matters most. ## How action items work Every action item moves through the same detect-triage-resolve cycle, regardless of who creates it or how it's resolved: ```mermaid flowchart LR A[Issue detected] --> B[Action item created] B --> C[Developer triages] C --> D[Resolve, defer, or reject] ``` 1. An issue is **detected**. It can come from an automated agent scanning your applications, a developer spotting something during a review, or an admin creating an item manually from Platform Settings. 2. The item is classified into a **category** (for example, "Security vulnerabilities" or "Cost optimization") and assigned a **priority**. 3. The developer responsible for the application **triages** it and decides how to handle it: resolve it, defer it, or reject it as not applicable. ### AI-assisted detection and resolution A specific variant of this flow is end-to-end automation by AI agents. An agent can detect an issue, create the action item, and attach an **AI suggestion** with a ready-to-apply fix. The developer reviews the suggestion, optionally adjusts parameters, and approves it. The agent then executes the change. ```mermaid flowchart LR A[Agent detects issue] --> B[Action item + AI suggestion] B --> C[Developer approves] C --> D[Agent applies fix] ``` AI-assisted resolution is one path. Most items, especially those created manually or without a suggestion attached, are resolved by the developer directly. Both paths share the same lifecycle, statuses, and governance rules. ## A real example: catching a PII leak in production logs Here's what an action item looks like end to end. The `log-pii-scanner` agent continuously watches the logs of every application in your organization. During a recent scan of the **Pharmacy Shops API**, it found 11 occurrences of JWT tokens leaking in URLs on the Production UK scope, a critical PII violation. The agent opened an action item with all the context a developer needs to act: The card packages the detection summary, obfuscated log samples (so developers see the shape of the leak without exposing real tokens), concrete remediation guidance, and structured metadata. No hunting for context, no guessing where to start. Alongside the details, the same agent attached an **AI suggestion** with a proposed fix: The developer reviews the suggestion, adjusts parameters if needed, and approves. The agent applies the change and the item moves to `resolved`. From scan to fix, the developer stays in control but skips the mechanical work. ## Core concepts ### Categories Categories classify action items into groups. A category defines what the items in it track (for example, dollars saved, hours reduced), how they look in the UI (color and icon), and limits on how items can be deferred. Categories follow the NRN hierarchy. A category defined at the organization level is inherited by every account, namespace, and application below it, so admins can set it once and apply it everywhere. Requiring approval before an item can be resolved, deferred, or rejected is handled separately, through nullplatform's [approvals](/docs/approvals) engine, so you govern those transitions with the same policies and notification channels you use for deployments and scopes. ### Priority and score Every item has a manual **priority**, from `low` to `critical`, and a **value**, which represents the estimated benefit of resolving it in the unit defined by its category (such as dollars or hours). Nullplatform computes a **score** as `value × priority weight`. Items in the list view are sorted by score by default, so high-value, high-priority items surface first. ### NRN scoping Action items are scoped to any level of your [NRN](/docs/NRN) hierarchy: organization, account, namespace, application, or scope. The list views filter by the current NRN context, so developers only see items for their application, and admins can query across the whole organization. ### AI suggestions An action item can have one or more **AI suggestions** attached to it. Each suggestion is a proposed fix with a description, a confidence score, and optional parameters the developer can adjust before approving. Once approved, the owning agent applies the fix and reports back with the result. ## Who uses action items Action items have two audiences, each with a different view and set of responsibilities: | Audience | Where they access it | What they do | |---|---|---| | **Developers** | **Application dashboard** and **Manage > Action Items** | See the items assigned to their application, triage them, resolve or defer them, and review AI suggestions. | | **Platform Settings admins** | **Settings > Action Items** | Create categories, set which transitions require approval, and create items manually across the organization. | The two views are tied together: admins configure categories and governance in Platform Settings, and developers work through items in their application dashboards. Approval requests for gated transitions are reviewed in the [approvals](/docs/approvals) flow, the same way as deployments and scopes. ## Next steps - [Triage and resolve action items](/docs/action-items/triage-and-resolve): from the application dashboard, with optional AI-suggested fixes - [Action item owners](/docs/action-items/owners): assign an item to a person and filter the list by owner - [Configure and manage action items](/docs/action-items/configure-and-manage): set up categories, gate transitions with approvals, and oversee items across the organization - [Approvals](/docs/approvals): require review before items can be resolved, deferred, or rejected - [Action items API reference](/docs/api/action-item-list): endpoints to create, update, and transition action items programmatically --- ## Action item owners URL: https://docs.nullplatform.com/docs/action-items/owners Every action item is scoped to a place in your [NRN](/docs/NRN) hierarchy, so you always know which team is responsible for it. An **owner** narrows that down to one person: whoever is working on the item. Ownership is optional, and it isn't permanent. Items start out unassigned, so anyone can pick one up, pass it to a teammate, or hand it back. ## Assign an owner You can set the owner from the **Owner** column in the action items list, or from the **Owner** field in an item's detail view. Both open the same picker. 1. Go to **Manage > Action Items** from the application sidebar. 2. Click the **Owner** cell of the item you want to assign. In the detail view, click the **Owner** field instead. 3. Click **Assign to me** to take the item, or search for a teammate by name or email. The search covers everyone in your organization, not only the names already on the list. 4. Click the person you want. The item is assigned right away, with no confirmation step. > ℹ️ **Note:** The picker only lists people. Machine identities such as API keys are never offered as owners. Send a [POST request](/docs/api/action-item-assign) to `/action_item/{id}/assign` with the nullplatform user ID of the new owner. The response is the updated item. ```bash curl -L -X POST 'https://api.nullplatform.com/action_item/xYz789AbCdEf/assign' \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer ' \ -d '{ "user_id": 2345 }' ``` :::note You can only change the owner while an item still represents work: `open`, `deferred`, `pending_deferral`, `pending_verification`, and `pending_rejection`. Once an item reaches `resolved`, `rejected`, or `closed`, the dashboard hides the control and the API answers `400`. ::: ## Change or remove the owner An item has one owner at a time, so assigning someone to an item that already has one reassigns it. Unassigning returns the item to the unassigned pool, which is what you want when you're not the right person for it, or when you're handing it back for someone else to pick up. 1. Click the current owner in the **Owner** column, or the **Owner** field in the item's detail view. 2. Pick a different person to reassign the item. 3. To leave the item unassigned, click **Unassign** at the bottom of the picker. Reassigning uses the same endpoint as assigning. Send the [assign request](/docs/api/action-item-assign) again with a different `user_id`, and the new owner replaces the previous one: ```bash curl -L -X POST 'https://api.nullplatform.com/action_item/xYz789AbCdEf/assign' \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer ' \ -d '{ "user_id": 6789 }' ``` To release an item, send a [POST request](/docs/api/action-item-unassign) to `/action_item/{id}/unassign`. It takes no body, and unassigning an item that nobody owns answers `400`: ```bash curl -L -X POST 'https://api.nullplatform.com/action_item/xYz789AbCdEf/unassign' \ -H 'Authorization: Bearer ' ``` ## Filter by owner The action items list has an **Owner** filter, next to the category, priority, and status ones. It takes several people at once, and **Unassigned** is one of the choices, so you can see your own items together with the ones still up for grabs instead of switching between two views. Over the API, the listing accepts `assignee_id` as a comma-separated list, where `none` stands for the unassigned pool. This is what that combined view sends: ```bash curl -L -X GET 'https://api.nullplatform.com/action_item?assignee_id=none,2345&status_not_in=closed,rejected' \ -H 'Authorization: Bearer ' ``` To see how the work is spread across the team instead, open the [board](/docs/action-items/triage-and-resolve#the-board-view) and set **Group by** to owner. The cards split into one horizontal lane per person, so you can tell at a glance who's carrying what. ## Track ownership changes Every assignment and every release is recorded on the item, so the **Activity** tab reads as a history of who picked the work up and when. They show up as **Assigned** and **Unassigned** entries with the actor and the timestamp, and a reassignment names both the previous and the new owner. Comments share the same chronology, so the reason for a handover sits right next to the handover itself. ## Next steps - [Triage and resolve action items](/docs/action-items/triage-and-resolve): decide what happens to the items you own - [Action items overview](/docs/action-items): see how the detect-triage-resolve cycle fits together - [NRN](/docs/NRN): understand the hierarchy that scopes items to teams and applications - [Assign an action item](/docs/api/action-item-assign): the endpoint reference for assignment --- ## Triage and resolve action items URL: https://docs.nullplatform.com/docs/action-items/triage-and-resolve Every action item created for your application lands in your queue. You'll find them in two places in the dashboard: a summary widget on the application home page, and the full backlog under **Manage > Action Items**, which you can read as a list or as a board. From there you decide what happens to each one: resolve it, defer it, or reject it as not applicable, with or without help from AI suggestions. ## The dashboard widget When you open an application, the home page shows a summary widget with the action items assigned to it. Use it as a quick pulse check. If any item is waiting on an admin decision, a pending approval notice appears alongside the counts. Click **View all** to open the full list. ## The list view Go to **Manage > Action Items** from the application sidebar to see every item assigned to your application. Action Items list view showing columns for Title, Category, Priority, Status, Owner, Score, and Created" src="/img/action-items/app-list-view.png" width="100%" className="helper-image" /> Click any title to open the detail view. Items are sorted by score, so the highest-value, highest-priority work surfaces first. Use the filters above the table to narrow by category, priority, owner, or status, and the search bar to match on item title. The status filter starts on the working set, so resolved and closed items stay out of the way until you ask for them. ## The board view The same items as cards in columns instead of rows. Switch between the two with the **View** control in the toolbar, which remembers your choice. The columns are always the statuses, in the order work moves through them: **Open**, **Pending approval**, **Deferred**, **Resolved**, **Rejected**, and **Closed**. The three `pending_*` statuses share one column, since they differ only in which transition is waiting on a reviewer. **Group by** splits the board into horizontal lanes, by priority, owner, or category. Each card carries the item's title, category, priority, and owner, plus a menu with the transitions its status allows, so you can triage without leaving the column you're in. Cards waiting on an approval have no menu, because nothing can move until the reviewer decides. Filters and search are shared with the list, so a set you narrowed in one view is the same set in the other. One difference worth knowing: the board loads a single window of items sorted by score instead of paging through all of them, and tells you so above the columns when there are more. ## Triaging an action item Click any item to open its detail view. The page has two tabs, **Overview** and **Activity**, and a properties column beside them that always shows the item's current state: status, owner, priority, dates, source, and labels. From the top of the page you can resolve, defer, or reject the item. Each one asks for a bit of context first: | Action | When to use it | What you provide | |---|---|---| | **Resolve** | The work is done | An optional note, and an evidence URL such as the pull request that fixed it | | **Defer** | It's real, but not now | A reason of at least 10 characters, and a target date | | **Reject** | It doesn't apply, like a false positive | A reason of at least 20 characters | If an [approval action](/docs/approvals/actions) gates the transition, your choice creates an approval request and the item waits in the matching `pending_*` status until a reviewer decides. Otherwise it moves straight to `resolved`, `deferred`, or `rejected`. A deferred item reopens on its own when the target date passes. :::info Deferral limits If the category defines `max_deferral_count` or `max_deferral_days`, those limits are enforced. An item that has already been deferred too many times, or for too long, is blocked from further deferrals. ::: Every transition has an API equivalent: Send a [POST request](/docs/api/action-item-resolve) to `/action_item/{id}/resolve`. ```bash curl -L -X POST 'https://api.nullplatform.com/action_item/xYz789AbCdEf/resolve' \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer ' \ -d '{ "resolution": "Upgraded base image to node:20-alpine3.19", "evidence_url": "https://github.com/org/repo/pull/42" }' ``` Send a [POST request](/docs/api/action-item-defer) to `/action_item/{id}/defer`. ```bash curl -L -X POST 'https://api.nullplatform.com/action_item/xYz789AbCdEf/defer' \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer ' \ -d '{ "defer_until": "2026-06-01", "reason": "Waiting for the next maintenance window to apply this change safely." }' ``` Send a [POST request](/docs/api/action-item-reject) to `/action_item/{id}/reject`. ```bash curl -L -X POST 'https://api.nullplatform.com/action_item/xYz789AbCdEf/reject' \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer ' \ -d '{ "reason": "This CVE does not affect our usage of this library. The vulnerable function is never called." }' ``` ### Categorizing the outcome :::info 🚀 Early release This is rolling out and the available labels may change. ::: When you resolve, defer, or reject an item, you can tag the outcome with a label from a fixed set defined by nullplatform, such as marking a rejection as a false positive or a resolution as a priority fix. It gives your team a consistent vocabulary for why items were closed out, which helps when reviewing activity later. ### Status flow The path an item takes depends on whether an approval action gates the transition. When one does, your action moves the item through a `pending_*` state first: ```mermaid stateDiagram-v2 direction TB [*] --> open open --> pending_verification : Resolve open --> pending_deferral : Defer open --> pending_rejection : Reject pending_verification --> resolved : Approve pending_verification --> open : Deny pending_deferral --> deferred : Approve pending_deferral --> open : Deny pending_rejection --> rejected : Approve pending_rejection --> open : Deny deferred --> open : Auto-reopen ``` While an item sits in a `pending_*` state, an [approval request](/docs/approvals/approval-requests) waits for a reviewer to approve or deny it, through Slack, a webhook, or the approval request page. You can keep tabs on it from the list view or the **Activity** tab. ## Reviewing AI suggestions Some items come with one or more AI-generated fix proposals, at the top of the **Overview** tab. Approving one is the only path that closes an item without anyone doing the work by hand, which is why the page offers it first. Some suggestions include editable **execution parameters**, like the target version of a dependency upgrade, that you can adjust before approving. Click **Approve** to authorize the fix, and the owning agent picks it up and runs it. Click **Reject** to dismiss the suggestion. After execution, the card shows whether the fix succeeded, usually with a link to the resulting pull request, or failed, in which case you can adjust the parameters and approve it again. Suggestions can expire. If you don't act on a pending or approved one before its expiration date, it can no longer be applied. Over the API, [approve](/docs/api/suggestion-approve) and [reject](/docs/api/suggestion-reject) are POST requests on `/action_item/{actionItemId}/suggestions/{id}`. ```bash curl -L -X POST 'https://api.nullplatform.com/action_item/xYz789AbCdEf/suggestions/sUg123AbCdEf/approve' \ -H 'Authorization: Bearer ' ``` ## Comments and activity The **Activity** tab is the item's whole history as one thread, newest first: what people said and what happened to the item, in the order it occurred. The comment box sits on top, since saying something is why most people open it. Agent comments are labeled with a `BOT` badge, similar to how Dependabot leaves notes on pull requests. Add a comment with a [POST request](/docs/api/action-item-comment-create) to `/action_item/{id}/comments`. ```bash curl -L -X POST 'https://api.nullplatform.com/action_item/xYz789AbCdEf/comments' \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer ' \ -d '{ "content": "Reviewed the affected resources. This should be prioritized for the next sprint." }' ``` The same thread records every status change, field update, assignment, and approval decision, with the actor and timestamp on each one. Use it to understand why an item is in its current state and who did what. ## Next steps - [Action items overview](/docs/action-items): see how the detect-triage-resolve cycle fits together - [Action item owners](/docs/action-items/owners): assign an item to a person and find what you own - [Configure and manage action items](/docs/action-items/configure-and-manage): learn what admins and ops engineers configure on the governance side - [Approvals](/docs/approvals): how the approval requests behind gated transitions work - [Action items API reference](/docs/api/action-item-list): endpoints to work with items programmatically --- ## Create a notification channel URL: https://docs.nullplatform.com/docs/agent/agent-channel This is the final step in the agent setup. The agent is installed and registered, but it stays idle until you connect it to platform events. A notification channel defines which events the agent listens to and what it runs in response. ## Create a channel Create a channel to define your agent’s response to platform events. **Supported types** You can configure the channel using either of these types: - `type: agent`: (Default) Executes a local script through the agent - `type: http`: ends a request to a remote HTTP handler ### Prerequisites - The agent installed. See [Install the agent](/docs/agent/installation). - An **new** API key also with **Agent** and **Ops** roles. See [Authenticate the agent](authentication.md). ### Example: Trigger a script on service events This channel listens for `service` events and runs a local script inside your environment: 1. Go to **Platform settings > Notifications > Channels** and click **+ New channel**. 2. Set the **source** (e.g., *Service*). 3. Select **agent** as the **type** (or **http** for HTTP channels). 4. Fill in configuration, then click **Create channel**. ```bash np notification channel create \ --body '{ "nrn": "organization=1:account=2:namespace=3:application=4", "source": [ "service" ], "description": "My agent channel for service events", "type": "agent", // or "http" for HTTP channels "configuration": { "api_key": "AAAA.1234567890abcdef1234567890abcdefPTs=", "command": { "type": "exec", "data": { "cmdline": "path-to-entrypoint-file/entrypoint --service-path=\"$SERVICE_PATH\"", "environment": { "NP_ACTION_CONTEXT": "${NOTIFICATION_CONTEXT}" } }, }, "selector": { "environment": "local" } }, "filters": { "service.specification.slug": "$service-spec-slug" } }' ``` ```bash curl -L 'https://api.nullplatform.com/notification/channel' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ' \ -d '{ "nrn": "organization=1:account=2:namespace=3:application=4", "source": [ "service" ], "description": "My agent channel for service events", "type": "agent", // or "http" for HTTP channels "configuration": { "api_key": "AAAA.1234567890abcdef1234567890abcdefPTs=", "command": { "data": { "cmdline": "path-to-entrypoint-file/entrypoint --service-path=\"$SERVICE_PATH\"", "environment": { "NP_ACTION_CONTEXT": "${NOTIFICATION_CONTEXT}" } }, "type": "exec" }, "selector": { "environment": "local" } }, "filters": { "service.specification.slug": "$service-spec-slug" } }' ``` ### Example: Trigger a script for telemetry actions If you want to handle telemetry events (logs, metrics, instances), set up an agent channel for `telemetry` events. 👉 See our dedicated documentation on [telemetry](/docs/telemetry/overview) for more details. ## How it works The diagram below shows the basic flow when a notification channel is triggered: ```mermaid sequenceDiagram autonumber participant NP as Nullplatform participant Channel as Notification channel participant Infra as Your infrastructure (Agent) NP->>Channel: Emit event Channel->>Infra: Send notification via Agent Infra->>Infra: Execute configured script/scope Infra-->>NP: (Optional) Send result/logs ``` ### What happens step by step 1. Nullplatform emits an event (e.g., service deployed). 2. A notification channel forwards the event to your infrastructure’s agent. 3. The agent runs the configured script or handler. 4. The script executes locally, inside your infrastructure. 5. The agent optionally returns logs or results to nullplatform. --- ## Refresh agent sources URL: https://docs.nullplatform.com/docs/agent/agent-refresh Use **Refresh sources** to make an agent **get up to date with the repositories** you configured in `AGENT_REPO`. This is helpful after you push updates (new scopes, handlers, config, etc.) and want the agent to use them, without reinstalling or redeploying. > ℹ️ This action **does not** reinstall or upgrade the agent binary or Helm chart. To change or update your Helm release > or rerun the installer. ## How to refresh agent sources You can refresh sources from the UI or via the API. In both cases, the agent pulls the latest from your configured repository (or repositories) and reloads its sources. ### From the UI 1. Go to **Platform settings > Agents**. 2. Find the agent to update. 3. Open its menu and click **Refresh sources**. ### Using the API Send a **POST** request to the [Agent command API](/docs/api/agent-command-execute) with the `refresh-sources` command: ```bash curl -L 'https://api.nullplatform.com/controlplane/agent_command' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ' \ -d '{ "command": { "type": "refresh-sources", "data": {} }, "selector": { "key": "value" } }' ``` - Replace the `"key": "value"` pair with your selector, for example: `"cluster": ""`. ## When to use it - You pushed changes to the branch in `AGENT_REPO` and want the agent to use them. - You added/removed repositories in a comma-separated `AGENT_REPO` value. - You changed repo-backed configuration the agent reads at runtime. ## What it does (and doesn’t) **It does:** - Get the latest version from each repo/branch listed in `AGENT_REPO`. - Reload sources the agent reads from those repos (e.g., scopes/handlers/config). **It doesn’t:** - Change runtime flags or environment variables (e.g., `TAGS`, `NP_API_KEY`). - Upgrade the agent binary or Helm chart. - Create/modify permissions or API keys. > 📖 To update your Helm release or reinstall the agent, see > [Install the agent](installation.md) docs. ## Tips and troubleshooting - **Ensure changes are pushed**: Refresh gets updates from the remote branch (`...repo.git#branch`), not local work. - **Multiple repos**: Use comma-separated URLs (no spaces) in `AGENT_REPO`. - **Private repos**: If your URL embeds a token, verify it’s valid and not expired. - **Heartbeat and logs**: Confirm the agent shows a recent **Last heartbeat** and check logs if updates don’t appear. --- ## Authenticate the agent URL: https://docs.nullplatform.com/docs/agent/authentication Agents authenticate with **API keys**. When the key includes the **Agent** role, the platform looks at every NRN permission on that key and builds the agent’s `nrns` array from them. This lets one agent serve multiple resources (accounts, namespaces, applications, or any NRN you include) without extra configuration. ## Create an agent API key 1. Go to **Platform settings > API keys** and click **+ New API key**. 2. Select each **resource** (NRN) the agent should serve. You can choose a single account or multiple NRNs across your hierarchy. 3. Assign the roles **agent** (`controlplane:agent`) and **ops**. Add any extra roles the agent needs for your workflows (e.g., `developer`, `secrets-reader`). 1. Save the API key securely—you will pass it to the agent as `NP_API_KEY` later. :::note You will use this same API key when you install the agent. ::: ## How NRN assignment works 1. The agent starts with `NP_API_KEY`. 2. The nullplatform reads the key’s permissions with the `controlplane:agent` role and builds the agent’s `nrns` array. 3. The agent registers with those NRNs and can respond to work for any matching scope. > 💡 Want one agent to cover multiple resources? Add all relevant NRNs to the API key. No extra agent configuration is needed. ## Verify the agent’s NRNs List agents and filter by NRN using the [Control plane API](/docs/agent-index): ```bash np agent list \ --nrn organization=1:account=2 ``` ```bash curl -L 'https://api.nullplatform.com/controlplane/agent?nrn=organization=1:account=2' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ' ``` The response includes the agent’s `nrns` array, for example: ```json { "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "name": "my-agent-name", "nrns": ["organization=1:account=2", "organization=1:account=2:namespace=3"], "status": "active", "capabilities": [] } ``` ## Migrating existing agents - **No downtime:** Agents using older account-level keys keep working. Those keys infer a default NRN from the account data. - **Migrate one agent at a time:** Edit or create a new key with `controlplane:agent` for the NRNs you need, set it as `NP_API_KEY`, and restart that agent. - **Verify after restart:** Call the agent list API (with optional `nrn` filter) and confirm the agent shows the expected `nrns` array. - **Roll back easily:** If something looks off, switch back to the previous key and restart the agent. ## Next steps Now that you created an API key, you can jump to [installing the agent](installation.md). --- ## GitHub App auth for agent repos URL: https://docs.nullplatform.com/docs/agent/github-app-command-repos The agent reads scopes and actions from the repos you list in `AGENT_REPO`. By default, a private repo is cloned with a static token embedded in its URL: ```bash AGENT_REPO="https://@github.com/your-org/private-repo.git#main" ``` That token is long-lived, tied to a user account, and stops working the day it expires or gets rotated. A GitHub App replaces it with short-lived installation tokens that the agent mints on demand and renews on its own, with no restart and no token in the URL. You declare credentials **per GitHub organization**, so an agent that serves repos from several orgs uses a different App for each one. ## How it works Each time the agent runs a git operation against a repo whose org has an App configured, it asks for a fresh installation token and authenticates with it. Tokens are cached per organization and re-minted before they expire, so a long-running agent keeps pulling without any manual rotation. ```mermaid flowchart LR A["**GitHub App credentials**App ID and private key,declared per organization"] B["**Agent**Mints an installation tokenfor the org and caches it.Renews it before expiry."] C["**Agent repo on GitHub**Cloned, fetched and pulledwith the installation token."] A -- "reads" --> B B -- "per git operation" --> C classDef customer fill:#eef3fb,stroke:#274a86,color:#0b1e3f,stroke-width:1.5px; class A,B,C customer; linkStyle 0 stroke:#0b1e3f,stroke-width:2.5px; linkStyle 1 stroke:#0b1e3f,stroke-width:2.5px; ``` :::note This applies only to HTTPS `github.com` remotes. SSH remotes, GitHub Enterprise, and other git providers are not affected and keep using the credentials you already configured for them. ::: ## Set up the GitHub App In the GitHub organization that owns your agent repo, [create a GitHub App](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app) with: - **Repository permissions:** `Contents: Read-only` and `Metadata: Read`. - A new **private key**. Download the `.pem` file when GitHub shows it, since you can't retrieve it later. Install the App on the organization and grant it access to the repos the agent needs. Then note the **App ID** from the App's settings page. You can also note the **Installation ID** (the numeric ID at the end of the URL after installing), but it's optional — the agent resolves it from the organization when you leave it out. Repeat this for every organization whose repos the agent clones. ## Configure the agent Pass one `--github-app` flag per organization: ```bash agent \ --github-app "org=acme-core,app-id=123456,installation-id=78901234,private-key=/etc/np/keys/acme-core.pem" \ --github-app "org=acme-labs,app-id=654321,private-key-ssm-parameter=/np/gh/acme-labs-key" ``` ### Flag keys | Key | Required | Description | | --- | --- | --- | | `org` | Yes | GitHub organization the App is installed in. Case-insensitive | | `app-id` | Yes | The App's App ID or Client ID | | `installation-id` | No | Pins the installation and skips the lookup. Resolved from the org when omitted | | `private-key` | One of the two | Path to the App's private key `.pem` file | | `private-key-ssm-parameter` | One of the two | Name of an AWS SSM SecureString parameter holding the PEM | Each organization can appear only once, and each entry needs exactly one key source. :::warning Inline PEM values aren't accepted. Point the agent at a file path or an SSM parameter name. ::: ### On Kubernetes Don't set the flag by hand. The `nullplatform-agent` chart takes the same credentials under `github.apps`, one entry per organization, and renders them into the agent's `NP_GITHUB_APPS` environment variable for you. In production, keep the PEMs in a Secret you manage and point the chart at it: ```yaml github: secret: create: false name: my-github-app-keys apps: - org: acme-core appId: "123456" installationId: "78901234" privateKeySecretKey: acme-core.pem - org: acme-labs appId: "654321" privateKeySsmParameter: /np/gh/acme-labs-key ``` | Value | Description | | --- | --- | | `github.apps[].org` | GitHub organization the App is installed in | | `github.apps[].appId` | The App's App ID or Client ID | | `github.apps[].installationId` | Optional. Resolved from the org when unset | | `github.apps[].privateKeySecretKey` | PEM file from the Secret. Defaults to `.pem` | | `github.apps[].privateKeySsmParameter` | AWS SSM parameter holding the PEM. Needs IRSA on the service account | | `github.apps[].privateKey` | Inline PEM. Requires `github.secret.create: true`, so dev only | | `github.secret.create` | Whether the chart creates the Secret from inline `privateKey` values | | `github.secret.name` | Existing Secret holding one PEM per org | | `github.mountPath` | Where the PEMs are mounted. Defaults to `/etc/nullplatform/github-apps` | Each entry needs exactly one key source. Leave `github.apps` empty to disable GitHub App auth entirely. :::info `github.apps` requires chart version **3.0.0** or later. It replaces `githubTokenInit`, which was [removed in that release](install-helm-github-app.md). ::: ## AWS permissions for SSM private keys If you store a private key with `private-key-ssm-parameter` (or `privateKeySsmParameter` in the chart), the agent reads it from AWS Systems Manager at startup. Store the PEM in a parameter of type `SecureString`, and grant the agent's IAM identity: | Action | On | Why | | --- | --- | --- | | `ssm:GetParameter` | Each parameter holding a PEM | Reads the parameter | | `kms:Decrypt` | The KMS key that encrypts those parameters | The agent always requests the value with decryption enabled | A minimal policy for two organizations looks like this: ```json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "ssm:GetParameter", "Resource": [ "arn:aws:ssm:us-east-1:123456789012:parameter/np/gh/acme-core-key", "arn:aws:ssm:us-east-1:123456789012:parameter/np/gh/acme-labs-key" ] }, { "Effect": "Allow", "Action": "kms:Decrypt", "Resource": "arn:aws:kms:us-east-1:123456789012:key/abcd1234-a123-456a-a12b-a123b4cd56ef" } ] } ``` Keep in mind: - **Scope the resources.** List only the parameters the agent actually reads, rather than granting `parameter/*`. - **Customer managed keys need both sides.** If your parameters use your own KMS key, allow the agent's role in the **key policy** too. An IAM policy alone isn't enough. - **On EKS, use [IRSA](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html).** Attach the policy to a role and annotate the agent's service account with it: ```yaml serviceAccount: annotations: eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/nullplatform-agent-irsa ``` - **Outside EKS**, the agent uses the standard AWS credential chain, so an instance profile or the environment's configured credentials work as usual. :::note These are the same permissions the agent needs for [`--ssm-apikey-parameter`](install-helm.md#use-aws-ssm-for-the-api-key). If you already use that, extend the existing policy with the new parameter ARNs instead of creating a second role. ::: If you'd rather not grant SSM access at all, use `private-key` and mount the PEM as a file instead. ## Migrate an existing agent You can move one organization at a time, and the agent keeps running the whole way through. 1. **Add the App entry** for the org and restart the agent. 2. **Drop the token from the URL.** Once the org has an App configured, the agent strips any credentials embedded in the repo URL and authenticates with the installation token instead. You can simplify `AGENT_REPO` to a clean URL: ```bash AGENT_REPO="https://github.com/your-org/private-repo.git#main" ``` 3. **Repeat for the remaining orgs.** Any org without an entry keeps using the token in its URL, and the agent logs a warning naming that org so you can see what's still pending. Repos already on disk are re-addressed in place, so there's no need to wipe the agent's working directory. :::tip Rolling back is just as simple. Remove the org's `--github-app` entry, put the token back in the repo URL, and restart. ::: ## Security notes - Private keys and minted tokens stay in memory. They're never written to disk or to logs. - Clone and fetch errors have any credentials in the repo URL redacted before they reach the logs. - Installation tokens are short-lived and scoped to the repos you granted the App, unlike a personal access token tied to a user account. ## Related pages - [Install the agent with Helm](install-helm.md) — where `AGENT_REPO` is configured. - [Authenticate private repos with a GitHub App](install-helm-github-app.md) — the chart's init container flow, which clones the repo once at pod startup. - [Refresh agent sources](agent-refresh.md) — pull new commits into a running agent. --- ## Authenticate private repos with a GitHub App (removed) URL: https://docs.nullplatform.com/docs/agent/install-helm-github-app :::danger Removed in chart 3.0.0 `githubTokenInit` was removed from the `nullplatform-agent` Helm chart in **version 3.0.0**, released on September 1, 2026. A release that still sets it won't render: the chart aborts with an error pointing you at `github.apps`. The init container minted one token at pod startup and never renewed it, and it supported a single installation ID. The agent now mints per-org installation tokens itself and renews them before they expire. See [GitHub App auth for agent repos](github-app-command-repos.md) for the supported setup. This page is kept for reference if you're still on chart 2.x. Everything below applies only to those versions. ::: When the agent reads scopes and actions from a **private GitHub repository**, chart 2.x can use a GitHub App to authenticate instead of embedding a personal access token in `AGENT_REPO`. ## When to use this The basic [Helm install](install-helm.md) authenticates the agent repo via a token embedded in the URL: ```bash AGENT_REPO="https://@github.com/your-org/private-repo.git#main" ``` That works, but personal access tokens are long-lived, tied to a user account, and easy to leak via Helm history or `kubectl describe pod`. The GitHub App flow avoids both problems: - **Short-lived credentials.** Each pod start generates a fresh installation token (valid for one hour). - **No personal account dependency.** The App belongs to your GitHub organization, not to an individual. - **Fine-grained permissions.** You grant the App access only to the specific repos the agent needs. ## How it works When the chart is deployed with `githubTokenInit.enabled=true`, the agent pod runs an init container before the agent itself starts. The init container reads GitHub App credentials from a Kubernetes Secret, exchanges them for a short-lived token, and clones the repo into a shared volume that the agent container then reads. ```mermaid flowchart LR A["**GitHub App credentials**Kubernetes Secret with App ID,Installation ID, private key"] B["**Init container**Builds a JWT from the private key.Exchanges it for a short-livedinstallation token. Clones theprivate repo into a shared volume."] C["**Agent container**Reads the cloned repo from theshared volume and connects tonullplatform."] A -- "reads" --> B B -- "shared volume" --> C classDef customer fill:#eef3fb,stroke:#274a86,color:#0b1e3f,stroke-width:1.5px; class A,B,C customer; linkStyle 0 stroke:#0b1e3f,stroke-width:2.5px; linkStyle 1 stroke:#0b1e3f,stroke-width:2.5px; ``` > 🔒 The GitHub App private key never leaves the cluster. The init container generates the JWT locally and only the resulting installation token reaches GitHub. ## Prerequisites - A Kubernetes cluster and Helm 3+ (same as the [base Helm install](install-helm.md)). - A nullplatform API key with the **agent** and **ops** roles. See [Authenticate the agent](authentication.md). - A GitHub App installed on the organization that owns the agent repo, with **read access** to the repos you want to clone. ## 1. Set up the GitHub App In your GitHub organization, [create a new GitHub App](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app) with the following: - **Repository permissions:** `Contents: Read-only`, `Metadata: Read-only`. - A new **private key** (download the `.pem` file when GitHub shows it; you can't retrieve it later). Then install the App on your organization and select the agent repo. After installation, note down: - The **App ID** (visible on the App's settings page). - The **Installation ID** (the numeric ID at the end of the URL after you install the App). - The contents of the **private key** `.pem` file. ## 2. Provide the credentials to the chart The chart supports two patterns. Pick the one that fits your secrets workflow. ### Option A: Chart-managed secret The chart creates the Kubernetes Secret for you from values you pass via `--set`. This is the simplest option for development or single-team installs. The private key value must be **base64-encoded**. Generate it from your `.pem` file: ```bash export PRIVATE_KEY_B64=$(base64 -w 0 < path/to/private-key.pem) ``` Then pass all three credentials when you install the chart: ```bash helm install nullplatform-agent nullplatform/nullplatform-agent \ --namespace nullplatform-tools --create-namespace \ --set githubTokenInit.enabled=true \ --set githubTokenInit.repositoryUrl=github.com/your-org/private-repo.git \ --set githubTokenInit.values.appId= \ --set githubTokenInit.values.installationId= \ --set githubTokenInit.values.privateKey=$PRIVATE_KEY_B64 \ --set configuration.values.NP_API_KEY=$NP_API_KEY \ --set configuration.values.TAGS="$AGENT_TAGS" ``` The chart creates a Secret named `github-app-secret` and wires the init container to read from it. ### Option B: Bring your own secret For production or GitOps workflows, you typically manage Secrets through a tool like External Secrets Operator or sealed-secrets. In that case, pre-create the Secret yourself and point the chart at it. Create a Secret with these three keys (default names: `APP_ID`, `INSTALLATION_ID`, `PRIVATE_KEY`): ```yaml apiVersion: v1 kind: Secret metadata: name: github-app-secret namespace: nullplatform-tools type: Opaque stringData: APP_ID: "" INSTALLATION_ID: "" PRIVATE_KEY: "" ``` Then install the chart referencing the existing Secret: ```bash helm install nullplatform-agent nullplatform/nullplatform-agent \ --namespace nullplatform-tools --create-namespace \ --set githubTokenInit.enabled=true \ --set githubTokenInit.repositoryUrl=github.com/your-org/private-repo.git \ --set githubTokenInit.secretName=github-app-secret \ --set configuration.values.NP_API_KEY=$NP_API_KEY \ --set configuration.values.TAGS="$AGENT_TAGS" ``` Override `githubTokenInit.secretKeys.*` if you use different key names in your Secret. ## 3. Verify the init container ran Once the chart is installed, check the init container logs to confirm the clone succeeded: ```bash kubectl logs -n nullplatform-tools -c github-token-init ``` You should see the token request to GitHub and the repo clone output. If the agent pod is `Running`, the agent container is connected and the cloned repo is mounted at `/root/.np/`. Then verify the agent appears in the platform UI under **Platform settings > Agents**. ## Configuration reference All values live under `githubTokenInit` in the chart values. | Value | Default | What it does | | --- | --- | --- | | `enabled` | `false` | Adds the init container that authenticates with GitHub and clones the repo | | `repositoryUrl` | `""` | Repo URL to clone. Omit the `https://` prefix (the init container prepends it with the token). Example: `github.com/your-org/private-repo.git` | | `secretName` | `github-app-secret` | Name of the Kubernetes Secret holding the App credentials | | `secretKeys.appId` | `APP_ID` | Key inside the Secret for the App ID | | `secretKeys.installationId` | `INSTALLATION_ID` | Key inside the Secret for the Installation ID | | `secretKeys.privateKey` | `PRIVATE_KEY` | Key inside the Secret for the base64-encoded private key | | `values.appId` | `""` | App ID, when letting the chart create the Secret (Option A) | | `values.installationId` | `""` | Installation ID, when letting the chart create the Secret (Option A) | | `values.privateKey` | `""` | Base64-encoded PEM, when letting the chart create the Secret (Option A) | | `workingDir` | `/root/.np` | Shared volume mount path. Must match the agent's command executor base path | | `image` | `alpine:3.22.2` | Init container image. Must include `apk` | ## Refreshing the cloned repo The init container runs **only at pod startup**. To pull new commits from the agent repo after the pod is running, restart the agent so the init container runs again with a fresh token: ```bash kubectl rollout restart deployment/nullplatform-agent- -n nullplatform-tools ``` With this init container flow, the agent container has no credentials of its own, so **Refresh sources** from the UI can't re-clone the repo. Treat pod restarts as the way to update the code the agent runs. This is one of the reasons the init container was removed. On chart 3.0.0 and later, configure [GitHub App auth for agent repos](github-app-command-repos.md): the agent mints its own installation tokens, so **Refresh sources** works normally and no restart is needed. ## Next step Once the agent is installed and registered, create a notification channel so it can respond to platform events. [Create a notification channel →](agent-channel.md) --- ## Install with Helm URL: https://docs.nullplatform.com/docs/agent/install-helm Use this method to run the agent on a Kubernetes cluster. It's the recommended approach for production environments and CI/CD pipelines. ## Prerequisites - A Kubernetes cluster - Helm 3+ - An API key with the **agent** and **ops** roles. See [Authenticate the agent](authentication.md) :::info All examples in this guide use the `nullplatform-tools` namespace. The `helm install` commands include `--create-namespace`, so Helm creates it on first install. If you use a different namespace, adjust `--namespace` and any `kubectl -n` flags accordingly. ::: ## 1. Add the Helm repository ```bash helm repo add nullplatform https://nullplatform.github.io/helm-charts helm repo update ``` ## 2. Set your values and install The minimum you need is an API key, an agent repo, and tags. ```bash export NP_API_KEY= export AGENT_TAGS= export AGENT_REPO= ``` > 💡 **Tip:** Check the logs to confirm that your `AGENT_REPO` was cloned correctly. See the [Variables section](#variables) below for format examples. Then install the chart: ```bash helm install nullplatform-agent nullplatform/nullplatform-agent \ --namespace nullplatform-tools --create-namespace \ --set configuration.values.NP_API_KEY=$NP_API_KEY \ --set configuration.values.AGENT_REPO=$AGENT_REPO \ --set configuration.values.TAGS="$AGENT_TAGS" ``` :::info For more advanced installs, see the [nullplatform Helm charts repo](https://github.com/nullplatform/helm-charts). ::: ## Verify installation 1. In the platform UI, go to **Platform settings > Agents**. 2. Filter by tags or agent ID to find your new agent. If it shows up, you're good to go. ## Variables Quick reference for the values you pass to the chart. For full details, see the [nullplatform Helm charts repo](https://github.com/nullplatform/helm-charts/tree/main/charts/agent). ### Required | Value | What it does | Helm path | Example | | --- | --- | --- | --- | | API key | Authenticates the agent | `configuration.values.NP_API_KEY` | `NP_API_KEY="npk_..."` | | Agent repo | Where the agent reads scopes and actions from | `configuration.values.AGENT_REPO` | `AGENT_REPO="https://git-provider/org/repo.git#main"` | | Tags | Metadata for filtering in the UI | `configuration.values.TAGS` | `TAGS="environment:dev,team:platform"` | ### Optional | Value | What it does | Helm path | Example | | --- | --- | --- | --- | | Log level | Controls verbosity | `configuration.values.NP_LOG_LEVEL` | `NP_LOG_LEVEL="INFO"` | ### AGENT_REPO formats You can pass one repo or several, separated by commas. Add `#branch` to target a specific branch. **Single public repo** ```bash AGENT_REPO="https://git-provider/your-org/your-repo.git#your_branch" ``` **Single private repo (with token)** ```bash AGENT_REPO="https://@git-provider.com/your-org/private-repo.git#your_branch" ``` **Multiple repos** ```bash AGENT_REPO="https://git-provider/your-org/your-repo.git#your_branch,https://@git-provider.com/your-org/private-repo.git#your_branch" ``` :::tip For private GitHub repos in production, prefer a [GitHub App](install-helm-github-app.md) over embedding a personal access token in the URL. The chart includes an init container that handles the App-based clone for you. ::: :::info For the full list of values and defaults, check the [nullplatform Helm charts repo](https://github.com/nullplatform/helm-charts). ::: ## Use AWS SSM for the API key For EKS installs, you can keep the API key in AWS SSM Parameter Store instead of passing it as a Helm value. The agent supports `--ssm-apikey-parameter` as an alternative to `--apikey` and fetches the parameter at startup with decryption enabled. You need: - An SSM parameter of type `SecureString` holding the API key (e.g. `/nullplatform/agent/api-key`). - An IAM role for the agent's service account ([IRSA](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html)) with `ssm:GetParameter` and `kms:Decrypt` on that parameter. Then override the chart `args` to use the SSM flag, leave `NP_API_KEY` empty, and annotate the service account with the IRSA role: ```yaml # values.yaml serviceAccount: annotations: eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/nullplatform-agent-irsa configuration: values: NP_API_KEY: "" AGENT_REPO: "https://git-provider/your-org/your-repo.git#main" TAGS: "environment:prod,team:platform" args: - --tags=$(TAGS) - --ssm-apikey-parameter=/nullplatform/agent/api-key - --runtime=host - --command-executor-debug - --webserver-enabled - --command-executor-git-command-repos - $(AGENT_REPO) ``` Install with: ```bash helm install nullplatform-agent nullplatform/nullplatform-agent \ --namespace nullplatform-tools --create-namespace \ -f values.yaml ``` ## Next step: create a notification channel The agent is now installed and registered. Without a notification channel, it won't respond to any platform events. [Create a notification channel →](agent-channel.md) --- ## Local install URL: https://docs.nullplatform.com/docs/agent/install-local Run the agent directly on your machine. No Kubernetes or Helm required. This is the fastest way to get started, and it's ideal for developing and testing locally before deploying to a cluster. ## Prerequisites You only need one thing: an **API key with the agent and ops roles**. See [Authenticate the agent](authentication.md) to set one up. ## 1. Download the agent This downloads the latest `np-agent` binary to your machine (usually to `~/.local/bin/np-agent`). ```bash curl https://cli.nullplatform.com/agent/install.sh | bash ``` Once it finishes, you can run `np-agent` from your terminal. ## 2. Start the agent Run the agent with your API key and a few required flags: ```bash np-agent \ --api-key=$NP_API_KEY \ --runtime=host \ --tags=key:value \ --command-executor-env=NP_API_KEY=$NP_API_KEY \ --command-executor-debug \ --webserver-enabled ``` :::tip `np-agent` also reads `NP_API_KEY` and `NP_LOG_LEVEL` from your environment if they're already set. ::: Here's what those flags do: | Flag | What it does | |---|---| | `--api-key` | Authenticates the agent with nullplatform | | `--runtime` | Where the agent runs (`host` means directly on your machine) | | `--tags` | Metadata tags for filtering. Format: `key:value`, comma-separated | | `--command-executor-env` | Environment variables passed to the scripts the agent runs | | `--command-executor-debug` | Enables detailed output for each command the agent executes | | `--webserver-enabled` | Starts the agent's built-in web server | :::tip See all available flags with `np-agent --help`. ::: ### Use AWS SSM for the API key If you keep credentials in AWS SSM Parameter Store, the agent can read its API key from there instead of taking it as a CLI flag. This keeps the key out of your shell history and out of local files. You need: - An SSM parameter of type `SecureString` holding the API key (e.g. `/nullplatform/agent/api-key`). - AWS credentials available to your shell (`aws configure` or an active `AWS_PROFILE`) with `ssm:GetParameter` and `kms:Decrypt` on the parameter. Then replace `--api-key=$NP_API_KEY` with `--ssm-apikey-parameter`: ```bash np-agent \ --ssm-apikey-parameter=/nullplatform/agent/api-key \ --runtime=host \ --tags=key:value \ --webserver-enabled ``` The agent fetches the parameter at startup with decryption enabled and retries up to 5 times on transient errors. ### Optional flags **Log verbosity** controls how much detail the agent prints: ```bash --log-level=INFO ``` Options: `ERROR`, `WARNING`, `INFO` (default), `DEBUG` **Pretty-print logs** makes output easier to read (colorized, nicely formatted JSON): ```bash --log-pretty-print ``` ## Verify installation 1. In the platform UI, go to **Platform settings > Agents**. 2. Filter by tags or agent ID to find your new agent. If it shows up, you're all set. ## Next step: create a notification channel The agent is now installed and registered. Without a notification channel, it won't respond to any platform events. [Create a notification channel →](agent-channel.md) --- ## Install the agent URL: https://docs.nullplatform.com/docs/agent/installation The agent connects your environment to nullplatform so it can run actions, respond to events, and execute workflows inside your infrastructure. Setting it up takes three steps: 1. **[Authenticate the agent](authentication.md)**: create an API key with the **agent** and **ops** roles. 2. **Install the agent**: use Helm for production clusters, or run the binary locally for development. 3. **[Create a notification channel](agent-channel.md)**: connect the agent to platform events. Without this, the agent registers but stays idle. ## Which install method is right for me? | | [Install with Helm](install-helm.md) | [Local install](install-local.md) | |---|---|---| | **Best for** | Production and Kubernetes clusters | Local development and testing | | **Requires Kubernetes** | Yes | No | | **Setup time** | A few minutes | Under a minute | ## Before you start Both methods need an **API key with the agent and ops roles**. See [Authenticate the agent](authentication.md) to set one up. ## Pick your path - [Install with Helm](install-helm.md): for Kubernetes environments and production. - [Local install](install-local.md): run the agent directly in your terminal. No Kubernetes required. :::tip Installing on Kubernetes and pulling agent code from a private GitHub repo? See [Authenticate private repos with a GitHub App](install-helm-github-app.md) for a more secure alternative to personal access tokens. ::: --- ## The nullplatform agent URL: https://docs.nullplatform.com/docs/agent/overview The **nullplatform agent** is a lightweight service you can run anywhere, from your laptop to a Kubernetes cluster. It connects your environment securely to the nullplatform API, so you can automate tasks, monitor scopes and services, trigger actions, and handle event-based notifications without exposing your infrastructure. > ℹ️ This setup is privacy-first: **your data stays in your cluster**. > Only minimal metadata and telemetry are sent to nullplatform. ## Required setup path Follow this order so the agent can actually execute work: 1. **Create an API key** with the Agent and Ops role. 2. **Install the agent** and pass the API key as `NP_API_KEY`. 3. **Create an agent notification channel** that targets this agent. ## How it works The agent is a small binary that can run in many ways, on your local machine, in a container, or on a Kubernetes cluster (via Helm, Docker, or just the binary). Here's what it does: - Authenticates with the nullplatform API using your API key - Registers itself with optional tags (e.g. `environment:dev`, `team:secops`) - Polls the API for matching tasks, scopes, or triggers - Runs matching commands or workflows inside your environment - Optionally sends logs, telemetry, and results back to nullplatform > 🔒 The agent is **outbound-only**. It only needs to be able to connect to `agents.nullplatform.com` on port `443`. ### Minimal flow ```mermaid sequenceDiagram autonumber participant You as You participant Agent as nullplatform agent participant API as nullplatform API You->>Agent: Install with API key and tags Agent->>API: Register and poll for work API-->>Agent: Send action Agent->>Agent: Run local script / command Agent->>API: Return output and logs ``` ## Key features at a glance | Feature | Description | | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- | | **Tag-based routing** | Agents only respond to tasks that match their tags | | **Secure API integration** | Use API keys with fine-grained access | | **Notification channels** | Run local scripts in response to platform events, using a git-audited flow | | **Flexible runtime options** | Deploy the agent using Helm (K8s), Docker, binary, or serverless (e.g. Lambda) | | **Observability** | Capture logs, exit codes, and outputs from workflows inside your infra. (Note: the agent itself doesn’t expose internal monitoring) | | **Multiple deployment models** | Use in local dev (e.g. Minikube), cloud environments (e.g. EKS, GKE), or CI/CD pipelines | ## What’s next Explore the following pages to learn more: - [**Security model**](./security): Understand what nullplatform guarantees and what you control - [**Authentication**](./authentication): Configure API keys with NRN-scoped permissions for agents - [**Installation**](./installation): Install the agent with Helm and verify it's working - [**Notification channels**](./agent-channel): Let platform events trigger your local scripts --- ## Agent security model URL: https://docs.nullplatform.com/docs/agent/security The nullplatform agent runs inside your own infrastructure. This page covers the security guarantees nullplatform provides for the agent and what remains under your control as the operator. ## How it connects ```mermaid flowchart LR A["**Customer infrastructure**Agent lives in your infrastructure.Initiates a WebSocket connectiontoward nullplatform."] B["**nullplatform**Receives the connection anddelivers events to the agent."] C["**Customer local execution**The agent executes events inyour environment: infrastructure,applications, internal services."] A -- "WebSocket TLS (outbound)" --> B B -- "events" --> C classDef customer fill:#eef3fb,stroke:#274a86,color:#0b1e3f,stroke-width:1.5px; classDef np fill:#e6faf4,stroke:#00b894,color:#0b1e3f,stroke-width:1.5px; class A,C customer; class B np; linkStyle 0 stroke:#0b1e3f,stroke-width:2.5px; linkStyle 1 stroke:#00b894,stroke-width:2px; ``` > 🔒 Nullplatform never initiates a connection into your environment — traffic flows outbound only, from your infrastructure to the platform. ## Security guarantees - **Encrypted and authenticated connection:** All communication between the agent and the platform runs over WebSocket TLS. The agent authenticates before receiving any instruction. - **No access to customer credentials:** Nullplatform doesn't store or access your API keys or secrets. Credentials live entirely in your environment. - **Execution scoped by design:** The agent operates with a declared set of capabilities. Commands can only run from explicitly allowed paths. Path traversal, symlink attacks, and arbitrary execution are blocked at the agent code level. - **Repository-bound execution:** The agent configuration includes a fixed allowlist of repositories and branches. This list is immutable after the agent starts, so all executions happen exclusively from pre-approved code. - **Authenticated and authorized commands:** All commands sent to the agent API are authenticated and authorized by nullplatform over HTTPS. The agent receives structured commands and runs them only within the configured repositories, with protections against subshell injection and path manipulation. - **IAM-bounded execution:** The agent runs with the IAM roles you assign to it. It can only access the cloud resources those roles explicitly permit, which limits the blast radius of any executed action. ## Auditability and control You have full visibility into every action the agent takes within your infrastructure: commands executed, scripts run, and services accessed. Certain actions, such as deployment executions, can be gated by approval workflows. The agent won't proceed until the required approval is granted, giving teams an additional governance layer over sensitive operations. > 💡 **Tip:** You can deploy multiple agents simultaneously, one per namespace, application, or team, for granular access control and per-environment isolation. ## Responsibilities at a glance Nullplatform handles security of the platform. You handle security in your infrastructure. guarantees You manage Responsibility forsecurity of the agent TransportTLS transport with agent authentication Identity & accessCommand authentication and authorization over HTTPS Secrets & credentialsZero retention of customer credentials Execution boundaryExecution guardrails enforced in agent code Source allowlistImmutable repository allowlist at agent startup DistributionVersioned agent image via Docker Registry ResilienceReconnection with retry and backoff You Responsibility forsecurity in your infra Identity & accessIAM permissions following least-privilege Secrets & credentialsSecrets and API keys in your vault or Kubernetes Secrets Rotation & auditingCredential rotation and access auditing ObservabilityAudit logs and observability of agent actions ## Next steps - [Authenticate the agent](./authentication): create an API key and configure NRN-scoped permissions - [Install the agent](./installation): deploy the agent with Helm and verify it's running --- ## Agent-driven workflows URL: https://docs.nullplatform.com/docs/agent/use-cases The nullplatform agent makes it easy to automate tasks across your infrastructure. It listens for events from the platform and runs workflows inside your environment, securely and without the need for extra tooling. Here are some of the most common ways teams use the agent to simplify automation and keep things running smoothly. ## Run agent-backed scopes Scopes are pipelines you define in your repo. When triggered, the agent picks them up and runs them inside your infrastructure. - Write scopes in YAML and version them alongside your code - Use tags to control where each scope runs (like `env:prod` or `team:secops`) - Trigger them manually, from service events, or through notification channels - See logs and output directly in the nullplatform UI 👉 [Check out our agent-backed scopes docs](/docs/agent-backed-scopes/) ## Extend service behavior You can use the agent to react to service updates or lifecycle changes with extra tasks. - Run smoke tests or post-deploy checks after a service is updated - Clean up automatically when something fails - Add custom scripts using notification channels 👉 [Check out our services docs](/docs/services/getting-started) ## Stream telemetry and execution data While tasks run, the agent collects useful runtime data and can report it back to the platform. - Capture logs and metrics - Send live updates to the nullplatform UI - Push results to dashboards or external tools through the API 👉 [Check out our telemetry docs](/docs/telemetry/overview) ## Automate event-based workflows The agent can help you respond to platform activity without setting up brittle scripts. - Run tasks when deployments finish, scopes complete, or configs change - Target actions using tags and filters - Chain steps together to build more complete workflows > ✅ Event > Agent > Script or Scope > Notification channels help connect platform events to your infrastructure. --- ## ALB metrics publishing URL: https://docs.nullplatform.com/docs/agent-backed-scopes/alb-metrics-publishing AWS Application Load Balancers have hard limits on the number of rules and target groups they can hold. When multiple scopes share the same ALB through a common `group.name`, these limits can be reached silently — causing deployment failures with no prior warning. ALB metrics publishing solves this by reporting the current rule count and target group count to CloudWatch or Datadog after every deployment. With these metrics in place, you can set up alerts and catch capacity problems before they break a deploy. ## Why this matters Neither CloudWatch nor Datadog track ALB rule or target group counts natively. The only metrics AWS publishes for ALBs are operational (request count, latency, healthy hosts), not resource usage. | Resource | Limit | Adjustable | |----------|-------|------------| | Rules per ALB | 100 | Yes (via AWS Support) | | Target groups per ALB | 100 | **No** | The target group limit is especially dangerous because it can't be increased. Once you hit it, the only option is to split traffic across multiple ALBs. ## How it works A `publish_alb_metrics` script runs as a post-deploy step — right after ingress reconciliation — in every deployment workflow (initial, switch traffic, and finalize). It: 1. Queries the ALB via the AWS API to count non-default rules across all listeners and target groups. 2. Publishes the counts as custom metrics to your chosen backend. ```mermaid flowchart LR A[Deploy] --> B[Verify ingress] B --> C[Publish ALB metrics] C --> D[Wait for pods] C -->|CloudWatch| E["nullplatform/ApplicationELB"] C -->|Datadog| F["nullplatform.applicationelb.*"] ``` ## Configuration Add these settings to your scope's `values.yaml`: ```yaml ALB_METRICS_PUBLISH_ENABLED: true ALB_METRICS_PUBLISH_TARGET: cloudwatch # cloudwatch | datadog ``` ### CloudWatch When the target is `cloudwatch`, the script publishes two metrics to the `nullplatform/ApplicationELB` namespace: | Metric | Unit | Dimension | |--------|------|-----------| | `RuleCount` | Count | `ALBName` | | `TargetGroupCount` | Count | `ALBName` | **IAM permissions required:** ```json { "Effect": "Allow", "Action": [ "elasticloadbalancing:DescribeLoadBalancers", "elasticloadbalancing:DescribeListeners", "elasticloadbalancing:DescribeRules", "elasticloadbalancing:DescribeTargetGroups", "cloudwatch:PutMetricData" ], "Resource": "*" } ``` ### Datadog When the target is `datadog`, the script publishes two metrics via the Datadog API: | Metric | Tags | |--------|------| | `nullplatform.applicationelb.rule_count` | `alb_name`, `region` | | `nullplatform.applicationelb.target_group_count` | `alb_name`, `region` | The script uses the `DATADOG_API_KEY` and `DATADOG_SITE` environment variables. These are available automatically if you have a Datadog metrics provider configured in nullplatform. ## Setting up alerts ### CloudWatch alarms CloudWatch alarms require one alarm per ALB per metric. You can automate this with OpenTofu — the example below discovers all ALBs in the account automatically: ```hcl data "aws_lbs" "all" {} data "aws_lb" "details" { for_each = toset(data.aws_lbs.all.arns) arn = each.value } locals { alb_names = [ for lb in data.aws_lb.details : lb.name if lb.load_balancer_type == "application" ] } resource "aws_sns_topic" "cloudwatch_alarms" { name = "Cloudwatch-Alarms" } resource "aws_sns_topic_subscription" "email" { topic_arn = aws_sns_topic.cloudwatch_alarms.arn protocol = "email" endpoint = "your-team@company.com" } resource "aws_cloudwatch_metric_alarm" "alb_rule_count" { for_each = toset(local.alb_names) alarm_name = "alb-rule-count-${each.value}" comparison_operator = "GreaterThanThreshold" evaluation_periods = 1 metric_name = "RuleCount" namespace = "nullplatform/ApplicationELB" period = 60 statistic = "Maximum" threshold = 80 treat_missing_data = "notBreaching" alarm_actions = [aws_sns_topic.cloudwatch_alarms.arn] dimensions = { ALBName = each.value } } resource "aws_cloudwatch_metric_alarm" "alb_target_group_count" { for_each = toset(local.alb_names) alarm_name = "alb-target-group-count-${each.value}" comparison_operator = "GreaterThanThreshold" evaluation_periods = 1 metric_name = "TargetGroupCount" namespace = "nullplatform/ApplicationELB" period = 60 statistic = "Maximum" threshold = 80 treat_missing_data = "notBreaching" alarm_actions = [aws_sns_topic.cloudwatch_alarms.arn] dimensions = { ALBName = each.value } } ``` After applying, AWS sends a confirmation email — accept it to start receiving alerts. :::note New ALBs added after the initial apply need another `tofu apply` to get their alarms. ::: ### Datadog monitors Datadog monitors can cover all ALBs with just two resources, since they group by the `alb_name` tag. New ALBs are detected automatically without re-applying. ```hcl resource "datadog_monitor" "alb_rule_count" { name = "ALB Rule Count High" type = "metric alert" message = "ALB {{alb_name.name}} has {{value}} rules. @your-team@company.com" query = "max(last_5m):max:nullplatform.applicationelb.rule_count{*} by {alb_name} > 80" notify_no_data = false monitor_thresholds { critical = 80 } } resource "datadog_monitor" "alb_target_group_count" { name = "ALB Target Group Count High" type = "metric alert" message = "ALB {{alb_name.name}} has {{value}} target groups. @your-team@company.com" query = "max(last_5m):max:nullplatform.applicationelb.target_group_count{*} by {alb_name} > 80" notify_no_data = false monitor_thresholds { critical = 80 } } ``` :::tip Datadog monitors support flexible notification targets — you can use `@slack-channel`, `@pagerduty-service`, or email addresses. ::: ## Deployment logs On a successful publish, you'll see a single log line: ``` ✓ ALB metrics published to CloudWatch (rules: 23, target_groups: 11) ``` When the feature is disabled, there's no output at all. Errors appear as warnings and don't block the deployment: ``` ⚠ ALB metrics: could not find ALB [k8s-nullplatform-internet-facing] ⚠ ALB metrics: failed to publish to CloudWatch ⚠ ALB metrics: DATADOG_API_KEY not set ``` ## AWS ALB limits reference | Resource | Default limit | Adjustable | |----------|--------------|------------| | ALBs per region | 50 | Yes | | Listeners per ALB | 50 | Yes | | Rules per ALB | 100 | Yes | | Target groups per ALB | 100 | **No** | | Targets per ALB | 1,000 | Yes | | Target groups per region | 3,000 | Yes | --- ## Configurable HTTP ports URL: https://docs.nullplatform.com/docs/agent-backed-scopes/containers/configurable-http-ports Containers scopes can bind the main HTTP listener to any port and expose extra HTTP ports alongside it. Both options live under the **Exposed Ports** section of the scope form. ## What it solves Not every application binds to `8080`, and some need to expose more than one HTTP listener. With configurable HTTP ports you can: - Pick the port your application binds to inside the container, with `8080` as the default - Add extra HTTP listeners next to the main one, each with its own service and ingress Typical use cases: a metrics endpoint on a different port, a webhook receiver, or a legacy app that binds to `9090` instead of `8080`. ## Prerequisites The traffic-manager sidecar image must be version **1.5.0 or later**. Older images ignore the new `UPSTREAM_PORT` environment variable and continue to route to `8080`, which breaks any scope that sets a custom main HTTP port. > ℹ️ Make sure the cluster pulls a compatible image before enabling the feature on production scopes. ## Configuration ### Main HTTP port The `main_http_port` capability sets the port your application binds to inside the container. Nullplatform threads it through the Service, Ingress, container port, liveness/readiness/startup probes, and the traffic-manager sidecar automatically. No additional changes are required. | Property | Value | |---|---| | Default | `8080` | | Valid range | `1024` to `65535` | | Required | Yes (default is applied if you don't set it) | :::info Ports below `1024` are reserved for privileged processes and are not allowed. ::: Send a [PATCH request](/docs/api/scope-update) to update the scope capabilities: 1. Open the scope form in the nullplatform UI. 2. Under **ADVANCED → Exposed Ports**, set **Main HTTP Port** to the value your application binds to. 3. Save and deploy. The platform reconciles ingress, service, and probes automatically. ```bash np scope patch --id 1234 --body '{ "capabilities": { "main_http_port": 9090 } }' ``` ```bash curl -L -X PATCH 'https://api.nullplatform.com/scope/1234' \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer ' \ -d '{ "capabilities": { "main_http_port": 9090 } }' ``` ### HTTP additional ports The `additional_ports` capability accepts entries of type `HTTP` (in addition to the existing `GRPC` type). Each HTTP additional port creates its own Service, Ingress, and traffic-manager sidecar. The port becomes reachable externally over HTTPS at the same hostname as the scope, on the configured port. A scope at `https://my-app.example.com` with an additional port `9090` is reachable at `https://my-app.example.com:9090`. SSL termination happens at the ALB using the same wildcard certificate as the main listener, so no extra TLS configuration is required. Existing GRPC additional ports keep working without changes. 1. Open the scope form in the nullplatform UI. 2. Under **ADVANCED → Exposed Ports → Additional Ports**, add an entry. 3. Set **type** to `HTTP` and **port** to the value your application binds to. 4. Save and deploy. ```bash np scope patch --id 1234 --body '{ "capabilities": { "main_http_port": 9090, "additional_ports": [ { "port": 9091, "type": "HTTP" } ] } }' ``` ```bash curl -L -X PATCH 'https://api.nullplatform.com/scope/1234' \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer ' \ -d '{ "capabilities": { "main_http_port": 9090, "additional_ports": [ { "port": 9091, "type": "HTTP" } ] } }' ``` ## ALB capacity considerations Each additional port (HTTP or GRPC) opens a dedicated listener on the shared Application Load Balancer. The main scope ingress keeps using the standard `:80` and `:443` listeners, which are shared across all scopes by hostname. Additional ports are not shared between scopes: each port number gets its own listener. AWS enforces a hard quota of **50 listeners per ALB**. Nullplatform validates the listener count before each deployment and rejects the deploy if it would exceed `ALB_MAX_LISTENERS` (default `48`, which leaves 2 slots of safety margin). If a deploy fails with a listener capacity error, the options are: - Reduce the number of additional ports across the scopes sharing the ALB - Move one or more scopes to a separate ALB - Request an AWS service-quota increase for listeners per ALB (limited adjustability) :::info Listeners are cleaned up automatically. The AWS Load Balancer Controller owns the listener lifecycle: when the last Ingress referencing a particular listener is deleted, the controller removes it from the ALB. Deleting a deployment is enough to reclaim its listener slots. If multiple scopes share the same additional port number on the same ALB, the listener stays until all of them are deleted. ::: ## Backward compatibility The change is fully backward-compatible: - Scopes that don't set `main_http_port` keep using `8080` everywhere. No migration is required. - Existing additional ports configured as `GRPC` continue to work without changes. - The traffic-manager image defaults `UPSTREAM_PORT` to `8080` when the env is missing, so an upgraded image with un-upgraded scope templates behaves like the old image. ## Next steps - [Containers scope overview](/docs/agent-backed-scopes/containers): cluster setup, supported providers, and the full feature list - [CPU and memory limits](/docs/agent-backed-scopes/containers/cpu-and-memory-limits): configure resource requests and limits for the Containers scope - [Scope configurations](/docs/agent-backed-scopes/scope-configurations): manage namespace, networking, deployment, and security settings --- ## K8s: Allowing CPU and memory to grow with demand URL: https://docs.nullplatform.com/docs/agent-backed-scopes/containers/cpu-and-memory-limits :::warning This page explains how to tune CPU and memory resource limits in a way that can introduce unstable behavior. We strongly recommend you stick to the default behavior. ::: ## Default behavior By default, nullplatform **fixes** the initial CPU and memory (`resources.requests`), and the maximum allowed usage (`resources.limits`) for your application. We recommend this as it favors stability by trading off resource usage. ## Danger zone: allowing CPU and memory to grow Nullplatform also lets you specify **different initial and maximum CPU and memory** parameters for your application, so `resources.requests` can be smaller than `resources.limits`, letting the application request more CPU and memory as it goes. Raising a limit above its request might allow you to use fewer resources in some situations, but it dangerously changes how Kubernetes schedules, throttles, and evicts your pod. ## How to configure limits higher than request In the **scope edit** section, `RAM Memory` (the request) sits at the top. `CPU Millicores`, `CPU Millicores Limit`, and `RAM Memory Limit` all live in the **ADVANCED → Resources** tab. Both limit fields default to `Same as request` and validate that `limit ≥ request` at save time. ## Risks ### QoS class downgrade Kubernetes assigns every pod a Quality of Service (QoS) class based on how its requests and limits compare. The QoS class decides which pods get killed first when a node runs out of resources. | Configuration | QoS class | Eviction priority | Recommendation | |---|---|---|---| | `request == limit` for all containers | **Guaranteed** | Last to be evicted | Recommended (default) | | `request != limit` for some resource | **Burstable** | Evicted before Guaranteed | Not recommended, described in this document | A Kubernetes Containers scope with limits at the defaults renders a **Guaranteed** pod. The moment you raise either limit above its request, the pod drops to **Burstable**, first in line if the node needs to free resources to keep critical workloads alive. :::warning A Burstable pod doesn't fail more often on a healthy cluster. It fails more often on a stressed one, exactly when you most want it to stay up. ::: ### Memory overcommit and OOMKill When you allow resources to grow, **memory is the dangerous one**. CPU contention just slows things down, but **memory contention kills processes**. If `limits.memory > requests.memory`, the scheduler can place the pod on a node where only the request is currently available. The pod is then allowed to grow up to its limit, but the node may not actually have that much free memory anymore if other Burstable pods on the same node also expanded. When this happens, one of two things kills your pod: 1. **OOMKill**: Your container exceeded `limits.memory`. The kernel kills the process and `RestartCount` increases. 2. **Node-pressure eviction**: The node ran out of memory globally. The kubelet picks Burstable pods that exceed their request first, even if they're still under their limit. :::warning Setting `limits.memory > requests.memory` is the most common cause of "my pod was killed while I didn't change anything." Reach for it only when you have a clear, intermittent reason to need more memory than your steady state. ::: ### CPU throttling CPU limits cause latency problems. When a container hits its CPU limit, the Linux kernel throttles it via CFS quotas, pausing the process for the rest of the scheduling period (default: 100 ms). A service with `requests.cpu: 200m` and `limits.cpu: 500m` can look fine on average but show p99 latency spikes in the seconds whenever it crosses the limit for a single window. :::tip For most steady-state workloads, leaving CPU limit at `Same as request` is the safer default. Raise it only after you've measured a real burst pattern, and watch `container_cpu_cfs_throttled_seconds_total` after the change. ::: ## Next steps - [Containers scope overview](/docs/agent-backed-scopes/containers): cluster setup, supported providers, and the full feature list. - [Diagnose](/docs/agent-backed-scopes/diagnose/overview): built-in checks that flag scopes whose pods are being evicted or restarted. --- ## Resource naming URL: https://docs.nullplatform.com/docs/agent-backed-scopes/containers/resource-naming Every Containers scope creates a set of Kubernetes objects: a Deployment, a Service, an HPA, a Secret, sometimes a PodDisruptionBudget, plus the Ingress or HTTPRoute that routes traffic to it. Nullplatform supports three strategies for naming those objects: **IDs** (the default) builds names from numeric ids, **qualified** from the application and scope slugs, and **custom** from a pattern you write. ## What it solves - Identify an application and scope straight from a pod, Deployment or Service name. - Match what your team already sees in dashboards, alerts and `kubectl` output. - Keep your own naming convention if you have one, without leaving nullplatform. ## Strategies Pick a strategy on the `container-orchestration` provider with `naming.strategy`. It applies to every Containers scope under that provider's NRN. ### IDs This is the default (`ids`). Objects are named from the raw scope and deployment ids. ``` deployment: d-123456-789012 hpa: hpa-d-123456-789012 scope ingress: k-8-s-production-123456-internet-facing ``` Nothing changes for an existing scope unless you choose another strategy. ### Qualified Objects take the application and scope slugs, followed by the numeric id. ```yaml naming: strategy: qualified ``` ``` deployment: checkout-api-production-789012 service: checkout-api-production-789012 hpa: checkout-api-production-789012 scope ingress: checkout-api-production-123456 ``` Every object of a deployment shares one name. Kubernetes names are unique per kind, so a Deployment, a Service, an HPA and a PodDisruptionBudget can all be called the same thing without colliding. The one exception is the pair of Secrets a scope creates, since those are the same kind: the one holding file parameters is suffixed `-files`. Additional ports keep a `-http-` or `-grpc-` suffix on their Services for the same reason. Deployment-level objects use `application`, `scope` and the deployment id. Scope-level objects (the Ingress, HTTPRoute and serving certificate) use `application`, `scope` and the scope id. ### Custom Write the pattern yourself. Each `{...}` is a path into the deployment context, so a pattern can use any field that context carries, including your scope's capabilities. ```yaml naming: strategy: custom deployment_pattern: "{.namespace.slug}-{.application.slug}-{.deployment.id}" scope_pattern: "{.application.slug}-{.scope.slug}-{.scope.id}" ``` ``` deployment: payments-checkout-api-789012 scope ingress: checkout-api-production-123456 ``` Defining only one of the two patterns is fine. The other falls back to what `qualified` would produce. #### Available paths The most useful ones: | Path | Example value | |---|---| | `{.account.slug}` | `acme` | | `{.namespace.slug}` | `payments` | | `{.application.slug}` | `checkout-api` | | `{.scope.slug}` | `production` | | `{.scope.dimensions.environment}` | `production` | | `{.deployment.id}` | `789012` | | `{.scope.id}` | `123456` | | `{.release.semver}` | `1.4.2` | Any scalar field in the deployment context works, so `{.scope.capabilities.visibility}` or a capability of your own is equally valid. #### Pattern rules A few constraints keep a pattern from producing a name Kubernetes rejects, or one that collides with another object. **Separate placeholders with a single hyphen.** A literal prefix or suffix is fine, so `edge-{.application.slug}-{.deployment.id}` works, but anything other than `-` between two placeholders is rejected. **Start with something alphabetic.** Kubernetes names must begin with a letter, so a pattern that starts with a numeric field is rejected. Put a slug first. **Use plain field access.** Only dotted paths like `.scope.capabilities.tenant` and bracketed keys like `.labels["team-name"]` are accepted. Pipes, filters and variable bindings are not. **Every path has to resolve.** A path that finds nothing fails the deploy rather than rendering an empty segment. This matters for fields that not every scope carries, such as a dimension or an optional capability: a pattern built around `{.scope.dimensions.environment}` works for scopes that define that dimension and breaks the ones that don't. Since the strategy applies to every scope under the provider's NRN, pick fields they all have. **The unique id is added if you leave it out.** A deployment pattern needs `{.deployment.id}` and a scope pattern needs `{.scope.id}`; without one, a new deployment would overwrite the previous one's objects. If your pattern already has it, anywhere, it's used exactly where you put it. If it doesn't, it's appended and a warning tells you the pattern that was actually used, so the deploy succeeds and you still find out. ## Name length Kubernetes caps object names at 63 characters, and nullplatform derives pod names from the Deployment name by appending a suffix. Deployment-level names are therefore budgeted at 46 characters by default, and scope-level names at 52. When a name would exceed its budget, the slugs are shortened evenly until it fits: ``` application: customer-notifications-dispatcher scope: production-canary-eu-west deployment: customer-notificati-production-canary-e-789012 ``` Numeric ids are never shortened, so names stay unique no matter how much trimming happens. ## Existing objects are never renamed Changing a scope's strategy, or editing a `custom` pattern, does not rename anything that already exists. Renaming an Ingress or HTTPRoute would mean creating a new one while the old one keeps routing, which causes downtime. So before naming a scope-level object, nullplatform looks for the live one in the cluster and keeps the name it already has. An existing scope keeps its original names for as long as it lives. A scope created after you set the strategy gets the new ones. Deployment-level objects work differently, and they don't need freezing: every deployment creates a fresh set of them, so the new strategy simply applies to the next deployment. The blue deployment during a rollback is the exception, and nullplatform finds its live objects rather than recomputing their names, so a rollback after a strategy change still points at objects that exist. ## Next steps - [Containers scope overview](/docs/agent-backed-scopes/containers): cluster setup, supported providers, and the full feature list - [Scope configurations](/docs/agent-backed-scopes/scope-configurations): manage namespace, networking, deployment, and security settings - [Configurable HTTP ports](/docs/agent-backed-scopes/containers/configurable-http-ports): set a custom main HTTP port and expose additional HTTP listeners - [CPU and memory limits](/docs/agent-backed-scopes/containers/cpu-and-memory-limits): configure resource requests and limits for the Containers scope --- ## Containers scope URL: https://docs.nullplatform.com/docs/agent-backed-scopes/containers The Containers scope runs your application as Docker containers on Kubernetes. It's the default scope type in nullplatform and the right choice for web applications, APIs, microservices, and any long-running workload. ## When to use it Use the Containers scope when your application runs as a Docker container and serves HTTP traffic, such as web apps, APIs, or microservices that need autoscaling and zero-downtime deployments. ## Supported cloud providers The Containers scope works with the following managed Kubernetes services: | Cloud provider | Kubernetes service | |---|---| | AWS | [EKS](/docs/providers/supported-integrations/container-orchestration/eks-configuration) | | Azure | [AKS](/docs/providers/supported-integrations/container-orchestration/aks-configuration) | | Azure | ARO (Azure Red Hat OpenShift) | | Google Cloud | [GKE](/docs/providers/supported-integrations/container-orchestration/gke-configuration) | | Oracle Cloud | [OKE](/docs/providers/supported-integrations/container-orchestration/oke) | ## What it includes The Containers scope provides built-in support for: - **Autoscaling and resource management**: scale pods based on CPU, memory, or custom metrics, and set resource requests and limits per container - **Blue-green deployments and traffic routing**: deploy new versions alongside the current one, split traffic between versions, and roll back instantly - **Health checks**: configure liveness and readiness probes to ensure your application is healthy - **Public and private access**: expose your application to the internet or keep it internal to your account - **Diagnostics**: run built-in health checks on your scopes to identify issues ## How it works When you create a Containers scope, nullplatform provisions Kubernetes resources in your cluster through the [nullplatform agent](/docs/agent/overview): 1. The scope is created and the agent receives a `create` action. 2. The agent provisions Kubernetes resources (Deployment, Service, Ingress, HPA). 3. On each deploy, the agent rolls out new container images using the configured deployment strategy. 4. Traffic is routed to healthy pods automatically. ```mermaid flowchart LR A[Build pushed] --> B[Deploy action] B --> C[Agent provisions resources] C --> D[Pods running] D --> E[Traffic routed] ``` ## Get started To set up a Containers scope, you need a Kubernetes cluster, [Helm](https://helm.sh/docs/intro/install/), [Gomplate](https://docs.gomplate.ca/), the [nullplatform CLI](/docs/cli/), and an [API key](/docs/authorization/api-keys) with agent roles. The setup process follows these steps: 1. Install the nullplatform agent in your cluster using Helm 2. Clone the [scopes repository](https://github.com/nullplatform/scopes) and set `SERVICE_PATH=k8s` 3. Configure your environment variables (API key, NRN, environment) 4. Run the `./configure` script to register the scope schema, actions, and notification channel 5. Create your first Containers scope from the UI The [Kubernetes setup tutorial](/docs/tutorials/kubernetes-prod-ready) provides the full walkthrough with commands and checkpoints. ## Customization The Containers scope covers the most common patterns out of the box. If you need to customize its behavior, for example to adjust deployment workflows or add organization-specific logic, see [Override workflows](/docs/agent-backed-scopes/overrides) in the Advanced section. ## Next steps - [Configurable HTTP ports](/docs/agent-backed-scopes/containers/configurable-http-ports): set a custom main HTTP port and expose additional HTTP listeners - [Resource naming](/docs/agent-backed-scopes/containers/resource-naming): choose how the Kubernetes objects of a scope are named - [Scope configurations](/docs/agent-backed-scopes/scope-configurations): manage namespace, networking, deployment, and security settings from the UI - [Deployment strategies](/docs/deployments/strategies): configure blue-green, rolling, or canary deployments - [Diagnose](/docs/agent-backed-scopes/diagnose/overview): run built-in diagnostics to identify scope issues - [Scheduled tasks](/docs/agent-backed-scopes/scheduled-tasks): run periodic jobs on Kubernetes using CronJobs - [Static files](/docs/agent-backed-scopes/static-files): deploy SPAs and static websites with CDN distribution --- ## Register the scope URL: https://docs.nullplatform.com/docs/agent-backed-scopes/craft-scopes/register-scope Once you've defined your scope as a service, you need to register it in your organization so it becomes available for use. You can do this using the [CLI](/docs/cli/) or the [API](/docs/api/scope-type-create). The `name` and `description` you set are shown to developers, and each one can be up to 60 characters long. ```bash np scope type create \ --body '{ "nrn": "organization=1:account=2:namespace=3", "type": "custom", "name": "My scope", "status": "active", "description": "Description of scope", "provider_type": "service", "provider_id": "5f21c3d8-6b2a-4e3f-9b62-18a7c91e7d3b" // the ID of your scope specification }' ``` ```bash curl -X POST "https://api.nullplatform.com/scope_type" \ -H "Content-Type: application/json" \ -d '{ "nrn": "organization=1:account=2:namespace=3", "type": "custom", "name": "My scope", "status": "active", "description": "Description of scope", "provider_type": "service", "provider_id": "5f21c3d8-6b2a-4e3f-9b62-18a7c91e7d3b" // the ID of your scope specification }' ``` :::note Request details In this example: - the scope is available to developers with access to account `2` in your organization `1`. - it's associated with the scope specification with ID `5f21c3d8-6b2a-4e3f-9b62-18a7c91e7d3b`. ::: --- ## Lifecycle action specifications URL: https://docs.nullplatform.com/docs/agent-backed-scopes/craft-scopes/scope-action-spec :::note Before you start If you haven't already, read about [scopes' main concepts](/docs/agent-backed-scopes/overview). Also, keep these references handy: - **Scope Service Action Specification API** * [Our API reference](/docs/service-action-specification-api-index) - **Schemas** - [JSON Schema (external link)](https://json-schema.org/learn/getting-started-step-by-step) - [Additional keywords supported by nullplatform](/docs/json-ui-schema/json-schema) - [UI Schema (external link)](https://jsonforms.io/docs/uischema/) - [How UI schema is integrated in nullplatform](/docs/json-ui-schema/overview) ::: ## Design your action specifications Now that you have a scope specification, bring it to life by designing actions that nullplatform will invoke at different points in the scope's lifecycle. Here are some key design questions: | Question | Guidance | | -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | Which input parameters are required to create the scope? | This defines the contents of the JSON schema under the `attributes` field. | | What optional actions should be available? | Think about actions that support specific operations, like adjusting traffic percentage during blue-green deployments. | ### Lifecycle: execution flow The execution flow for lifecycle actions is illustrated below: ```mermaid graph TB; Step1["Create scope"] -.-> Step0["The first deployment uses initial strategy"]:::note -.-> Initial; subgraph Initial["Initial deployment process"] direction LR Step20["Start initial"] note1["No switch traffic nor rollback here, as there is no old version running"] end subgraph BlueGreen["Blue green deployment process"] direction TB note4["After a blue green is finalized or roll-backed, new ones can be created"]~~~blank1[" "]~~~blank2[" "] Step30["Start blue green"]-->Step31["Switch traffic"]; Step31["Switch traffic"]-->Step32["Finalize blue green"]; Step31["Switch traffic"]-->Step33["Roll back deployment"]; note3["Switch traffic is executed with different `desired_traffic` values "]~~~blank4[" "]~~~blank5[" "] end Initial -.-> note2["Further deployments use the blue green strategy"] note2 -.-> BlueGreen classDef box fill:#cbd5e1,stroke:#94a3b8,stroke-width:1.4px,color:#0f172a; classDef note fill:#111827,stroke:#475569,color:#e2e8f0; classDef invisible fill:none,stroke:none; class note1,note2,note3,note4,note5 note class Initial,BlueGreen box classDef blank fill:none,stroke:none; class blank1,blank2,blank3,blank4,blank5 blank ``` ## Define all required actions Most scopes require at least these actions: - **create** or **delete** the scope - Handle deployments, like triggering initial deployments or finalizing blue-green ones In addition, you can define **optional** custom actions to extend your scope’s functionality. This means that you'll likely have to design and create several actions for a scope. Your actions should use one of the following types: `create`, `update`, or `custom`. ### Action specification reference | Name | Action type | Parameters | Description | Required | | ----------------------- | ----------- | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------- | -------- | | **Create scope** | `create` | `scope_id` | Triggered during scope creation to provision infrastructure independent of app versions (e.g., DNS). | Yes | | **Start initial** | `custom` | `scope_id`, `deployment_id` | Starts an initial deployment, typically the first or one after a scope restart. | Yes | | **Start blue green** | `custom` | `scope_id`, `deployment_id` | Initiates a blue-green deployment. | Yes | | **Switch traffic** | `custom` | `scope_id`, `deployment_id`, `desired_traffic` | Adjusts traffic percentage during blue-green deployments. | No | | **Finalize blue green** | `custom` | `scope_id`, `deployment_id` | Cleans up old infrastructure after blue-green deployment completion. | Yes | | **Rollback deployment** | `custom` | `scope_id`, `deployment_id` | Reverts to the previous deployment and removes the new infrastructure. | Yes | | **Delete deployment** | `custom` | `scope_id`, `deployment_id` | Deletes deployment infrastructure when the active scope is stopped. | No | | **Wait for instances** | `custom` | `scope_id`, `deployment_id`, `ready` | Waits for instances to be ready before proceeding with deployment operations. | No | | **Update scope** | `update` | `scope_id` | Updates the scope’s infrastructure if changes are required. | No | | **Delete scope** | `custom` | `scope_id` | Deletes app-version-independent infrastructure during scope deletion. | Yes | ## Parameters Each action must define the following: - **`parameters`**: inputs required to execute the action (as a JSON schema) :::note UI Schema See the [UI schema reference section](/docs/json-ui-schema/overview) for more information on how to control the fields in the UI. ::: ## Creating action specifications To define the actions for your scope, use the [CLI](/docs/cli/) or [API](/docs/service-action-specification-api-index). While not strictly required, most scopes rely on actions to create, update, or delete the scope and to manage scope workflows. When using the UI, nullplatform automatically looks up the corresponding action specifications and creates instances as needed. If you're integrating programmatically, you’ll need to trigger these actions explicitly. :::info Action types Use the `type` field on the action specification to distinguish between `create`, `update`, and `custom` actions. ::: ### Create scope ```bash curl -L -X POST "https://api.nullplatform.com/service_specification/$service-spec/action_specification" \ -H "Content-Type: application/json" \ -d '{ "name": "Create scope", "type": "create", "parameters": { // defines the input requirements for the action "schema": { "type": "object", "required": ["scope_id"], // Required parameter to create a scope "properties": { "scope_id": { "type": "string" } } }, "values": {} }, "results": { "schema": { "type": "object", "properties": {} }, "values": {} } }' ``` ```bash np service specification action specification create \ --serviceSpecificationId $service-spec-id \ --body '{ "name": "Create scope", "type": "create", "parameters": { // defines the input requirements for the action "schema": { "type": "object", "required": ["scope_id"], // required parameter to create a scope "properties": { "scope_id": { "type": "string" } } }, "values": {} }, "results": { "schema": { "type": "object", "properties": {} }, "values": {} } }' ``` ### Start initial ```bash curl -L -X POST "https://api.nullplatform.com/service_specification/$service-spec/action_specification" \ -H "Content-Type: application/json" \ -d '{ "name": "Start initial", "type": "custom", "parameters": { // defines the input requirements for the action "schema": { "type": "object", "required": ["scope_id", "deployment_id"], // required parameters to start initial "properties": { "scope_id": { "type": "string" }, "deployment_id": { "type": "string" } } }, "values": {} }, "results": { "schema": { "type": "object", "properties": {} }, "values": {} } }' ``` ```bash np service specification action specification create \ --serviceSpecificationId $service-spec-id \ --body '{ "name": "Start initial", "type": "custom", "parameters": { // defines the input requirements for the action "schema": { "type": "object", "required": ["scope_id", "deployment_id"], // required parameters to start initial "properties": { "scope_id": { "type": "string" }, "deployment_id": { "type": "string" } } }, "values": {} }, "results": { "schema": { "type": "object", "properties": {} }, "values": {} } }' ``` ### Start blue green ```bash curl -L -X POST "https://api.nullplatform.com/service_specification/$service-spec/action_specification" \ -H "Content-Type: application/json" \ -d '{ "name": "Start blue green", "type": "custom", "parameters": { // defines the input requirements for the action "schema": { "type": "object", "required": ["scope_id", "deployment_id"], // required parameters to start blue green "properties": { "scope_id": { "type": "string" }, "deployment_id": { "type": "string" } } }, "values": {} }, "results": { "schema": { "type": "object", "properties": {} }, "values": {} } }' ``` ```bash np service specification action specification create \ --serviceSpecificationId $service-spec-id \ --body '{ "name": "Start blue green", "type": "custom", "parameters": { // defines the input requirements for the action "schema": { "type": "object", "required": ["scope_id", "deployment_id"], // required parameters to start blue green "properties": { "scope_id": { "type": "string" }, "deployment_id": { "type": "string" } } }, "values": {} }, "results": { "schema": { "type": "object", "properties": {} }, "values": {} } }' ``` ### Finalize blue green ```bash curl -L -X POST "https://api.nullplatform.com/service_specification/$service-spec/action_specification" \ -H "Content-Type: application/json" \ -d '{ "name": "Finalize blue green", "type": "custom", "parameters": { // defines the input requirements for the action "schema": { "type": "object", "required": ["scope_id", "deployment_id"], // required parameters to finalize blue green "properties": { "scope_id": { "type": "string" }, "deployment_id": { "type": "string" } } }, "values": {} }, "results": { "schema": { "type": "object", "properties": {} }, "values": {} } }' ``` ```bash np service specification action specification create \ --serviceSpecificationId $service-spec-id \ --body '{ "name": "Finalize blue green", "type": "custom", "parameters": { // defines the input requirements for the action "schema": { "type": "object", "required": ["scope_id", "deployment_id"], // required parameters to finalize blue green "properties": { "scope_id": { "type": "string" }, "deployment_id": { "type": "string" } } }, "values": {} }, "results": { "schema": { "type": "object", "properties": {} }, "values": {} } }' ``` ### Rollback deployment ```bash curl -L -X POST "https://api.nullplatform.com/service_specification/$service-spec/action_specification" \ -H "Content-Type: application/json" \ -d '{ "name": "Roll back deployment", "type": "custom", "parameters": { // defines the input requirements for the action "schema": { "type": "object", "required": ["scope_id", "deployment_id"], // required parameters to roll back deployment "properties": { "scope_id": { "type": "string" }, "deployment_id": { "type": "string" } } }, "values": {} }, "results": { "schema": { "type": "object", "properties": {} }, "values": {} } }' ``` ```bash np service specification action specification create \ --serviceSpecificationId $service-spec-id \ --body '{ "name": "Roll back deployment", "type": "custom", "parameters": { // defines the input requirements for the action "schema": { "type": "object", "required": ["scope_id", "deployment_id"], // required parameters to roll back deployment "properties": { "scope_id": { "type": "string" }, "deployment_id": { "type": "string" } } }, "values": {} }, "results": { "schema": { "type": "object", "properties": {} }, "values": {} } }' ``` ### Delete deployment ```bash curl -L -X POST "https://api.nullplatform.com/service_specification/$service-spec/action_specification" \ -H "Content-Type: application/json" \ -d '{ "name": "Delete deployment", "type": "custom", "parameters": { // defines the input requirements for the action "schema": { "type": "object", "required": ["scope_id", "deployment_id"], // required parameters to delete deployment "properties": { "scope_id": { "type": "string" }, "deployment_id": { "type": "string" } } }, "values": {} }, "results": { "schema": { "type": "object", "properties": {} }, "values": {} } }' ``` ```bash np service specification action specification create \ --serviceSpecificationId $service-spec-id \ --body '{ "name": "Delete deployment", "type": "custom", "parameters": { // defines the input requirements for the action "schema": { "type": "object", "required": ["scope_id", "deployment_id"], // required parameters to roll back deployment "properties": { "scope_id": { "type": "string" }, "deployment_id": { "type": "string" } } }, "values": {} }, "results": { "schema": { "type": "object", "properties": {} }, "values": {} } }' ``` ### Update scope ```bash curl -L -X POST "https://api.nullplatform.com/service_specification/$service-spec/action_specification" \ -H "Content-Type: application/json" \ -d '{ "name": "Update scope", "type": "update", "parameters": { // defines the input requirements for the action "schema": { "type": "object", "required": ["scope_id"], // required parameters to update deployment "properties": { "scope_id": { "type": "string" } } }, "values": {} }, "results": { "schema": { "type": "object", "properties": {} }, "values": {} } }' ``` ```bash np service specification action specification create \ --serviceSpecificationId $service-spec-id \ --body '{ "name": "Update scope", "type": "update", "parameters": { // defines the input requirements for the action "schema": { "type": "object", "required": ["scope_id"], // required parameters to update deployment "properties": { "scope_id": { "type": "string" } } }, "values": {} }, "results": { "schema": { "type": "object", "properties": {} }, "values": {} } }' ``` ### Delete scope ```bash curl -L -X POST "https://api.nullplatform.com/service_specification/$service-spec/action_specification" \ -H "Content-Type: application/json" \ -d '{ "name": "Delete scope", "type": "custom", "parameters": { // defines the input requirements for the action "schema": { "type": "object", "required": ["scope_id"], // required parameters to delete deployment "properties": { "scope_id": { "type": "string" } } }, "values": {} }, "results": { "schema": { "type": "object", "properties": {} }, "values": {} } }' ``` ```bash np service specification action specification create \ --serviceSpecificationId $service-spec-id \ --body '{ "name": "Delete scope", "type": "custom", "parameters": { // defines the input requirements for the action "schema": { "type": "object", "required": ["scope_id"], // required parameters to delete deployment "properties": { "scope_id": { "type": "string" } } }, "values": {} }, "results": { "schema": { "type": "object", "properties": {} }, "values": {} } }' ``` ### Wait for instances ```bash curl -L -X POST "https://api.nullplatform.com/service_specification/$service-spec/action_specification" \ -H "Content-Type: application/json" \ -d '{ "name": "Wait for instances", "type": "custom", "parameters": { // defines the input requirements for the action "schema": { "type": "object", "required": ["scope_id", "deployment_id"], // required parameters to wait for instances "properties": { "scope_id": { "type": "string" }, "deployment_id": { "type": "string" }, "ready": { "type": "boolean", "description": "Indicates whether instances should be ready" } } }, "values": {} }, "results": { "schema": { "type": "object", "properties": {} }, "values": {} } }' ``` ```bash np service specification action specification create \ --serviceSpecificationId $service-spec-id \ --body '{ "name": "Wait for instances", "type": "custom", "parameters": { // defines the input requirements for the action "schema": { "type": "object", "required": ["scope_id", "deployment_id"], // required parameters to wait for instances "properties": { "scope_id": { "type": "string" }, "deployment_id": { "type": "string" }, "ready": { "type": "boolean", "description": "Indicates whether instances should be ready" } } }, "values": {} }, "results": { "schema": { "type": "object", "properties": {} }, "values": {} } }' ``` ## Run scope actions in parallel By default, scope lifecycle actions are executed sequentially to ensure safety and predictability. However, some lifecycle operations can safely run in parallel. With the `parallelize` flag, you can configure certain **custom scope actions** to run concurrently, improving deployment speed and throughput when actions don’t depend on each other. **When to use parallel execution** Parallel execution is valuable when lifecycle actions are independent and don’t modify the same resource at the same time. Typical use cases include: - **Blue-green deployments:** Triggering multiple rollout steps or health checks simultaneously. - **Traffic management:** Adjusting traffic for multiple services or endpoints in parallel. - **Infrastructure operations:** Running instance readiness checks (`wait-for-instances`) concurrently across deployments. > ℹ️ **Best practice:** Only parallelize lifecycle actions that are **idempotent** or operate on **isolated** resources. Keep sequential execution for steps like `switch traffic` or `rollback deployment`, where order and consistency matter. ### How to enable parallel execution To mark a lifecycle action as parallelizable, set `"parallelize": true` in its action specification: ```json { "actions": { "wait-for-instances": { "parallelize": true }, "switch-traffic": { "parallelize": false } } } ``` - `parallelize: true` → the action can run alongside other actions. - `parallelize: false` → the action must complete before others start. This gives you fine-grained control over which lifecycle steps can safely overlap. > 📖 See our [API reference](/docs/api/service-specification-actions-create) for more info. --- ## Scope specification URL: https://docs.nullplatform.com/docs/agent-backed-scopes/craft-scopes/scope-spec Agent-backed scopes use service specifications to define their name, category, visibility, and allowed parameters. :::note Before you start If you haven't done it yet, read about [scopes' main concepts](/docs/agent-backed-scopes/overview). Also, have these handy references ready: * **Service Specification API** * [Our API reference](/docs/service-specification-api-index) * **Schemas** * [JSON Schema (external link)](https://json-schema.org/learn/getting-started-step-by-step) | [Additional keywords supported by nullplatform](/docs/json-ui-schema/json-schema) * [UI Schema (external link)](https://jsonforms.io/docs/uischema/) | [How UI schema is integrated in nullplatform](/docs/json-ui-schema/overview) ::: ## Design your scope service specification Before you create the scope, ask yourself a few questions that we'll present in a table, so you can see how your answers impact your scope specification: | Question | Guidance | | ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | What's the name for the scope? | Choose a natural name (e.g., CronJob) and set it into the `name` field. | | Which properties do I need to define for the scope? | These are the properties that define the scope. They are usually a combination of input parameters plus values obtained from the cloud provider upon service creation. Set these as a JSON schema in the `attributes` field. | | Where in the organization is the scope going to be available? | A good default is to start by making the service visible everywhere with the value `organization=your-org-id:account=*`. Set your choice in the `visible_to` array. | ## Create the scope specification To set up an agent-backed scope: - Create a new service of type `scope`. - Declare the type of asset it will deploy. You can create a scope specification using our [CLI](/docs/cli/) or [API](/docs/api/service-specification-create). Here’s an example of a scope that accepts `docker-image` assets and includes a mandatory `warmup` parameter. ```bash np service specification create \ --body '{ "name": "My scope name", "slug": "my-scope", "type": "scope", "visible_to": [ "organization=1:account=2" // replace with a real NRN ], "attributes": { "schema": { "type": "object", "required": [ "warmup" ], "properties": { "asset_type": { "type": "string", "export": false, "default": "docker-image", "readOnly": true }, "warmup": { "type": "boolean", "export": false, "readOnly": false } } }, "values": {} }, "selectors": { "category": "Deployment Services", "imported": false, "provider": "AWS", "sub_category": "Agent-backed Scope" } } ``` ```bash curl -L 'https://api.nullplatform.com/service_specification' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer the-token' \ -d '{ "name": "My scope name", "type": "scope", "visible_to": [ "organization=1:account=2" // replace with your NRN ], "attributes": { "schema": { "type": "object", "required": [ "warmup" ], "properties": { "asset_type": { "type": "string", "export": false, "default": "docker-image", "readOnly": true }, "warmup": { "type": "boolean", "export": false, "readOnly": false } } }, "values": {} }, "selectors": { "category": "Deployment Services", "imported": false, "provider": "AWS", "sub_category": "Agent-backed Scope" } } ``` :::info Service details The JSON schema under `attributes` determines the scope's attributes, fields, and layout in the UI. These properties appear in the **See details** view in the nullplatform interface. You can further enhance the rendering of that section using UI Schema. ::: #### Some request details - `attributes`: This field is mandatory and contains a JSON schema where you define: - the asset type for this scope. - the parameters that developers will have to set when creating or editing your scope. - `asset_type`: This parameter determines the types of assets that can be deployed to the scope. - **Single-asset scopes:** For scopes supporting a single asset type (e.g., `docker-images`), define `asset_type` as an **optional** parameter with a default value. - **Multi-asset scopes:** For scopes supporting multiple asset types, make the `asset_type` a **required** parameter so developers can select the type. - Additional parameters: You can include optional parameters in your scope configuration. They can be: - **required**: Developers must provide values for these when creating a new scope. - **optional**: These can have default values and are not mandatory. :::info As of April 2025, optional parameters are not displayed in the UI but can still be configured using [our API](https://docs.nullplatform.com/api/service-specification-create). ::: --- ## Add Diagnose to your scopes URL: https://docs.nullplatform.com/docs/agent-backed-scopes/diagnose/add-diagnose-to-scopes Diagnose runs automated checks on your scopes and deployments. **If your scope doesn't have the Diagnose tool** yet, or **you want to build your own diagnose flow**, you need to register the right **action specifications** on your scope specification. ## How Diagnose connects to your scope Diagnose relies on three components working together: 1. **Action specifications** register the Diagnose actions so they appear in the UI. 2. **Workflows** define the execution flow when an action is triggered. 3. **The diagnose module** contains the context builder and individual checks. ```mermaid flowchart LR A[Action spec] -->|triggers| B[Workflow] B --> C[Build context] C --> D[Run checks in parallel] D --> E[Report results] ``` ## Action specifications The agent registers two action specifications that make Diagnose available in the UI: ### Diagnose Scope Runs diagnostics against a scope. This action appears on the **manage** and **performance** tabs. ```json { "name": "Diagnose Scope", "slug": "diagnose-scope", "type": "diagnose", "retryable": true, "parameters": { "schema": { "type": "object", "required": ["scope_id"], "properties": { "scope_id": { "type": "number", "readOnly": true, "visibleOn": ["read"] } } }, "values": {} }, "results": { "schema": { "type": "object", "required": [], "properties": {} }, "values": {} }, "annotations": { "show_on": ["manage", "performance"], "runs_over": "scope" } } ``` ### Diagnose Deployment Runs diagnostics against a specific deployment. This action appears on the **deployment** tab and includes both `scope_id` and `deployment_id`. ```json { "name": "Diagnose Deployment", "slug": "diagnose-deployment", "type": "diagnose", "retryable": true, "parameters": { "schema": { "type": "object", "required": ["scope_id", "deployment_id"], "properties": { "scope_id": { "type": "number", "readOnly": true, "visibleOn": ["read"] }, "deployment_id": { "type": "number", "readOnly": true, "visibleOn": ["read"] } } }, "values": {} }, "results": { "schema": { "type": "object", "required": [], "properties": {} }, "values": {} }, "annotations": { "show_on": ["deployment"], "runs_over": "deployment" } } ``` ### Annotations: `show_on` and `runs_over` Diagnose action specifications use `annotations` to tell the UI **where to display the action button** and **what entity the action targets**. Without annotations, the UI won't know where to render the Diagnose tool or what data to pass to the workflow. #### `show_on` Controls which sections of the UI display the Diagnose tool. The value is an array, so a single action can appear in multiple places. | Value | Where the button appears | |---|---| | `manage` | The **Manage** tab of the scope | | `performance` | The **Performance** tab of the scope | | `deployment` | The **Deployment** detail view | For example, `"show_on": ["manage", "performance"]` makes the Diagnose Scope button visible in both the Manage and Performance tabs of the scope. #### `runs_over` Tells the UI what entity the action runs against. This determines which parameters the UI passes to the workflow when a user clicks the button. | Value | What the action targets | Parameters passed | |---|---|---| | `scope` | The scope as a whole | `scope_id` | | `deployment` | A specific deployment | `scope_id` and `deployment_id` | A scope-level action uses the scope's label selector to gather resources, while a deployment-level action narrows the scope further with the deployment ID. #### How they work together The combination of `show_on` and `runs_over` gives you precise control over the Diagnose experience: ```json "annotations": { "show_on": ["manage", "performance"], "runs_over": "scope" } ``` This registers a button on the Manage and Performance tabs that runs diagnostics at the scope level. ```json "annotations": { "show_on": ["deployment"], "runs_over": "deployment" } ``` This registers a button on the Deployment view that runs diagnostics scoped to that specific deployment. :::tip Match `runs_over` with the right parameters If `runs_over` is `"deployment"`, make sure `deployment_id` is in the `required` array of your `parameters` schema. The UI will pass it automatically, but the workflow will fail if the schema doesn't expect it. ::: ### Register the action specifications If Diagnose isn't available on your scope yet, you need to register these action specifications manually. Use the [CLI or API](/docs/api/service-specification-actions-create). Here's an example for the Diagnose Scope action: ```bash np service specification action specification create \ --serviceSpecificationId $service-spec-id \ --body '{ "name": "Diagnose Scope", "slug": "diagnose-scope", "type": "diagnose", "retryable": true, "parameters": { "schema": { "type": "object", "required": ["scope_id"], "properties": { "scope_id": { "type": "number", "readOnly": true, "visibleOn": ["read"] } } }, "values": {} }, "results": { "schema": { "type": "object", "required": [], "properties": {} }, "values": {} }, "annotations": { "show_on": ["manage", "performance"], "runs_over": "scope" } }' ``` ```bash curl -L -X POST 'https://api.nullplatform.com/service_specification/$service-spec-id/action_specification' \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer ' \ -d '{ "name": "Diagnose Scope", "slug": "diagnose-scope", "type": "diagnose", "retryable": true, "parameters": { "schema": { "type": "object", "required": ["scope_id"], "properties": { "scope_id": { "type": "number", "readOnly": true, "visibleOn": ["read"] } } }, "values": {} }, "results": { "schema": { "type": "object", "required": [], "properties": {} }, "values": {} }, "annotations": { "show_on": ["manage", "performance"], "runs_over": "scope" } }' ``` :::note The `$service-spec-id` is the ID of your [scope specification](/docs/agent-backed-scopes/craft-scopes/scope-spec). You can find it in the response when you create the specification, or by [listing your existing ones](/docs/api/scope-list). ::: Follow the same pattern to register the Diagnose Deployment action specification. See the [lifecycle action specifications](/docs/agent-backed-scopes/craft-scopes/scope-action-spec) guide for more details on how action specs work. ## The diagnose workflow When you trigger a Diagnose action, the agent runs a workflow with three steps: ### 1. Load utility functions The workflow starts by sourcing helper functions that every check relies on. These include output formatting (`print_success`, `print_error`, `print_warning`), resource validation (`require_pods`, `require_services`, `require_ingresses`), and the `update_check_result` function that reports check outcomes. ### 2. Build context Before any checks run, the `build_context` script collects a **point-in-time snapshot** of the Kubernetes cluster. This snapshot ensures every check evaluates the same data, even if the cluster changes during the run. The context includes: | Resource | What it collects | |---|---| | Pods | All pods matching the scope labels | | Services | Services associated with the deployment | | Endpoints | Service endpoint information | | Ingresses | Ingress resources for the scope | | Secrets | Metadata only, no secret data | | IngressClasses | Available ingress classes in the cluster | | Events | Recent Kubernetes events | | ALB controller | Controller pods and logs, if applicable | All data is stored as JSON files in a `data/` directory. Checks read from these files instead of making direct API calls, which keeps the run fast and consistent. ### 3. Execute checks The workflow uses an `executor` step that discovers and runs all check scripts in parallel from three folders: - `diagnose/service/` -- Kubernetes Service checks - `diagnose/scope/` -- pod and workload checks - `diagnose/networking/` -- ingress and routing checks For each check, the executor: 1. Runs a **before_each** hook that sets the check status to "running" and notifies the UI. 2. Executes the check script. 3. Runs an **after_each** hook that collects the result and sends it to the UI. This is what makes the Diagnose UI update in real time as each check completes. ## What the workflow looks like Both the scope and deployment workflows share the same structure: ```yaml continue_on_error: true include: - "$SERVICE_PATH/values.yaml" steps: - name: load_functions type: script file: "$SERVICE_PATH/diagnose/utils/diagnose_utils" output: - name: update_check_result type: function - name: notify_results type: function - name: build context type: script file: "$SERVICE_PATH/diagnose/build_context" output: - name: CONTEXT type: environment - name: LABEL_SELECTOR type: environment - name: diagnose type: executor before_each: name: notify_check_running type: script file: "$SERVICE_PATH/diagnose/notify_check_running" after_each: name: notify_check_results type: script file: "$SERVICE_PATH/diagnose/notify_diagnose_results" folders: - "$SERVICE_PATH/diagnose/service" - "$SERVICE_PATH/diagnose/scope" - "$SERVICE_PATH/diagnose/networking" ``` The `continue_on_error: true` flag ensures that if one check fails, the remaining checks still run. This is important because you want to see the full picture, not just the first failure. ## What you need to enable Diagnose If you're adding Diagnose to your scope or verifying that it's properly configured: 1. **Action specifications** must be registered for the scope. The agent handles this automatically during setup, but you can also [register them manually](#register-the-action-specifications). 2. **Workflow files** must exist at `k8s/scope/workflows/diagnose.yaml` and `k8s/deployment/workflows/diagnose.yaml`. 3. **The diagnose module** must be present at `k8s/diagnose/` with its full directory structure. 4. **kubectl access** must be available inside the agent, since `build_context` uses it to collect cluster data. For standard Kubernetes agents, all of this is included out of the box. You only need to think about these components if you're customizing the agent or building support for a new runtime. ## Build your own diagnose flow If the built-in Diagnose doesn't fit your scope — or your scope runs on a runtime other than Kubernetes — you can build your own diagnose flow from scratch. This means writing your own data collection, checks, and result reporting, while following the contract that the UI expects. ### What the UI needs from your workflow The Diagnose UI renders results based on a specific structure. Your workflow must produce results that match this contract, regardless of what your checks actually inspect. Each check must produce a JSON result with these fields: ```json { "name": "My Check", "description": "What this check validates", "category": "Networking", "status": "success", "evidence": { "pods_checked": 3 }, "logs": ["✓ All endpoints healthy", "✓ Port 8080 open"], "start_at": "2025-01-15T10:30:00Z", "end_at": "2025-01-15T10:30:02Z" } ``` | Field | Description | |---|---| | `name` | Display name shown in the UI | | `description` | Short explanation of what the check validates | | `category` | Groups checks in the UI. Use any string (e.g., `Networking`, `Scope`, `Database`) | | `status` | One of `success`, `failed`, `warning`, or `skipped` | | `evidence` | Arbitrary JSON with data that helps explain the result | | `logs` | Array of strings shown as check logs in the UI | | `start_at` / `end_at` | ISO 8601 timestamps for the check execution window | ### How results reach the UI At the end of the workflow, results must be sent to the nullplatform API using the [service action update](/docs/api/service-action-update) endpoint. The payload groups checks by category: ```json { "results": { "categories": [ { "category": "Networking", "summary": { "pending": 0, "running": 0, "success": 2, "failed": 1, "warning": 0, "skipped": 0 }, "checks": [ { "name": "...", "status": "...", "..." : "..." } ] } ] } } ``` The built-in diagnose flow handles this automatically through the `notify_results` function, which reads all check result files and groups them. If you're building your own flow, you need to build and send this payload yourself. ### Wiring it up To create a complete custom diagnose flow: 1. **Register the action specifications** with `"type": "diagnose"` as shown [above](#register-the-action-specifications). Use [`annotations`](#annotations-show_on-and-runs_over) to control where the action button appears in the UI and what entity it targets. 2. **Create the workflow file** at the expected path. The filename must match the action type: - `k8s/scope/workflows/diagnose.yaml` for scope-level diagnosis - `k8s/deployment/workflows/diagnose.yaml` for deployment-level diagnosis 3. **Collect your data.** Replace `build_context` with whatever data collection makes sense for your runtime. The built-in flow uses `kubectl` to snapshot Kubernetes resources, but yours could query a database, call an API, or read metrics. 4. **Write your checks.** Each check should analyze the collected data and report a result with `status` and `evidence`. See [Create a custom check](/docs/agent-backed-scopes/diagnose/create-custom-check) for the check structure and conventions. 5. **Report results** by sending the grouped result payload to the nullplatform API at the end of the workflow. :::tip Use the built-in flow as a reference The standard Kubernetes diagnose workflow is a good template for your own. It follows the same collect → check → report pattern described here. You can browse the source at [nullplatform/scopes — k8s/diagnose](https://github.com/nullplatform/scopes/tree/main/k8s/diagnose). ::: --- ## Built-in checks and error handling URL: https://docs.nullplatform.com/docs/agent-backed-scopes/diagnose/built-in-checks Diagnose runs a set of built-in checks to help you understand where a scope might be failing. Each check looks at a specific aspect of your scope and answers a clear question, such as whether traffic can reach the workload, whether pods are healthy, or whether services are configured correctly. This guide groups common troubleshooting actions by check category so you can find a resolution faster. ## How to use this guide When Diagnose reports a failure: 1. Identify the failed check and its category. 2. Open the check to review the explanation and logs. 3. Use the sections below to understand common causes and next steps. 4. Apply a fix, redeploy the scope, and then use Diagnose again if you want to confirm the redeployed scope is healthy. ## Networking issues Networking issues usually prevent traffic from reaching your scope.
Ingress Existence **What this usually means** No ingress resource was found for the scope. **Common causes** - Ingress was never created - Ingress exists in a different namespace - Labels do not match the scope **What to do next** - Verify that an ingress resource exists for the scope - Check that the ingress is in the correct namespace - Confirm ingress labels match the scope configuration
Ingress Class **What this usually means** The ingress references an invalid or missing ingress class. **Common causes** - Ingress class does not exist in the cluster - Ingress controller is not installed - Deprecated annotations are used instead of ingressClassName **What to do next** - Verify the ingress class exists - Confirm the ingress controller is running - Update the ingress to use a valid ingress class
Ingress Controller Sync **What this usually means** The ingress controller failed to reconcile the ingress. **Common causes** - Controller pods are not running - Backend services are missing or unhealthy - Cloud provider configuration errors **What to do next** - Check ingress controller logs - Verify backend services exist and have endpoints - Review cloud provider events related to load balancers
Ingress Backend Service **What this usually means** Ingress backends do not exist or have no healthy endpoints. **Common causes** - Service does not exist - Service selectors do not match pods - Pods are not ready **What to do next** - Verify the service exists - Check service selectors and pod labels - Review pod readiness status
TLS Configuration **What this usually means** TLS configuration for the ingress is invalid. **Common causes** - TLS secret does not exist - Certificate does not match hostnames - Certificate is expired or invalid **What to do next** - Verify the TLS secret exists - Check certificate validity and host coverage - Regenerate or update certificates if needed
ALB Capacity **What this usually means** The load balancer could not be provisioned or assigned capacity. **Common causes** - Subnet IP exhaustion - Invalid load balancer annotations - Cloud provider limits **What to do next** - Review cloud provider logs and events - Verify subnet capacity and configuration - Adjust load balancer annotations
Host & Path Rules **What this usually means** Your ingress rules have an issue that can break routing, even if the ingress resource exists. **Common causes** - No rules defined and no default backend configured - Invalid `pathType` (must be `Exact`, `Prefix`, or `ImplementationSpecific`) - Trailing `/` on a `Prefix` path (can cause unexpected routing behavior) - Duplicate host rules - Wildcard hosts without the right configuration **What to do next** - Define at least one rule or configure a default backend - Confirm every rule uses a valid `pathType` - Remove trailing slashes from `Prefix` paths - Consolidate duplicate host rules - Prefer explicit hostnames over wildcards when possible
## Scope issues Scope issues usually indicate problems with the workload itself.
Pod Existence **What this usually means** No pods were found for the scope. **Common causes** - Deployment failed to create pods - Pods failed to schedule - Pods were deleted or evicted **What to do next** - Check deployment status and events - Review scheduling errors - Verify resource availability
Container Crash Detection **What this usually means** One or more containers are repeatedly crashing. **Common causes** - Application startup errors - Missing configuration or secrets - Insufficient memory or CPU **What to do next** - Review container logs - Verify configuration and environment variables - Check resource limits and usage
Image Pull Status **What this usually means** Container images could not be pulled. **Common causes** - Incorrect image name or tag - Missing image pull credentials - Registry connectivity issues **What to do next** - Verify image name and tag - Check image pull secrets - Confirm registry access from the cluster
Memory Limits **What this usually means** Containers do not have memory limits configured. **Common causes** - Resource limits were omitted - Defaults were not applied **What to do next** - Add memory limits to container specifications - Review namespace limit ranges
Container Port Health **What this usually means** Containers are not listening on the ports defined in the deployment. **Common causes** - Application is bound to a different port than configured - Port binding failed (permissions, conflicts) - Wrong port set in the deployment spec - Environment variable for the port is missing or incorrect **What to do next** - Verify containerPort matches the application’s listen port - Check startup logs for port binding errors - Confirm port-related environment variables are set correctly - Test port binding from inside the container if needed
Health Probe Endpoints **What this usually means** Health check probes are not configured correctly or are failing. **Common causes** - Liveness or readiness probe endpoints do not exist - Probe paths return non-success status codes - Probe timeouts are too short for the application - Wrong HTTP headers or ports in probe configuration **What to do next** - Verify probe endpoints exist and return success codes - Check probe configuration matches application routes - Increase timeout or initial delay if the application needs more time to start - Review probe logs in pod events
Storage Mounting **What this usually means** Volumes failed to mount. **Common causes** - Missing persistent volume claims - Invalid storage class - Volume attachment errors **What to do next** - Verify PVCs exist and are bound - Check storage class configuration - Review node and CSI driver logs
Resource Availability **What this usually means** Your pods cannot be scheduled because the cluster does not currently have enough available capacity or scheduling rules prevent placement. **Common causes** - CPU or memory requests are higher than what any node can provide - Nodes are at capacity - Namespace resource quotas are exceeded - Taints and missing tolerations prevent scheduling - Node selector or affinity rules are too restrictive - Too many replicas requested for the current cluster size **What to do next** - Reduce CPU/memory requests in your deployment - Scale up the cluster by adding nodes - Relax node selector or affinity constraints - Check resource quotas and adjust if needed - Review node taints and add tolerations if appropriate - Consider priority classes if this workload must schedule ahead of others
## Service issues Service issues usually prevent internal traffic from reaching pods.
Service Existence **What this usually means** No service exists for the scope. **Common causes** - Service was not created - Service exists in a different namespace **What to do next** - Verify service exists - Confirm namespace and labels
Service Selector Match **What this usually means** Service selectors do not match any pods. **Common causes** - Label mismatches - Incorrect selector configuration **What to do next** - Compare service selectors with pod labels - Update selectors or labels as needed
Service Endpoints **What this usually means** The service has no healthy endpoints. **Common causes** - Pods are not ready - Selector mismatch - Network policies blocking traffic **What to do next** - Verify pod readiness - Review service selectors - Check network policies
Service Port Configuration **What this usually means** Service ports do not map correctly to container ports. **Common causes** - Incorrect targetPort - Container not listening on expected port **What to do next** - Verify container ports - Update service port configuration
Service Type Validation **What this usually means** The service type is invalid or unsupported in the current environment. **Common causes** - LoadBalancer not supported in the cluster - NodePort outside the allowed range **What to do next** - Update the service type to one your cluster supports - Verify cluster support and cloud provider configuration for the selected service type
## After applying a fix After you apply a fix, **redeploy the scope** so the change takes effect. Once the new deployment is live, you can run Diagnose again to confirm the issue is resolved on the updated scope. Diagnose is safe to run multiple times and works best as an iterative troubleshooting tool after each deployment. --- ## Create a custom check URL: https://docs.nullplatform.com/docs/agent-backed-scopes/diagnose/create-custom-check Diagnose ships with a set of [built-in checks](/docs/agent-backed-scopes/diagnose/built-in-checks) that cover common Kubernetes issues. You can browse the [source for existing checks](https://github.com/nullplatform/scopes/tree/main/k8s/diagnose) and extend Diagnose with custom checks to validate conditions specific to your setup. This guide walks you through the process of creating, registering, and testing a custom diagnostic check. ## How checks work Every diagnostic check is a bash script that: 1. Validates that the resources it needs exist (pods, services, ingresses). 2. Reads data from pre-collected JSON files. 3. Analyzes the data and prints findings. 4. Reports a result with a status and evidence. Checks don't make Kubernetes API calls. During a Diagnose run, the `build_context` step collects a snapshot of the cluster before any checks execute. Your check reads from that snapshot, which keeps results consistent and fast. ## Check statuses Each check must report exactly one of these statuses: | Status | Meaning | |---|---| | `success` | The check passed. No issues found. | | `failed` | The check found a problem that needs attention. | | `warning` | Something looks unusual but isn't necessarily broken. | | `skipped` | The check couldn't run because a prerequisite was missing (e.g., no pods exist). | ## Step 1: Create the check script Create a new file in the appropriate category folder under `k8s/diagnose/`: - `networking/` for ingress, routing, and load balancer checks - `scope/` for pod and workload checks - `service/` for Kubernetes Service checks The filename becomes the check identifier. Use `snake_case` with no file extension. ```bash touch k8s/diagnose/scope/my_custom_check chmod +x k8s/diagnose/scope/my_custom_check ``` ## Step 2: Write the check logic Here's the structure every check should follow: ```bash #!/bin/bash # Check: My Custom Check # Validates that [what this check does] # 1. Validate prerequisites require_pods || return 0 # 2. Read data from pre-collected files PODS=$(jq -r '.items[].metadata.name' "$PODS_FILE" 2>/dev/null | tr '\n' ' ') # 3. Analyze and report HAS_ISSUES=0 for POD_NAME in $PODS; do POD_INFO=$(jq --arg name "$POD_NAME" \ '.items[] | select(.metadata.name == $name)' \ "$PODS_FILE" 2>/dev/null) # Your validation logic here SOME_VALUE=$(echo "$POD_INFO" | jq -r '.spec.someField // empty') if [[ -z "$SOME_VALUE" ]]; then HAS_ISSUES=1 print_error "Pod $POD_NAME: Missing someField" print_action "Add someField to your deployment spec" else print_success "Pod $POD_NAME: someField is configured" fi done # 4. Report final result if [[ $HAS_ISSUES -eq 1 ]]; then update_check_result --status "failed" --evidence '{"checked": true}' else update_check_result --status "success" --evidence '{"checked": true}' fi ``` :::warning Always return 0 after require Use `require_pods || return 0` (not `return 1`). Returning 0 tells the executor the script ran successfully, even though the check was skipped. Returning 1 would cause the executor to treat it as an unexpected error. ::: ## Step 3: Register the check in the workflow Open the `workflow.yml` file in the same category folder and add your check: ```yaml # k8s/diagnose/scope/workflow.yml steps: # ... existing checks ... - name: My Custom Check description: Validates that [what it checks] category: Scope type: script file: "$SERVICE_PATH/diagnose/scope/my_custom_check" ``` The `category` field determines how the check is grouped in the UI. Use one of: - `Networking` for networking checks - `Scope` for scope checks - `K8s Service` for service checks The `name` and `description` are what users see in the Diagnose results. ## Available data files The `build_context` step exports these environment variables pointing to pre-collected JSON files: | Variable | Contents | |---|---| | `$PODS_FILE` | Pods matching the scope/deployment labels | | `$SERVICES_FILE` | Services matching the labels | | `$ENDPOINTS_FILE` | All endpoints in the namespace | | `$INGRESSES_FILE` | Ingresses matching the scope labels | | `$SECRETS_FILE` | Secret metadata (no secret data) | | `$INGRESSCLASSES_FILE` | Available IngressClasses in the cluster | | `$EVENTS_FILE` | Recent events in the namespace | | `$ALB_CONTROLLER_PODS_FILE` | ALB controller pod information | | `$ALB_CONTROLLER_LOGS_DIR` | Directory with ALB controller log files | You also have access to these context variables: | Variable | Contents | |---|---| | `$NAMESPACE` | The Kubernetes namespace | | `$SCOPE_ID` | The nullplatform scope ID | | `$DEPLOYMENT_ID` | The deployment ID (if running a deployment-level diagnosis) | | `$LABEL_SELECTOR` | Full label selector including scope and deployment | | `$SCOPE_LABEL_SELECTOR` | Label selector for scope only (`scope_id=...`) | ## Helper functions reference These functions are available to every check, loaded automatically from [`diagnose_utils`](https://github.com/nullplatform/scopes/blob/main/k8s/diagnose/utils/diagnose_utils). ### Output functions Use these to print formatted messages. They appear in the check logs that users see in the UI. ```bash print_success "All pods are healthy" # ✓ green print_error "Pod web-123 is not ready" # ✗ red print_warning "High restart count detected" # ⚠ yellow print_info "Checking pod web-123" # ℹ cyan print_action "Increase memory limits" # 🔧 cyan ``` ### Resource validation These functions check whether prerequisite resources exist. If they don't, the check is automatically set to "skipped" and the function returns 1. ```bash require_pods # Checks $PODS_FILE has items require_services # Checks $SERVICES_FILE has items require_ingresses # Checks $INGRESSES_FILE has items ``` Always call these before accessing the corresponding data files: ```bash require_pods || return 0 # Safe to read $PODS_FILE from here ``` ### Reporting results Call `update_check_result` once at the end of your check to report the final status. ```bash # Named parameters update_check_result --status "success" --evidence '{"pods_checked": 3}' # Positional parameters update_check_result "failed" '{"reason": "no healthy endpoints"}' ``` The `evidence` parameter must be valid JSON. It's stored with the check result and can contain any data that helps explain what the check found. ## Example: a complete custom check Here's a real-world example that validates containers have CPU limits configured: ```bash #!/bin/bash # Check: CPU Limits # Validates that all containers have CPU limits configured require_pods || return 0 PODS=$(jq -r '.items[].metadata.name' "$PODS_FILE" 2>/dev/null | tr '\n' ' ') TOTAL_CONTAINERS=0 MISSING_LIMITS=0 for POD_NAME in $PODS; do CONTAINERS=$(jq --arg name "$POD_NAME" ' .items[] | select(.metadata.name == $name) | .spec.containers[] | { name: .name, cpu_limit: (.resources.limits.cpu // null) } ' "$PODS_FILE" 2>/dev/null) while IFS= read -r CONTAINER; do CONTAINER_NAME=$(echo "$CONTAINER" | jq -r '.name') CPU_LIMIT=$(echo "$CONTAINER" | jq -r '.cpu_limit') TOTAL_CONTAINERS=$((TOTAL_CONTAINERS + 1)) if [[ "$CPU_LIMIT" == "null" ]]; then MISSING_LIMITS=$((MISSING_LIMITS + 1)) print_error "Pod $POD_NAME, container $CONTAINER_NAME: No CPU limit" print_action "Add resources.limits.cpu to the container spec" else print_success "Pod $POD_NAME, container $CONTAINER_NAME: CPU limit $CPU_LIMIT" fi done <<< "$(echo "$CONTAINERS" | jq -c '.')" done if [[ $MISSING_LIMITS -gt 0 ]]; then update_check_result --status "failed" \ --evidence "{\"total\":$TOTAL_CONTAINERS,\"missing_limits\":$MISSING_LIMITS}" else update_check_result --status "success" \ --evidence "{\"total\":$TOTAL_CONTAINERS,\"missing_limits\":0}" fi ``` Register it in `k8s/diagnose/scope/workflow.yml`: ```yaml - name: CPU Limits description: Validates that all containers have CPU limits configured category: Scope type: script file: "$SERVICE_PATH/diagnose/scope/cpu_limits_check" ``` ## Tips - **Read from files, not the API.** Never use `kubectl` inside a check. All data you need is in the pre-collected JSON files. - **Keep evidence useful.** Include counts, names, and specific values in your evidence JSON. This helps users understand the result without reading logs. - **Handle empty data gracefully.** Use `jq` with `// empty` or `// null` defaults to avoid errors when fields are missing. - **One concern per check.** Each check should validate a single condition. If you're checking multiple things, consider splitting into separate checks. - **Use the right category folder.** Place your check where it logically belongs. If it inspects pods, it goes in `scope/`. If it inspects ingress configuration, it goes in `networking/`. --- ## Real-world troubleshooting example URL: https://docs.nullplatform.com/docs/agent-backed-scopes/diagnose/diagnose-use-case This example mirrors a common failure: the deployment doesn't become healthy, Diagnose runs automatically, and you learn that the container isn't listening on the expected port (or that its memory limits are wrong). ## Scenario: misconfigured port You deploy a scope, but the rollout fails and Diagnose auto-runs. The Diagnose results show: - ❌ **Scope checks** show a failure - ✅ **Service checks** are successful - ✅ **Networking checks** are successful When you expand the failing Scope check, you see `container_port_health` failed. The check and logs say the application is not listening on port **8080** (the port configured in the deployment). ### Step 1: Confirm the signal In Diagnose, open `container_port_health` to review the explanation and logs. The log snippet points out the container is bound to a different port than the `containerPort` specified, so traffic can't reach the app. ### Step 2: Apply a fix Update your deployment so the app listens on the configured port: - Set the container to listen on `8080` (or update `containerPort` and probes to match the actual port). - If probes are involved, verify readiness/liveness probes use the same port and path. Save the change in your repo. ### Step 3: Redeploy and verify Redeploy the scope so the port change takes effect. After the new deployment is live, rerun Diagnose: - `container_port_health` now passes - Other checks complete successfully Traffic flows and the rollout succeeds. ## Another example: memory misconfiguration If the failure is about memory instead of ports, Diagnose may surface `memory_limits_check` or a related check. Fix it by adding or correcting memory limits/requests in the deployment, redeploy the scope, and rerun Diagnose to confirm the issue is resolved. ## Why this workflow works - Diagnose pinpoints misconfigurations (ports, memory) without guesswork. - You verify the fix on the redeployed scope, not on the failed rollout. - You can rerun Diagnose anytime after redeploying to confirm the new state. --- ## Diagnose URL: https://docs.nullplatform.com/docs/agent-backed-scopes/diagnose/overview **The diagnose tool** gives you a fast way to understand why a scope is not working as expected. Instead of guessing whether the problem is related to networking, pods, services, or configuration, **Diagnose runs automated checks on your agent-backed scope** and presents clear results, logs, and guidance so you know where to start. :::info Agent support Diagnose is on by default for new agents. If yours is older, upgrade the agent to enable the feature. ::: ## Diagnose your scope Diagnose is an action you can run from the UI on agent-backed scopes. When you run Diagnose, the agent: - Collects a snapshot of your scope’s Kubernetes resources - Runs a set of predefined checks - Groups results by area such as Networking, Scope, and Service - Shows which checks passed, which failed, and why Each check focuses on a specific condition. For example, Diagnose can tell you whether an ingress exists, whether pods are running and ready, or whether services have healthy endpoints. ## What happens when you run a diagnosis A Diagnose run follows a simple flow. First, Diagnose captures a snapshot of the Kubernetes resources related to the scope. This ensures all checks evaluate the same point in time. Then, the checks analyze that data and report their results independently. This keeps the experience fast and consistent, even as more checks are added over time. > 💡 **Note:** You do not need to configure anything. The full flow runs > automatically when you click **Diagnose** or when a scope fails. ## What Diagnose checks for you Diagnose runs default checks grouped into three categories. ### Networking Networking checks focus on ingress, routing, TLS, and load balancer configuration. They help you identify issues related to: - Ingress existence and class validation - Backend service connectivity - TLS certificate configuration - ALB capacity and synchronization checks ### Scope Scope checks focus on the workload itself. They surface problems like: - Image pull failures - Container crashes and restarts - Missing resource limits - Pod readiness issues and volume mounting errors ### Service Service checks validate service configuration and connectivity. They help ensure: - Service existence - Selectors match running pods - Endpoints are available - Ports are configured correctly Each check shows a clear status, explains what it found, and gives you access to logs for deeper investigation. --- ## Diagnose your scope URL: https://docs.nullplatform.com/docs/agent-backed-scopes/diagnose/run-diagnose-ui You can run **Diagnose directly from the nullplatform UI** to quickly understand why a scope is not behaving as expected. Running Diagnose does not change your scope or your cluster. It only inspects the current state and reports findings. ## Where to find Diagnose Diagnose is available as an **action** on agent-backed scopes. To run it: 1. Go to **Development > Scopes**. 2. Select the scope you want to diagnose and open the **Actions** menu. 3. Click **Diagnose**. This opens the Diagnose view and starts a new run immediately. :::tip Rerun Diagnose at any time After you apply a fix, **redeploy the scope first**. Once the new deployment is live, you can rerun Diagnose to confirm the issue is resolved. ::: ## What happens after you click Diagnose Once the run starts, Diagnose executes a series of checks on networking, the scope itself, and services, and shows progress in real time. Each section represents a different area of the system and runs its checks independently. This makes it easy to see which part of the scope is healthy and where issues are concentrated. ## Inspecting checks and logs When you open a check, you can: - See what the check validated - Review why it passed or failed - Inspect logs collected during execution Logs are especially useful when a check fails. They give you concrete evidence to understand the root cause and decide what to do next. :::info What Diagnose does not do Running **Diagnose does not** modify resources, restart workloads, or block deployments. It is safe to run as often as needed. ::: --- ## Scopes URL: https://docs.nullplatform.com/docs/agent-backed-scopes/ Agent-backed scopes let you extend nullplatform to support all kinds of deployment environments, whether you're shipping PL/SQL apps, static frontends, or something entirely custom. ## Stay up to date on scopes We maintain a dedicated changelog for scopes so you can follow every update, version by version. Find it in the [Scopes repository](https://github.com/nullplatform/scopes/blob/main/CHANGELOG.md). Other sample scopes (AWS Lambda, static files, and more) live in their own repositories, each with its own changelog. See the full list in [Sample scopes](/docs/agent-backed-scopes/sample-scopes). ## Why use agent-backed scopes? Agent-backed scopes give you the power to: - **Extend nullplatform** with your own runtime logic and custom infrastructure - **Keep the developer experience unified** — there's no UI difference between standard and agent-backed scopes - **Use the full platform toolkit** — scopes work with approvals, parameters, policies, entity hooks, and more - **Model your real-world environments** — including business-specific configs and deployment behaviors ## Scopes vs. services Agent-backed scopes are technically a type of service, but they’re meant for different use cases. - **Scopes** represent units of execution tied to an application's lifecycle (like a deployment or environment). - **Services** have broader lifecycles that aren’t always connected to a single application. > **Tip:** If your dependency is tightly coupled to how a specific app is deployed or promoted, a scope is likely the right fit. You can read more in our [services documentation](/docs/services/getting-started). ## Next steps - [What are scopes?](/docs/agent-backed-scopes/what-are-scopes): understand scopes, their lifecycle, and how they relate to applications. - [Scope types](/docs/agent-backed-scopes/scope-types): choose a scope type for your workload, including legacy types. - [Sample scopes](/docs/agent-backed-scopes/sample-scopes): every sample scope type, with its repository. - [Scope configurations](/docs/agent-backed-scopes/scope-configurations): configure agent-backed scopes from the UI using providers. - [Diagnose and troubleshoot](/docs/agent-backed-scopes/diagnose/overview): identify and fix issues with your scopes. --- ## Make behavior-based overrides URL: https://docs.nullplatform.com/docs/agent-backed-scopes/overrides/behavior If configuration overrides aren’t enough, for example, you need to **skip a step**, **add new logic**, or **replace how something works**, then it’s time to use a behavior override. Behavior overrides let you change how a scope runs by customizing its workflows. You can: - Skip or replace workflow steps - Add new scripts or logic - Inject behavior based on conditions (like platform type or scope kind) This is especially useful when you're building advanced scopes (like background workers or scheduled tasks) and need full control over runtime behavior. ## How it works Each scope defines its lifecycle as a set of **ordered YAML steps** (e.g., `create`, `initial`, `delete`). These workflows live in the base scope repo. You can override those steps by placing your own workflow files in the same path inside your **override repo**. When the agent detects these files, it uses them **instead of** the default ones. You can: - `action: skip` – ignore specific steps - `action: replace` – fully replace a step with a custom script - Insert your own named steps using `type: script`, `type: command`, etc. > **Note:** See the [lifecycle execution flow for scope actions](/docs/agent-backed-scopes/craft-scopes/scope-action-spec) > for more details. ### Repo structure Place workflow overrides under `scope/workflows/` or `deployment/workflows/` depending on the lifecycle action you're customizing. ``` your-override-repo/ ├── scope/ # Scope lifecycle actions │ └── my_domain_generator │ └── workflows/ │ ├── create.yaml │ ├── update.yaml │ └── .. ├── deployment/ │ └── build_tolerations │ └── workflows/ │ ├── initial.yaml │ ├── blue_green.yaml ├── values.yaml # Your configuration overrides └── .. ``` :::important After changing your override repo, you’ll need to [**refresh the agent sources**](/docs/agent/agent-refresh.md) so it pulls the latest code. ::: --- ## Example 1: Skip DNS configuration By default, the `create` lifecycle step includes logic to generate a DNS record. If you're building something that doesn’t need networking, like a background worker, you can skip this step. Create a `create.yaml` in your overrides repo: ``` your-override-repo/ ├── scope/ # Scope lifecycle actions │ └── workflows/ │ ├── create.yaml ``` And add a step like this: ```yaml steps: - name: networking action: skip ``` When this override is active, the `networking` step won't run during scope creation. --- ## Example 2: Add tolerations for arm64 deployments Say you want your application to run only on **arm64 nodes**. Kubernetes uses tolerations for this. You can inject them dynamically using a script override. #### What you’ll do: 1. Create a script that checks the asset's platform (e.g. `arm_64`) 2. Add tolerations to the deployment context if needed 3. Insert that script into the deployment workflows (`initial.yaml`, `blue_green.yaml`) **File structure** These are the files you'll need to create throughout the guide. ``` your-override-repo/ ├── deployment/ │ └── build_tolerations # your shell script │ └── workflows/ │ ├── initial.yaml │ ├── blue_green.yaml └── .. ``` - In your `build_tolerations` script: ```bash #!/bin/bash # Find the platform of the asset being deployed ASSET_PLATFORM=$(echo "$CONTEXT" | jq -r .asset.platform) MODIFIERS=$(echo "$CONTEXT" | jq .k8s_modifiers) # ARM64 assets require a toleration to target the correct Kubernetes node if [[ "$ASSET_PLATFORM" == "arm_64" ]]; then TOLERATIONS='[{"key": "architecture", "operator": "Equal", "value": "arm64", "effect": "NoSchedule"}]' echo "The asset is built for arm, adding tolerations to the deployment" echo "$TOLERATIONS" | jq . # Add the toleration to the modifiers and # store it in the context so it is included in the deployment manifest MODIFIERS=$(echo "$MODIFIERS" | jq --argjson tolerations "$TOLERATIONS" ' if .deployment then .deployment.tolerations = (.deployment.tolerations // []) + $tolerations else .deployment = {"tolerations": $tolerations} end ') CONTEXT=$(echo "$CONTEXT" | jq --argjson modifiers "$MODIFIERS" '.k8s_modifiers = $modifiers') fi ``` This script modifies the deployment context only if the target architecture is arm64. - Then, add the step in `initial.yaml`: ```bash steps: - name: build_tolerations type: script file: "$OVERRIDES_PATH/deployment/build_tolerations" after: build context ``` - Repeat the same step in `blue_green.yaml` if you're using both strategies. :::info `$OVERRIDES_PATH` points to the root of your override repo inside the agent. It’s set in your notification channel configuration as the `--override-path`. Example path: `/root/.np/your-org/your-repo` ::: In both workflows, we've added a `build_tolerations` step that runs your custom script right after the build context step, allowing the scope to inject tolerations dynamically during deployment. --- ## Example 3: Generate your own domain Scopes automatically generate a domain name during the create step. If you want to control the format of that domain, you can override just the step that generates it. **File structure** ``` your-override-repo/ ├── scope/ # Scope lifecycle actions │ └── my_domain_generator │ └── workflows/ │ ├── create.yaml └── .. ``` **Here’s how to do it:** Add the replace step in `create.yaml`. ```yaml steps: - name: networking steps: - name: generate domain action: replace type: script file: "$OVERRIDES_PATH/scope/my_domain_generator" ``` This tells the agent to replace the `generate` domain step inside the networking subworkflow with a step that runs the `my_domain_generator` script. Here’s a basic example of what the `my_domain_generator` script might look like: ```bash SCOPE_SLUG=$(echo $CONTEXT | jq .scope.slug -r) SCOPE_DOMAIN="$SCOPE_SLUG.acme_corp.com" # Notify nullplatform the new domain_name np scope patch --id "$SCOPE_ID" --body "{\"domain\":\"$SCOPE_DOMAIN\"}" # Add the scope domain to the current and export as a global environment variable CONTEXT=$(echo "$CONTEXT" | jq \ --arg scope_domain "$SCOPE_DOMAIN" \ '.scope.domain = $scope_domain') export SCOPE_DOMAIN ``` Now your scope uses a custom domain like `my-scope.acme_corp.com` instead of the default. --- ## What’s next: Combine both override types Now that you’ve seen both configuration and behavior overrides, you can combine them to build advanced, production-ready scopes. - [Scheduled tasks scope](/docs/agent-backed-scopes/scheduled-tasks): see both override types combined in a real example --- ## Make configuration-based overrides URL: https://docs.nullplatform.com/docs/agent-backed-scopes/overrides/configuration Configuration-based overrides let you change how a scope behaves by updating its default settings **without writing or editing any workflows or scripts**. All you need to do is add a `values.yaml` file to your override repo. This is a good option when you want to: - Tweak default behavior - Enable optional features - Swap in your own manifest templates ## How it works Each scope defines a set of configuration values it reads at runtime. When the scope runs, the agent checks if your override repo contains a `values.yaml` file. If it does, your values will be merged with the base scope’s configuration. Any matching keys in your file will override the default values. You can see the default config values for the Kubernetes scope here: [nullplatform/scopes/k8s/values.yaml](https://github.com/nullplatform/scopes/blob/main/k8s/values.yaml) :::important After changing your override repo, you’ll need to [**refresh the agent sources**](/docs/agent/agent-refresh.md) so it pulls the latest code. ::: ## Where to put `values.yaml` Your override file should be placed at the **root** of your override repo. ### Example repo structure ``` your-override-repo/ ├── values.yaml ``` > Need help setting up the repo and agent? > See the [environment setup guide](/docs/agent-backed-scopes/overrides/setup). --- ## Example 1: Add a label to your deployment The Kubernetes base scope supports customizing **labels and annotations** for any Kubernetes object it creates, such as Ingress, Service, or Deployment. For example, if you want to label every deployment with the business unit it belongs to, use the `K8S_MODIFIERS` key. In your override repo’s `values.yaml`: ```yaml configuration: K8S_MODIFIERS: deployment: labels: business_unit: payments ``` Now every deployment created by the scope will include this label. ```bash kubectl get deployment -l "business_unit=payments" ``` This can be useful for filtering deployments or integrating with monitoring tools. --- ## Example 2: Use a different manifest template The scope allows you to override the templates it uses to generate Kubernetes objects. This is helpful if you want to change how resources like Ingresses or Services are created. You can override the default manifest templates used by the scope. For example, if you want to replace the default `Ingress` with Gateway API and Istio’s `HTTPRoute`. In your override repo’s `values.yaml`: ```yaml configuration: INITIAL_INGRESS_PATH: "$SERVICE_PATH/deployment/templates/istio/initial-httproute.yaml.tpl" BLUE_GREEN_INGRESS_PATH: "$SERVICE_PATH/deployment/templates/istio/blue-green-httproute.yaml.tpl" ``` This replaces the default ingress templates for both the initial and blue-green deployment strategies. > The nullplatform Kubernetes scope includes example templates for this, check the > [Istio templates here](https://github.com/nullplatform/scopes/tree/main/k8s/deployment/templates/istio). --- ## Example 3: Enable IAM Role for Service Accounts (IRSA) The Kubernetes scope supports enabling IRSA. This feature lets you associate an AWS IAM Role with your scope, so your applications can access cloud resources securely (e.g. S3, DynamoDB). Here’s a simple example to enable IRSA and attach a policy: ```yaml IAM: ENABLED: true PREFIX: nullplatform-scopes ROLE: POLICIES: - TYPE: arn VALUE: arn:aws:iam::aws:policy/AmazonS3FullAccess ``` You can include multiple policies: - `TYPE: arn` (like in the example above) - `TYPE: inline`: where `VALUE` contains the full JSON of the policy - The scope also supports configuring an [IAM permissions boundary](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html) if you need to restrict maximum permissions. > For reference, see the [`values.yaml` file in our repo](https://github.com/nullplatform/scopes/blob/main/k8s/values.yaml). --- ## Example 4: Back up your manifests The scope supports storing backups of all generated manifests in an S3 bucket. This is useful for disaster recovery or audit trails. To enable this, configure the `MANIFEST_BACKUP` block in your override `values.yaml`: ```yaml MANIFEST_BACKUP: ENABLED: true TYPE: s3 BUCKET: nullplatform-k8s-backups PREFIX: cluster-name/manifests ``` Once enabled, all generated manifests will be stored automatically. --- ## Next up: Behavior overrides If configuration changes aren’t enough and you need to actually change what the scope does, you can create behavior overrides by modifying or extending the scope's workflows. - [Learn how to make behavior-based overrides](/docs/agent-backed-scopes/overrides/behavior). --- ## Overriding scope workflows URL: https://docs.nullplatform.com/docs/agent-backed-scopes/overrides/ Agent-backed scopes come with flexible defaults, but sometimes you'll want to change how a scope behaves without rewriting it from scratch. Overrides let you extend or adjust what a scope does by plugging in custom logic, parameters, or workflow changes. They’re ideal for cases where the base scope gets you *almost* there, but not quite. ## Why use overrides? Use an override when you want to - **tweak deployment behavior** without editing the base scope - **inject organization-specific logic** (e.g. tags, tolerations, domain generation) - **reuse a base scope** like `k8s`, but with your own configuration or steps ## Two types of overrides ### Configuration overrides These change **runtime values** by adding a `values.yaml` to your override repo. Use these when you want to: - Add labels or annotations - Swap out a manifest template - Enable features like IRSA, logging, or backups ### Behavior overrides These change **how workflows run**. You can skip, replace, or add steps to scope actions like `create` or `deploy`. Use these when you want to: - Remove a step like DNS setup - Add custom scripts based on platform - Replace domain generation or workload logic ## Full example Check out our hands-on case where we: - Disable networking - Use a scheduled task (`CronJob`) template - Add retry and logging logic ## Setting up your override environment Before applying overrides, you'll need to: - Choose a base scope to extend - Store your overrides in a Git repo - Install the nullplatform agent - Set up a notification channel with override support ## Next steps - [Make configuration-based overrides](./configuration.md) - [Make behavior-based overrides](./behavior.md) - [Scheduled tasks scope](/docs/agent-backed-scopes/scheduled-tasks): see how overrides are applied in practice - [Set up your override environment](./setup.md) - [Understand the base scope workflows](/docs/agent-backed-scopes/workflows-and-execution/index.md) --- ## Using multiple override layers URL: https://docs.nullplatform.com/docs/agent-backed-scopes/overrides/multiple-overrides Agent-backed scopes support using **multiple override layers**, allowing you to extend or customize behavior by stacking different override repositories. This gives you a flexible way to build on top of the base scope implementation without duplicating code. ## How it works You can specify multiple override repositories by passing more than one `--overrides-path` flag to the scope entrypoint. Each repository is loaded in order, creating a sequence of override layers. ```bash /root/.np/nullplatform/scopes/entrypoint \ --overrides-path=/root/.np/your-org/your-override-repo \ --overrides-path=/root/.np/your-org/your-second-override-repo ``` :::info Good to know Existing parameters, channels, and workflows do not require additional configuration. ::: ## How layers interact Override layers behave like a "stack": - Each override path can extend or replace behavior from earlier layers. - Overrides may add files, modify them, or completely replace logic defined previously. - This enables incremental customization without duplicating entire implementations. ## Priority rules Override priority is **last-one-wins**: The **last** override provided in the command takes precedence over earlier ones. This means that if multiple override repositories contain the **same file or logic**, the file from the *last* override path will overwrite the previous versions. ## Example setup Below is a common configuration that combines the base Kubernetes scope, the Scheduled Task override, and a custom organization-level override: ```bash /root/.np/nullplatform/scopes/entrypoint \ --service-path=/root/.np/nullplatform/scopes/k8s \ --overrides-path=/root/.np/nullplatform/scopes/scheduled_task \ --overrides-path=/root/.np/acme-corp/platform-resources/custom-scope-override ``` #### Breakdown of layers 1. **Base Kubernetes scope** Defines core lifecycle and deployment behavior. 2. **Scheduled Task override** Adds logic specific to scheduled workloads. 3. **Custom organizational override** Uses or adapts the Scheduled Task behavior with tenant-specific needs such as permissions, logging/APM configuration (e.g., Datadog), or specialized task logic. ## When to use multiple overrides This approach is especially helpful when a scope needs multiple kinds of customization without merging everything into a single repository. A typical example is Scheduled Tasks, where you extend the base Kubernetes behavior, apply the task-specific override, and then apply any additional customizations your team or tenant requires. The [Scheduled tasks scope](/docs/agent-backed-scopes/scheduled-tasks) page shows this pattern in detail. ## Best practices - Use overrides thoughtfully: since the last override overwrites the rest, keep your paths ordered with intent. - Keep responsibilities separate: aim for each override layer to have a clear purpose. - Document your layers: helps teams reason about what is being overridden and why. --- ## Setting up your override environment URL: https://docs.nullplatform.com/docs/agent-backed-scopes/overrides/setup If you already have an **agent-backed scope** running and want to extend its behavior, you'll need to prepare your environment so that nullplatform can find and execute your custom logic. This setup is required whether you're applying configuration tweaks or fully replacing workflows. ## 1. Choose a base scope to extend Overrides are always built on top of an existing scope. This guide assumes you’re extending our [prod-ready Containers scope](https://github.com/nullplatform/scopes/tree/main/k8s), but you can use any agent-backed scope as your base. > 💡 You won’t modify the base repository directly. Instead, you’ll reference it from your own override repository. ## 2. Prepare your override repository Your override repository stores the configuration and workflow files that will extend the base scope. Create a Git repository and include the following files: - `values.yaml` — for configuration (runtime value) overrides. - Workflow files — for behavior overrides (e.g., `create.yaml`, `deploy.yaml`). Your repo might look like this: ```json your-override-repo/ ├── values.yaml ├── scope/workflows/ │ ├── create.yaml │ └── delete.yaml ├── deployment/workflows/ │ └── initial.yaml └── ... ``` > Each file in your override repo extends or replaces part of the base scope logic. ## 3. Update your notification channel Your **notification channel** tells the agent when and where to run commands. Because you already have a channel configured for your base scope, you’ll only need to update it to include your override path. This ensures the agent knows where to look for your custom workflows. #### Update via the UI: 1. Go to **Platform settings > Notifications > Channels**. 2. Locate your existing **Agent** channel and click it to edit. 3. Scroll to the **Command configuration** section. 4. In **CMDLINE**, you’ll see your current base scope path—for example: ```bash /root/.np/nullplatform/scopes/entrypoint ``` 5. Add your override repository path to the command, using the `--overrides-path` flag: ```bash /root/.np/nullplatform/scopes/entrypoint --overrides-path=/root/.np/your-org/your-override-repo ``` Alternatively, you can update this channel through the [Notification channel API](/docs/api/notification-channel-update). ## 4. Update the agent configuration Your **agent** must also know where your override code lives so it can clone and run your changes. Set the following environment variables: ```bash export NP_API_KEY= export AGENT_TAGS=<"key:value"> export AGENT_REPO="https://git-provider/your-org/your-override-repo.git#main,https://github.com/nullplatform/scopes.git#main" ``` Then, upgrade the running agent using Helm: ```bash helm upgrade nullplatform-agent nullplatform/nullplatform-agent \ --set configuration.values.NP_API_KEY=$NP_API_KEY \ --set configuration.values.TAGS="$AGENT_TAGS" \ --set configuration.values.AGENT_REPO=$AGENT_REPO ``` You should see a message like this once the agent reconnects: ```bash {"level":"info","time":"2025-08-05T16:40:56.721Z","message":"Commands Executor connected"} ``` > For more info on the nullplatform agent, check our [Agent docs](/docs/agent/overview). ## You’re ready to apply overrides Once your agent is running and the channel is configured, your custom logic will be picked up and applied during scope execution. From here, you can: - [Tweak configuration values](/docs/agent-backed-scopes/overrides/configuration.md) - [Modify workflows or logic](/docs/agent-backed-scopes/overrides/behavior.md) --- ## Craft a scope from scratch URL: https://docs.nullplatform.com/docs/agent-backed-scopes/overview When the [sample scope types](/docs/agent-backed-scopes/sample-scopes) don’t fit your workload, you can build your own. This section walks you through defining a scope specification, implementing lifecycle actions, and registering the scope so developers in your organization can use it. ## How it works Agent-backed scopes run using the [nullplatform agent](/docs/agent/overview), which lives inside your infrastructure (for example, a Kubernetes cluster) and listens for actions. 1. You define your scope using a **specification** and a set of lifecycle **actions**. 2. The scope is registered with your organization. 3. The agent listens for matching actions and sends them to your **runner**. 4. The runner provisions infrastructure or executes commands as needed. ```mermaid graph LR subgraph nullplatform nullAPI[nullplatform API] end subgraph client_infrastructure subgraph cluster agent[nullplatform agent] end end agent -- communicates with --> nullAPI ``` ## Key concepts ### Scope specification A scope specification defines the structure of your scope: its name, category, visibility, accepted parameters, and input schema. Think of it like a class definition. The scopes you create from it are the instances. ```json { "name": "My scope name", "slug": "my-scope", "type": "scope", "visible_to": [ "organization=1:account=2" ], "attributes": { "schema": { "type": "object", "required": ["warmup"], "properties": { "asset_type": { "type": "string", "export": false, "default": "docker-image", "readOnly": true }, "warmup": { "type": "boolean", "export": false, "readOnly": false } } }, "values": {} }, "selectors": { "category": "Deployment Services", "imported": false, "provider": "AWS", "sub_category": "Agent backed scope" } } ``` ### Lifecycle actions Actions define what you can do with a scope. Each action is defined through an **action specification**, which outlines its parameters and expected behavior. Scopes support standard actions (`create`, `update`, `delete`) and custom actions like `rollback` or `switch_traffic`. ```json { "name": "Create a scope", "type": "create", "parameters": { "schema": { "type": "object", "required": ["scope_id"], "properties": { "scope_id": { "type": "string" } } }, "values": {} }, "results": { "schema": { "type": "object", "properties": {} }, "values": {} } } ``` :::warning Actions and action specs are different things A scope specification is like a class, and a scope is like an instance. Similarly, an action specification defines the shape of an action, and a concrete action is an instance executed through a runner. ::: ### Visibility You can limit which users or teams can use your scopes by setting visibility based on [NRN](/docs/NRN)s. This lets you control scope availability across different parts of your organization. ## Summary | Entity | Description | | -------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | Scope specification | Defines the name, category, visibility, and parameters for the scope it defines. | | Action specification | Defines the type (create, update, delete, custom) of actions along with its parameters. | | Action | An action instance executed through a runner to manage the scope’s lifecycle. | ## Next steps - [Scope specification](/docs/agent-backed-scopes/craft-scopes/scope-spec): define your scope’s structure, parameters, and visibility - [Action specifications](/docs/agent-backed-scopes/craft-scopes/scope-action-spec): set up lifecycle and custom actions - [Register your scope](/docs/agent-backed-scopes/craft-scopes/register-scope): make the scope available in your organization - [Override workflows](/docs/agent-backed-scopes/overrides): customize existing scope behavior --- ## Sample scopes URL: https://docs.nullplatform.com/docs/agent-backed-scopes/sample-scopes Sample scopes are working examples of scope types that nullplatform maintains in public repositories. Each one ships its scope specification, actions, workflows, and the code your agent runs to deploy to it, so you can adopt it as is or use it as the starting point for your own. They are examples, not the limit of what you can run: any scope type you craft works the same way. ## Samples Each row is one scope type, as defined in the latest release of its repository: - **Scope**: the name developers see when they create a scope. - **Description**: what the scope deploys, as the scope type describes itself. - **Repository**: links to the folder with the scope's code. Some repositories hold several scope types. {/* sample-scopes:start */} | Scope | Description | Repository | | --- | --- | --- | | Containers | Docker containers on pods | [scopes/k8s](https://github.com/nullplatform/scopes/tree/v1.18.0/k8s) | | Scheduled Task | Allows you to deploy periodic jobs in Kubernetes | [scopes/scheduled_task](https://github.com/nullplatform/scopes/tree/v1.18.0/scheduled_task) | | Static files | Allows you to deploy static files applications | [scopes-static-files/static-files](https://github.com/nullplatform/scopes-static-files/tree/v4.1.2/static-files) | | AWS Lambda | Serverless functions on AWS Lambda | [scopes-lambda/lambda](https://github.com/nullplatform/scopes-lambda/tree/v0.11.2/lambda) | | Containers On-Premise | Docker containers on pods, on self-managed/on-premise clusters | [scopes/onprem](https://github.com/nullplatform/scopes/tree/v1.18.0/onprem) | | Volt MX Foundry app | Publishes a Volt MX Foundry app to a Foundry environment | [scopes-voltmx-foundry-app/voltmx-foundry-app](https://github.com/nullplatform/scopes-voltmx-foundry-app/tree/v0.1.4/voltmx-foundry-app) | | Mobile store | Publishes mobile app binaries (Android, iOS) to a distribution channel: tester groups, app stores or an enterprise store | [scopes-mobile-store/mobile-store](https://github.com/nullplatform/scopes-mobile-store/tree/v0.5.2/mobile-store) | {/* sample-scopes:end */} Each repository has its own releases and changelog. A scope that's no longer maintained stays listed, with **Deprecated** at the start of its name. ## How adopting a sample scope works The repository's README has the exact steps and values for each scope. The flow is the same for all of them: 1. **Grant the agent its cloud permissions.** If the scope has a requirements module in `/specs/requirements/`, apply it in your cloud account. It sets up the permissions your agent needs to deploy to the scope. 2. **Register the scope type in nullplatform.** Use the `scope_definition` and `scope_definition_agent_association` modules from [tofu-modules](https://github.com/nullplatform/tofu-modules). The first one registers the scope type, its actions and its specification from the repository, and the second one routes the scope's actions to your agents. Some repositories include a ready-to-apply module for this in `/specs/install/`. 3. **Complete the scope's own prerequisites.** Some scopes need more before their first deployment, such as a scope configuration, DNS zones, or a cluster the agent can reach. Once it's registered, developers pick the scope type when they create a scope for their applications, like any other scope type. :::note You need a running agent with a notification channel for your scopes before you adopt one. If you don't have it yet, set it up first. ::: ## Next steps - [Craft a scope from scratch](/docs/agent-backed-scopes/overview): build your own scope type or adapt a sample - [Scope types](/docs/agent-backed-scopes/scope-types): choose the right scope type for your workload - [Overrides](/docs/agent-backed-scopes/overrides): change how a scope behaves without copying it - [Sample services](/docs/services/sample-services): the same kind of catalog, for services --- ## Scheduled tasks scope URL: https://docs.nullplatform.com/docs/agent-backed-scopes/scheduled-tasks The Scheduled tasks scope runs periodic jobs on Kubernetes using CronJobs. Use it for any workload that runs on a schedule, does its work, and exits without exposing a network endpoint. ## When to use it Use the Scheduled tasks scope when your workload runs on a recurring schedule, doesn't serve HTTP traffic, and performs a finite task before exiting, such as ETL pipelines, report generation, data cleanup, or batch event processing. ## What it includes The Scheduled tasks scope provides built-in support for: - **Cron scheduling and concurrency control**: define when jobs run using cron expressions, and control whether concurrent runs are allowed, replaced, or queued - **Retries and job history**: set retry attempts for failed jobs and configure how many runs to keep - **Resource limits and logging**: define CPU and memory per job, with logs streamed through the same pipeline as other scopes ## How it works Scheduled tasks are built as an override of the [Containers](/docs/agent-backed-scopes/containers) scope. Instead of creating a long-running Deployment with networking, the scope: 1. Replaces the Kubernetes Deployment with a **CronJob**. 2. Skips DNS, Service, and Ingress provisioning (no network endpoint needed). 3. Manages secrets, resources, and logs just like a Containers scope. ```mermaid flowchart LR A[Cron triggers] --> B[Job created] B --> C[Pod runs task] C --> D[Task completes] D --> E[Pod exits] ``` ## Get started To set up a Scheduled tasks scope, you need a Kubernetes cluster, [Helm](https://helm.sh/docs/intro/install/), [Gomplate](https://docs.gomplate.ca/), the [nullplatform CLI](/docs/cli/), and an [API key](/docs/authorization/api-keys) with agent roles. The setup process follows these steps: 1. Install the nullplatform agent in your cluster using Helm. 2. Clone the [scopes repository](https://github.com/nullplatform/scopes) and set `SERVICE_PATH=scheduled_task`. 3. Configure your environment variables (API key, NRN, environment). 4. Run the `./configure` script to register the scope schema, actions, and notification channel. 5. Create your first Scheduled tasks scope from the UI. The [Scheduled tasks tutorial](/docs/tutorials/scheduled-task) provides the full walkthrough with commands and checkpoints. ## How the overrides work The Scheduled tasks scope customizes the base Containers scope through configuration and behavior overrides. This section explains what each override does, so you can understand the implementation or further customize it for your needs. ### Repo structure Place workflow overrides under `scope/workflows/` or `deployment/workflows/` depending on the lifecycle action you're customizing. ``` your-override-repo/ ├── scope/ # Scope lifecycle actions │ └── workflows/ │ ├── create.yaml │ ├── update.yaml │ └── delete.yaml ├── deployment/ │ └── templates/ │ ├── deployment.yaml.tpl │ └── workflows/ │ ├── initial.yaml │ ├── blue_green.yaml ├── values.yaml # Your configuration overrides └── .. ``` ### Skip DNS configuration Scheduled jobs don't need a service or DNS record. The `create` and `delete` workflows skip the networking step: ```yaml # path: scope/workflows/create.yaml include: - "$SERVICE_PATH/values.yaml" steps: - name: networking action: skip ``` ```yaml # path: scope/workflows/delete.yaml include: - "$SERVICE_PATH/values.yaml" steps: - name: networking action: skip ``` ### Use a CronJob instead of a Deployment In Kubernetes, scheduled jobs use `CronJob` objects. The override replaces the base Deployment template with a CronJob template in `deployment/templates/deployment.yaml.tpl`: ```yaml apiVersion: batch/v1 kind: CronJob metadata: name: job-{{ .scope.id }}-{{ .deployment.id }} namespace: {{ .k8s_namespace }} labels: name: d-{{ .scope.id }}-{{ .deployment.id }} app.kubernetes.io/part-of: {{ .namespace.slug }}-{{ .application.slug }} nullplatform: "true" spec: schedule: "{{ .scope.capabilities.cron }}" concurrencyPolicy: {{ .scope.capabilities.concurrency_policy }} successfulJobsHistoryLimit: {{ .scope.capabilities.history_limit }} failedJobsHistoryLimit: {{ .scope.capabilities.history_limit }} jobTemplate: metadata: labels: name: d-{{ .scope.id }}-{{ .deployment.id }} app.kubernetes.io/part-of: {{ .namespace.slug }}-{{ .application.slug }} nullplatform: "true" spec: backoffLimit: {{ .scope.capabilities.retries }} template: metadata: labels: name: d-{{ .scope.id }}-{{ .deployment.id }} app.kubernetes.io/part-of: {{ .namespace.slug }}-{{ .application.slug }} nullplatform: "true" annotations: nullplatform.logs.cloudwatch: 'true' nullplatform.logs.cloudwatch.log_group_name: {{ .namespace.slug }}.{{ .application.slug }} nullplatform.logs.cloudwatch.log_stream_log_retention_days: '7' nullplatform.logs.cloudwatch.log_stream_name_pattern: >- type=${type};application={{ .application.id }};scope={{ .scope.id }};deploy={{ .deployment.id }};instance=${instance};container=${container} nullplatform.logs.cloudwatch.region: us-east-1 spec: restartPolicy: OnFailure securityContext: runAsUser: 0 containers: - name: application image: {{ .asset.url }} envFrom: - secretRef: name: s-{{ .scope.id }}-d-{{ .deployment.id }} resources: limits: cpu: {{ .scope.capabilities.cpu_millicores }}m memory: {{ .scope.capabilities.ram_memory }}Mi requests: cpu: {{ .scope.capabilities.cpu_millicores }}m memory: {{ .scope.capabilities.ram_memory }}Mi ``` Then, point your `values.yaml` to this new template: ```yaml configuration: DEPLOYMENT_TEMPLATE: "$OVERRIDES_PATH/deployment/templates/deployment.yaml.tpl" ``` ### Modify the deployment workflow Because the scope no longer uses `Ingress` or `Service`, the deployment workflow skips the `route traffic` step and replaces the deployment logic with one that applies the CronJob template. In `deployment/workflows/initial.yaml`: ```yaml include: - "$SERVICE_PATH/values.yaml" steps: - name: route traffic action: skip - name: create deployment type: script action: replace file: "$OVERRIDES_PATH/deployment/build_deployment" output: - name: DEPLOYMENT_PATH type: file file: "$OUTPUT_DIR/deployment-$SCOPE_ID-$DEPLOYMENT_ID.yaml" - name: SECRET_PATH type: file file: "$OUTPUT_DIR/secret-$SCOPE_ID-$DEPLOYMENT_ID.yaml" - name: apply type: script file: "$SERVICE_PATH/apply_templates" configuration: ACTION: apply DRY_RUN: false post: name: wait deployment active action: skip ``` ### Refresh the agent sources After changing your override repo, [refresh the agent sources](/docs/agent/agent-refresh) so it pulls the latest code. ### Result With these overrides, your scheduled task scope will: - Skip DNS creation and deletion - Use a Kubernetes `CronJob` instead of a `Deployment` - Remove unnecessary networking steps - Still manage secrets, resources, and logs You can find the complete implementation in the [scopes repository](https://github.com/nullplatform/scopes/tree/main/scheduled_task). ## Next steps - [Override workflows](/docs/agent-backed-scopes/overrides): customize scope behavior for your organization - [Containers](/docs/agent-backed-scopes/containers): the default scope type for long-running workloads - [Scope types](/docs/agent-backed-scopes/scope-types): choose a scope type for your workload --- ## Scope configurations URL: https://docs.nullplatform.com/docs/agent-backed-scopes/scope-configurations Scope configurations let you **define settings like Kubernetes namespace**, **domain**, **deployment strategy**, and **security options** directly from the nullplatform UI, using the same provider you already use for cloud credentials and integrations. Apply them at any level (organization, account, namespace, or individual scope). Set a base domain for the whole organization and override just the namespace per account, without duplicating configuration or managing multiple agents. ## How configuration is resolved When the agent processes an action (like creating or deploying a scope), it resolves each configuration value using this priority order: ```mermaid flowchart TD A[Provider configuration] -->|found?| B{Yes} B --> C[Use provider value] A -->|not found| D[Environment variable] D -->|found?| E{Yes} E --> F[Use env var value] D -->|not found| G[Default value] G --> H[Use default] style A fill:#2d6a4f,stroke:#40916c,color:#fff style D fill:#1b4965,stroke:#5fa8d3,color:#fff style G fill:#495057,stroke:#adb5bd,color:#fff ``` 1. **Provider configuration** (highest priority) — values set through **Platform settings > Scopes > Configurations** in the UI 2. **Environment variables** — values set on the agent server, in the notification channel, or in `values.yaml` 3. **Default values** — built-in fallback values defined in the scope Every variable always resolves to a value. If you don't configure it explicitly, the scope uses its default. :::tip This feature is fully optional and backward-compatible. Existing setups that rely on environment variables or `values.yaml` continue to work exactly as before. ::: ## Prerequisites - **Role**: [Ops](/docs/authorization/roles) - A registered agent-backed scope (for example, the [Containers scope](/docs/agent-backed-scopes/containers)) ## Create a scope configuration 1. Go to **Platform settings > Scopes > Configurations**, and click **+ New configuration**. 2. Select **Static files** as the scope provider. 3. Under **Where will this configuration apply?**, choose the target resource. This can be the entire organization, a specific account, a namespace, or an individual scope. 4. Optionally select an **environment** and **dimension values** (like country) to narrow when this configuration applies. 5. Fill in the settings you want to configure across the available tabs. 6. Click **Create configuration**. The configuration appears in the **Scope configurations** list view. It takes effect immediately for any matching scope actions processed by the agent. ## Inheritance across levels To apply different settings at different levels, create multiple configurations. Configurations follow the nullplatform resource hierarchy. A configuration set at a higher level applies to everything below it, unless a more specific one exists: ```mermaid flowchart TD O["Organizationdomain: example.com"] --> A1["Account Anamespace: team-a"] O --> A2["Account Binherits organization config"] A1 --> N1["Namespace 1inherits Account A config"] A1 --> N2["Namespace 2namespace: team-a-staging"] style O fill:#2d6a4f,stroke:#40916c,color:#fff style A1 fill:#1b4965,stroke:#5fa8d3,color:#fff style A2 fill:#495057,stroke:#adb5bd,color:#fff style N1 fill:#495057,stroke:#adb5bd,color:#fff style N2 fill:#1b4965,stroke:#5fa8d3,color:#fff ``` In this example, Account B and Namespace 1 inherit the domain from the organization level. Account A overrides the namespace, and Namespace 2 overrides it again with its own value. For more details on how inheritance works across the resource tree, see the [provider inheritance model](/docs/providers/overview#inheritance-and-overrides). ## Configuration reference The available variables are organized into categories that match the tabs in the UI: Cluster, Networking, Deployment, Security, and Advanced. Each scope type defines its own set of variables. For the full list of variables, default values, and descriptions, see the [Kubernetes scope README](https://github.com/nullplatform/scopes/blob/main/k8s/README.md). ## Next steps - [Configuration-based overrides](/docs/agent-backed-scopes/overrides/configuration): override scope defaults using `values.yaml` in an override repo - [Providers overview](/docs/providers/overview): learn how provider inheritance and overrides work across the resource hierarchy - [Containers scope](/docs/agent-backed-scopes/containers): set up the default Kubernetes scope type --- ## Scope types URL: https://docs.nullplatform.com/docs/agent-backed-scopes/scope-types A scope type defines what kind of infrastructure backs your deployment environment: how it's provisioned, how your application runs, and how traffic reaches it. Nullplatform maintains sample scope types that cover the most common deployment patterns. They are examples, not the limit of what you can run: [Sample scopes](/docs/agent-backed-scopes/sample-scopes) lists every one of them with its repository, and you can adapt one or craft your own. ## Choosing a scope type | Workload | Start from | |----------|-----------| | Web apps, APIs, microservices | [Containers](/docs/agent-backed-scopes/containers) | | Scheduled tasks, batch jobs | [Scheduled tasks](/docs/agent-backed-scopes/scheduled-tasks) | | SPAs, static websites, front-end bundles | [Static files](/docs/agent-backed-scopes/static-files) | | Serverless functions | AWS Lambda, in [Sample scopes](/docs/agent-backed-scopes/sample-scopes) | | Containers on self-managed or on-premise clusters | Containers On-Premise, in [Sample scopes](/docs/agent-backed-scopes/sample-scopes) | | Mobile apps (Android, iOS) | Mobile store, in [Sample scopes](/docs/agent-backed-scopes/sample-scopes) | | PL/SQL or other custom workloads | [Craft a scope from scratch](/docs/agent-backed-scopes/overview) | ## Legacy scope types Nullplatform also supports legacy scope types for existing workloads. Unlike agent-backed scopes, nullplatform deploys them directly in your cloud account, using the cloud credentials you give it, instead of through workflows your agent runs: - **Kubernetes**: container-based deployments on EKS - **Server instances (EC2)**: VM-based deployments with auto-scaling groups - **Serverless (AWS Lambda)**: serverless function deployments These remain available for backward compatibility. For new serverless workloads, use the agent-backed **AWS Lambda** scope in [Sample scopes](/docs/agent-backed-scopes/sample-scopes). See [Legacy scopes](/docs/scopes-legacy/scopes) for existing workloads. ## Next steps - [Sample scopes](/docs/agent-backed-scopes/sample-scopes): every sample scope type, with its repository - [Containers](/docs/agent-backed-scopes/containers): deep dive into the default scope type - [Scheduled tasks](/docs/agent-backed-scopes/scheduled-tasks): configure periodic jobs on Kubernetes - [Static files](/docs/agent-backed-scopes/static-files): deploy SPAs and static websites with CDN distribution - [Craft a scope from scratch](/docs/agent-backed-scopes/overview): build your own scope type for workloads that don't fit the samples - [Legacy scopes](/docs/scopes-legacy/scopes): reference for existing Kubernetes, EC2, and Lambda workloads --- ## Extend provider support URL: https://docs.nullplatform.com/docs/agent-backed-scopes/static-files/extend Scope configurations let you define cloud-specific settings for the Static files scope directly from the nullplatform UI, without setting environment variables on the agent. You can apply a configuration at any level — organization, account, namespace, or individual scope — and override it at a more specific level when needed. > 💡 **Tip:** Scope configurations are the recommended way to set cloud provider settings for the Static files scope. Existing setups that rely on environment variables continue to work. ## How to create a configuration Go to **Platform settings > Scopes > Configurations**, click **+ New configuration**, and select **Static Files** as the provider. Choose where the configuration applies, select your cloud provider, and fill in the **Provider**, **Distribution**, and **Network** tabs. ## Configuration reference The available fields are organized across four tabs. The fields in the **Provider**, **Distribution**, and **Network** tabs change depending on the cloud provider selected. ### Cloud Provider tab | Field | Description | |---|---| | Cloud Provider | The cloud provider for this configuration: Amazon Web Services or Microsoft Azure | ### Provider tab Configures the agent's state backend and cloud credentials. **AWS** | Field | Description | |---|---| | AWS Region | AWS region where resources will be deployed | | S3 State Bucket | S3 bucket for storing OpenTofu state, also used for S3-native state locking | > 🔒 The agent needs an IAM role attached to its Kubernetes service account (IRSA) with permissions for S3, Route 53, CloudFront, ACM, and STS. Configure this in your agent Helm installation via `serviceAccount` annotations. See [AWS setup](/docs/agent-backed-scopes/static-files/setup-aws) for the full IAM policy. **Azure** | Field | Description | |---|---| | Azure Subscription ID | Azure subscription where resources will be deployed | | Azure Resource Group | Resource group for scope resources | | State Storage Account | Azure storage account for storing OpenTofu state | | State Container | Blob container name for state files | ### Distribution tab Selects the CDN distribution provider. The scope uses this to provision and manage the CDN endpoint for your static files. | Cloud | Distribution | |---|---| | AWS | Amazon CloudFront | | Azure | Azure Blob CDN | ### Network tab Configures the DNS zone used to create the scope's custom domain. **AWS** | Field | Description | |---|---| | AWS DNS Provider | DNS provider for managing records (Amazon Route 53) | | Route 53 Hosted Zone ID | Public hosted zone ID for DNS records (e.g., `Z1234567890ABC`) | **Azure** | Field | Description | |---|---| | DNS Zone Name | Azure DNS zone name (e.g., `example.com`) | | DNS Zone Resource Group | Resource group that contains the DNS zone | ## Inheritance A configuration set at a higher level applies to all resources below it, unless a more specific configuration exists at a lower level. This lets you set a base domain or region at the organization level and override only what differs per namespace or scope. For the full resolution order and inheritance model, see [Scope configurations](/docs/agent-backed-scopes/scope-configurations). ## Next steps - [AWS setup](/docs/agent-backed-scopes/static-files/setup-aws): IAM permissions for the agent on AWS - [Azure setup](/docs/agent-backed-scopes/static-files/setup-azure): RBAC permissions for the agent on Azure - [Scope configurations](/docs/agent-backed-scopes/scope-configurations): how configuration values are resolved and inherited across the resource hierarchy --- ## Static files scope URL: https://docs.nullplatform.com/docs/agent-backed-scopes/static-files/ The Static files scope lets you deploy single-page apps (SPAs), static websites, and front-end bundles directly from nullplatform. It provisions a CDN distribution, configures DNS and HTTPS, and manages deployments using OpenTofu. ## When to use it Use the Static files scope when your application only ships static file assets (HTML, CSS, JS, images), doesn't need a server-side runtime, and needs to be distributed through public custom domains. ## Supported cloud providers The Static files scope works with the following cloud providers: | Cloud provider | CDN / Distribution | DNS | Storage | |---|---|---|---| | AWS | CloudFront | Route 53 | S3 | | Azure | Azure CDN | Azure DNS | Blob Storage | You can add support for other cloud providers. See [Extend the scope](/docs/agent-backed-scopes/static-files/extend) for details. ## What gets provisioned When you deploy a Static files scope, nullplatform provisions and manages the following infrastructure: ### AWS (CloudFront + Route 53) - **CloudFront distribution** with HTTPS, gzip compression, and SPA-friendly error handling (404/403 → `index.html`) - **S3 bucket policy** for secure CloudFront-only access via Origin Access Control (OAC) - **Route 53 DNS record** (A alias) pointing your custom domain to the distribution - **Automatic cache invalidation** on each deployment ### Azure (Azure CDN + Azure DNS) - **Azure CDN profile and endpoint** serving content from Blob Storage, with SPA routing, HTTPS redirect, and cache rules - **Azure DNS CNAME record** pointing your custom domain to the CDN endpoint - **CDN-managed HTTPS certificate** for custom domains (TLS 1.2) - **Blob Storage** path-based origin for asset isolation ## How it works When you trigger a deployment, the scope runs through the following steps using OpenTofu: ```mermaid flowchart LR A["**Deploy triggered**nullplatform resolves the DNS zoneand computes the subdomain"] B["**Infrastructure applied**OpenTofu provisions the CDN,bucket policy, and DNS record"] C["**Assets served**CDN origin points tothe new release path"] A --> B --> C classDef np fill:#e6faf4,stroke:#00b894,color:#0b1e3f,stroke-width:1.5px; classDef infra fill:#eef3fb,stroke:#274a86,color:#0b1e3f,stroke-width:1.5px; class A np; class B,C infra; linkStyle 0 stroke:#00b894,stroke-width:2px; linkStyle 1 stroke:#274a86,stroke-width:2px; ``` 1. Nullplatform resolves your DNS zone and computes the subdomain (`{app-slug}-{scope-slug}.{domain}`). 2. The scope provisions a CDN distribution pointed at your assets in storage and creates a bucket policy for secure access. 3. OpenTofu applies the infrastructure changes (`tofu init` + `tofu apply`). 4. DNS is updated and traffic starts flowing to the new release. ## Deployment strategies - **Initial deployment**: provisions all infrastructure from scratch (CDN, DNS, bucket policy) and points the distribution to the release's asset path in your storage bucket. - **Blue-green deployment**: reuses the initial deployment setup. Since Static files are immutable and stored at versioned paths in the bucket, each deployment updates the CDN origin path to point to the new release. :::warning Switching traffic is not supported The **switch traffic step is not supported** for Static files. Traffic switches instantly when the CDN origin path is updated. **To roll back, trigger a new deployment** pointing to the previous release. ::: ## Custom domains Nullplatform configures custom domains automatically: - **AWS**: the scope reads the `hosted_public_zone_id` from your cloud-provider configuration, resolves the domain from Route 53, and creates an alias record. If a wildcard ACM certificate exists for the domain, HTTPS is enabled automatically. - **Azure**: the scope reads `public_dns_zone_name` and `public_dns_zone_resource_group_name` from your cloud-provider configuration, verifies the DNS zone exists, and creates a CNAME record pointing to the Azure CDN endpoint. The subdomain follows the pattern `{application-slug}-{scope-slug}`, resulting in URLs like `my-app-production.example.com`. ## Lifecycle actions | Action | Description | |---|---| | `start-initial` | Provisions all infrastructure (CDN, DNS, bucket policy) for the first deployment | | `start-blue-green` | Updates the CDN origin path to point to the new release | | `delete-deployment` | Destroys all provisioned infrastructure | ## Next steps - [Set up the Static files scope](/docs/agent-backed-scopes/static-files/setup): prerequisites and agent permissions for AWS and Azure - [Extend provider support](/docs/agent-backed-scopes/static-files/extend): define cloud provider settings, DNS zones, and state backend via the UI - [Scope types](/docs/agent-backed-scopes/scope-types): choose a scope type for your workload - [Containers](/docs/agent-backed-scopes/containers): the default scope type for long-running workloads --- ## AWS setup URL: https://docs.nullplatform.com/docs/agent-backed-scopes/static-files/setup-aws ## Prerequisites | Requirement | Description | |---|---| | Cloud provider | An AWS cloud-provider with `hosted_public_zone_id` set | | Assets repository | An S3-based assets-repository provider at the correct NRN level | | Agent environment variables | `AWS_REGION`, `TOFU_PROVIDER_BUCKET` | | ACM certificate (optional) | Wildcard certificate (`*.example.com`) in `us-east-1` for HTTPS on custom domains | ## IAM policy The agent's IAM role or user needs the following permissions: ```json { "Version": "2012-10-17", "Statement": [ { "Sid": "TofuStateBackend", "Effect": "Allow", "Action": [ "s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:ListBucket" ], "Resource": [ "arn:aws:s3:::YOUR_STATE_BUCKET", "arn:aws:s3:::YOUR_STATE_BUCKET/*" ] }, { "Sid": "Route53Network", "Effect": "Allow", "Action": [ "route53:GetHostedZone", "route53:ChangeResourceRecordSets", "route53:ListResourceRecordSets" ], "Resource": "arn:aws:route53:::hostedzone/YOUR_HOSTED_ZONE_ID" }, { "Sid": "Route53ListZones", "Effect": "Allow", "Action": [ "route53:ListHostedZones", "route53:ListHostedZonesByName" ], "Resource": "*" }, { "Sid": "Route53ChangePolling", "Effect": "Allow", "Action": [ "route53:GetChange" ], "Resource": "arn:aws:route53:::change/*" }, { "Sid": "CloudFrontDistribution", "Effect": "Allow", "Action": [ "cloudfront:CreateDistribution", "cloudfront:UpdateDistribution", "cloudfront:DeleteDistribution", "cloudfront:GetDistribution", "cloudfront:TagResource", "cloudfront:UntagResource", "cloudfront:ListTagsForResource", "cloudfront:CreateInvalidation", "cloudfront:CreateOriginAccessControl", "cloudfront:GetOriginAccessControl", "cloudfront:UpdateOriginAccessControl", "cloudfront:DeleteOriginAccessControl" ], "Resource": "*" }, { "Sid": "S3BucketAccess", "Effect": "Allow", "Action": [ "s3:GetBucketLocation", "s3:GetBucketPolicy", "s3:PutBucketPolicy", "s3:DeleteBucketPolicy", "s3:ListBucket" ], "Resource": "arn:aws:s3:::YOUR_ASSETS_BUCKET" }, { "Sid": "ACMCertificateLookup", "Effect": "Allow", "Action": [ "acm:ListCertificates", "acm:DescribeCertificate", "acm:GetCertificate" ], "Resource": "*" }, { "Sid": "STSIdentity", "Effect": "Allow", "Action": "sts:GetCallerIdentity", "Resource": "*" } ] } ``` Replace `YOUR_STATE_BUCKET`, `YOUR_REGION`, `YOUR_ACCOUNT_ID`, `YOUR_HOSTED_ZONE_ID`, and `YOUR_ASSETS_BUCKET` with the values from your environment. ## Next steps - [WAF protection](/docs/agent-backed-scopes/static-files/waf): optional, attach an existing AWS WAFv2 WebACL to the CloudFront distribution - [Set up the Static files scope](/docs/agent-backed-scopes/static-files/setup): back to the setup overview - [Static files scope](/docs/agent-backed-scopes/static-files): overview of the scope type and how it works --- ## Azure setup URL: https://docs.nullplatform.com/docs/agent-backed-scopes/static-files/setup-azure ## Prerequisites | Requirement | Description | |---|---| | Cloud provider | An Azure cloud-provider with `public_dns_zone_name` and `public_dns_zone_resource_group_name` set | | Assets repository | A Blob Storage-based assets-repository provider | | Agent environment variables | `AZURE_SUBSCRIPTION_ID`, `AZURE_RESOURCE_GROUP`, `TOFU_PROVIDER_STORAGE_ACCOUNT`, `TOFU_PROVIDER_CONTAINER` | ## RBAC permissions The agent's service principal needs permissions across four areas: state backend, DNS management, CDN management, and storage account access. You can either assign built-in roles or create a custom role with the minimum required permissions. ### Option 1: Built-in roles ```bash # Replace placeholders with your values PRINCIPAL_ID="YOUR_SERVICE_PRINCIPAL_ID" SUBSCRIPTION="/subscriptions/YOUR_SUBSCRIPTION_ID" STATE_RG="$SUBSCRIPTION/resourceGroups/YOUR_STATE_RESOURCE_GROUP" DNS_RG="$SUBSCRIPTION/resourceGroups/YOUR_DNS_RESOURCE_GROUP" CDN_RG="$SUBSCRIPTION/resourceGroups/YOUR_CDN_RESOURCE_GROUP" STORAGE_ACCOUNT="$SUBSCRIPTION/resourceGroups/YOUR_RG/providers/Microsoft.Storage/storageAccounts/YOUR_ASSETS_STORAGE_ACCOUNT" # State backend: read/write state blobs in the storage container az role assignment create --assignee $PRINCIPAL_ID \ --role "Storage Blob Data Contributor" --scope $STATE_RG # Network layer: create and delete DNS records az role assignment create --assignee $PRINCIPAL_ID \ --role "DNS Zone Contributor" --scope $DNS_RG # Distribution layer: manage CDN profiles, endpoints, and custom domains az role assignment create --assignee $PRINCIPAL_ID \ --role "CDN Endpoint Contributor" --scope $CDN_RG az role assignment create --assignee $PRINCIPAL_ID \ --role "CDN Profile Contributor" --scope $CDN_RG # Distribution layer: read storage account properties for CDN origin az role assignment create --assignee $PRINCIPAL_ID \ --role "Reader" --scope $STORAGE_ACCOUNT ``` ### Option 2: Custom role with minimum permissions ```json { "Name": "Nullplatform Static Files Scope", "Description": "Minimum permissions for the nullplatform agent to manage Static files scopes", "Actions": [ "Microsoft.Storage/storageAccounts/read", "Microsoft.Storage/storageAccounts/listkeys/action", "Microsoft.Storage/storageAccounts/blobServices/containers/read", "Microsoft.Storage/storageAccounts/blobServices/containers/write", "Microsoft.Network/dnszones/read", "Microsoft.Network/dnszones/CNAME/read", "Microsoft.Network/dnszones/CNAME/write", "Microsoft.Network/dnszones/CNAME/delete", "Microsoft.Network/dnszones/A/read", "Microsoft.Network/dnszones/A/write", "Microsoft.Network/dnszones/A/delete", "Microsoft.Cdn/profiles/read", "Microsoft.Cdn/profiles/write", "Microsoft.Cdn/profiles/delete", "Microsoft.Cdn/profiles/endpoints/read", "Microsoft.Cdn/profiles/endpoints/write", "Microsoft.Cdn/profiles/endpoints/delete", "Microsoft.Cdn/profiles/endpoints/customdomains/read", "Microsoft.Cdn/profiles/endpoints/customdomains/write", "Microsoft.Cdn/profiles/endpoints/customdomains/delete", "Microsoft.Cdn/profiles/endpoints/customdomains/EnableCustomHttps/action", "Microsoft.Cdn/profiles/endpoints/customdomains/DisableCustomHttps/action" ], "DataActions": [ "Microsoft.Storage/storageAccounts/blobServices/containers/blobs/read", "Microsoft.Storage/storageAccounts/blobServices/containers/blobs/write", "Microsoft.Storage/storageAccounts/blobServices/containers/blobs/delete" ], "AssignableScopes": [ "/subscriptions/YOUR_SUBSCRIPTION_ID" ] } ``` Create and assign the custom role: ```bash az role definition create --role-definition custom-role.json az role assignment create \ --assignee YOUR_SERVICE_PRINCIPAL_ID \ --role "Nullplatform Static Files Scope" \ --scope /subscriptions/YOUR_SUBSCRIPTION_ID ``` Replace `YOUR_SUBSCRIPTION_ID`, `YOUR_SERVICE_PRINCIPAL_ID`, and the resource group names with values from your environment. ## Next steps - [Set up the Static files scope](/docs/agent-backed-scopes/static-files/setup): back to the setup overview - [Static files scope](/docs/agent-backed-scopes/static-files): overview of the scope type and how it works --- ## Set up the Static files scope URL: https://docs.nullplatform.com/docs/agent-backed-scopes/static-files/setup Before creating a Static files scope, configure the following in your nullplatform account: - A **cloud provider** with DNS zone information set - An **assets repository** provider backed by S3 or Blob Storage - **Agent environment variables** for the OpenTofu state backend - **Agent cloud permissions** to manage CDN distributions, DNS records, and storage access Select your cloud provider for the full prerequisites and minimum permissions: - [AWS](/docs/agent-backed-scopes/static-files/setup-aws): CloudFront, Route 53, and S3 - [WAF](/docs/agent-backed-scopes/static-files/waf): optional, attach an existing AWS WAFv2 WebACL to the CloudFront distribution - [Azure](/docs/agent-backed-scopes/static-files/setup-azure): Azure CDN, Azure DNS, and Blob Storage --- ## WAF protection URL: https://docs.nullplatform.com/docs/agent-backed-scopes/static-files/waf The Static files scope can attach an existing AWS WAF WebACL to its CloudFront distribution, adding an extra security layer in front of your assets. The feature is opt-in and adds no IAM overhead unless enabled. ## When to use it Attach a WebACL when you need: - Managed AWS rules (common rule set, IP reputation lists, bot control) - Rate-based rules per IP - Custom request filtering or geo restrictions - WAF logging to S3, CloudWatch, or Firehose If you only need basic HTTPS and edge caching, leave the WAF off. ## Limits The integration is intentionally narrow to keep the configuration simple. The following are out of scope today: | Limit | Detail | |---|---| | WAF version | Only AWS WAFv2 is supported. WAF Classic (`aws_waf_*`) is not. | | Account | Same-account only. The WebACL must live in the same AWS account that the agent authenticates against. | | Scope | The WebACL must have `scope=CLOUDFRONT`. AWS confines those WebACLs to `us-east-1`, so the WebACL must live in `us-east-1`. | | Lifecycle | The scope attaches an existing WebACL by name. It does not create, update, or delete WebACL resources. | If you need any of the above, manage the WebACL outside the scope and reference it by name once it exists. ## Configuration Add a `security` block to the `attributes` of your scope-configurations provider: ```hcl attributes = { cloud_provider = "aws" provider = { ... } network = { ... } distribution = { aws_distribution = "cloudfront" } security = { aws_security = "waf" aws_web_acl_name = "my-web-acl" } } ``` | Field | Values | Description | |---|---|---| | `security.aws_security` | `"none"` (default) or `"waf"` | Choose whether to attach a WebACL. `"none"` skips the WAF integration entirely. | | `security.aws_web_acl_name` | string | Name of an existing WAFv2 WebACL with `scope=CLOUDFRONT` in `us-east-1`. Required when `aws_security="waf"`. | The same fields are also available in the nullplatform UI under the **Security** tab of the scope configuration. ## IAM permissions When `aws_security="waf"`, the agent needs two additional permissions on top of the ones documented in [AWS setup](/docs/agent-backed-scopes/static-files/setup-aws): ```json { "Sid": "WAFv2WebACLLookup", "Effect": "Allow", "Action": [ "wafv2:ListWebACLs", "wafv2:GetWebACL" ], "Resource": "arn:aws:wafv2:us-east-1:YOUR_ACCOUNT_ID:global/webacl/*/*" } ``` `ListWebACLs` is called by the scope's setup script to validate the WebACL name before running OpenTofu. `GetWebACL` is called by the OpenTofu data source that resolves the name to an ARN. When `aws_security="none"`, neither action is invoked, so the policy above is only needed in clusters that actually use the WAF feature. ## Operational behavior Attaching or detaching a WebACL is always an **in-place update** of the CloudFront distribution. The distribution ID stays stable across the change, no DNS or edge propagation reset is required, and there is no downtime. - **Attach**: setting `aws_security="waf"` with a valid name on an existing scope triggers a distribution update that sets `WebACLId` to the WebACL ARN. The scope is redeployed normally. - **Detach**: setting `aws_security="none"` (or clearing the name) on the next deployment sets `WebACLId` back to empty. The distribution keeps the same ID. - **Swap**: changing `aws_web_acl_name` to a different existing WebACL re-points `WebACLId` to the new ARN, also in-place. :::note The configuration change in the provider does not by itself update CloudFront. You need to trigger a deployment of the scope (initial, blue-green, or any redeploy) for the agent to apply the new value. ::: ## Validation and errors Before running OpenTofu, the scope's setup script validates that the WebACL exists by calling `wafv2:ListWebACLs` with `scope=CLOUDFRONT` in `us-east-1`. If the name is misspelled, lives in a different account, or has the wrong scope, the deployment fails fast with a message that points to the most common causes: - Name misspelled - WebACL exists but with `scope=REGIONAL` instead of `CLOUDFRONT` - WebACL lives in a different AWS account - Agent role lacks `wafv2:ListWebACLs` Fix the underlying issue and redeploy. No partial state is left behind because the failure happens before the OpenTofu apply. --- ## Force delete for misconfigured scopes URL: https://docs.nullplatform.com/docs/agent-backed-scopes/troubleshooting/force-delete-scopes Sometimes, scopes can't be deleted normally due to configuration errors that are considered "unfixable". This can happen in cases where the system can't guarantee successful deletion, such as when: - authentication credentials have expired. - a provider is misconfigured, such as with GCP. - a request to create a scope fails. In such cases, you can use **the `force` query parameter**. This option lets you retry deleting a scope after a previous attempt has failed. When enabled, the system will attempt to remove the scope by bypassing the checks that caused the failure. ### Considerations - The option becomes available after you've attempted to delete the scope using a standard delete request and that attempt has failed. - The **approvals** process is not bypassed when performing a force delete. All approval workflows and policies will proceed as usual and **won't be skipped**. - **Residual infrastructure**: When using force delete, there's a possibility that some infrastructure components may remain if they couldn’t be successfully deleted in previous attempts. ### How it works - If a standard delete operation fails, the `force` option becomes available. - When you include `force=true` in your delete request, the system attempts to delete the scope again, ignoring conditions that blocked the initial attempt. ```mermaid flowchart LR A[Start:delete scope request] --> C{Deletesuccessful?} C -- Yes --> D[Scope deleted] C -- No --> E[Retry deletewith force=true] E --> H{Deletesuccessful?} H -- Yes --> D H -- No --> I[Deletion failed:contact support] ``` ## Force deleting scopes using the API :::info You need an [**Ops role**](/docs/authorization/roles) to use force delete on a scope. ::: ### 1. Send the request Send a [DELETE request](/docs/api/scope-delete) using our CLI or API to remove a scope. ```bash np scope delete \ --id :id \ --force ``` ```bash curl -L -X DELETE 'https://api.nullplatform.com/scope/:id?force=true' \ -H 'Authorization: Bearer ' ``` Where: - `id`: the unique identifier of the scope to delete. - `force=true`: enables force deletion of the scope. If the request is successful, you'll receive a **200 OK** response. ### 2. Verify the scope is deleted Send a [GET request](/docs/api/scope-read) to check the status of the scope. ```bash np scope read \ --id :id ``` ```bash curl -L 'https://api.nullplatform.com/scope/:id' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ' ``` where: - `id`: the unique identifier of the scope to verify. You'll receive a response with `status: "deleted"`, confirming that the misconfigured scope has been successfully removed. --- ## Troubleshooting ingress and ALB reconciliation (AWS) URL: https://docs.nullplatform.com/docs/agent-backed-scopes/troubleshooting/networking-validation This guide explains how the nullplatform agent **detects and prevents Ingress and ALB reconciliation issues** in **agent-backed Kubernetes scopes on AWS**, what problems can occur, and how to troubleshoot them. To do this, the agent runs **networking validations** during deployment. These validations catch AWS ALB and Ingress reconciliation failures early, before they result in unreachable applications or a blocked Load Balancer Ingress Controller. When enabled, the agent verifies the Ingress and ALB reconciliation process and detects errors that may occur during reconciliation, including (but not limited to) the following: - Your Ingress hostname can be reconciled to a valid ALB rule. - Required SSL/TLS certificates exist in ACM. - ALB limits and traffic configuration issues that could prevent successful reconciliation. If a mismatch or invalid state is found, the deployment fails early with a clear error message and troubleshooting information in the logs. ## The problem: failed Ingress and ALB reconciliation When deploying an application, Kubernetes relies on the **AWS ALB Ingress Controller** to reconcile `Ingress` resources with an Application Load Balancer (ALB). In some situations, this reconciliation fails due to invalid or constrained AWS or Ingress configuration, such as: - An **Ingress hostname without a matching SSL/TLS certificate** in AWS Certificate Manager (ACM). - The ALB reaching its **maximum number of rules** (commonly 100). - Insufficient IP capacity or other AWS-side limitations. When reconciliation fails, it can lead to two critical outcomes: 1. **The deployed application is not reachable** through its expected domain. 2. The ALB Ingress Controller enters an **invalid or blocked state**, preventing *subsequent Ingresses* from being reconciled on the same Load Balancer. These failures are often difficult to diagnose after the fact and may impact unrelated services sharing the ALB. ## What the networking validation checks To reduce the risk of these failures, the agent includes a **networking validation step that runs during deployment**. At a high level, the validation: 1. Observes **Ingress reconciliation events** in the cluster. 2. Identifies the ALB rule associated with the scope’s hostname. 3. Compares the **expected traffic configuration** (for example, blue/green traffic weights) with the **actual ALB configuration** in AWS. 4. Fails the deployment early if a mismatch or invalid state is detected. By stopping the deployment at this stage, the validation prevents broken rollouts from progressing and avoids leaving the ALB in a bad or locked state. ## How validation errors surface If a networking validation fails, the **deployment itself fails** and the error is surfaced directly in the **deployment logs**. A common example is a certificate mismatch: - The Ingress hostname does not match any valid SSL/TLS certificate in ACM. - The ALB listener rule cannot be created or updated. In these cases, the logs clearly report: - The detected root cause. - The specific hostname, listener, or rule that failed validation. - Suggested next steps to correct the configuration. ## Troubleshooting a failed networking validation When a deployment fails due to a networking validation error, use the following steps to diagnose and resolve the issue: 1. **Review the deployment logs first** The validation error message points directly to the failing condition (for example, a missing certificate or rule limit). 2. **Review SSL/TLS certificates and hostname alignment** Verify that the ACM certificates configured in the target AWS account and region correctly match the hostname defined in the `Ingress`. The hostname reflects the desired domain, so the certificate must be issued for that exact configuration. 3. **Check ALB rule limits and capacity** Confirm that the ALB has not reached its maximum number of listener rules and that there are no AWS-side capacity constraints. After correcting the issue, re-run the deployment. No manual cleanup is typically required unless the ALB was left in a partially configured state. ## Requirements Because the validation inspects ALB configuration directly, the agent’s IAM role must include **read-only permissions** for Elastic Load Balancing. Attach the following policy to the agent role: ```json { "Version": "2012-10-17", "Statement": [ { "Sid": "AllowALBValidation", "Effect": "Allow", "Action": [ "elasticloadbalancing:DescribeLoadBalancers", "elasticloadbalancing:DescribeListeners", "elasticloadbalancing:DescribeRules" ], "Resource": "*" } ] } ``` These permissions allow the agent to list Load Balancers and inspect listeners and rules required for validation. ## Enabling networking validations Networking validations are **disabled by default** and must be explicitly enabled. ### Option 1: Enable via Helm (recommended) If you installed the agent using Helm, enable the validation by setting the following value: ```bash helm upgrade nullplatform-agent nullplatform/nullplatform-agent \ --reuse-values \ --set configuration.values.ALB_RECONCILIATION_ENABLED=true \ --set configuration.values.REGION=us-east-1 ``` > 📖 Check our [Agent docs](/docs/agent/overview) for info. ### Option 2: Enable via override values If you manage configuration through a repository, add the following to your `values.yaml`: ```yaml configuration: ALB_RECONCILIATION_ENABLED: true REGION: true ``` > 📖 Check our [Override workflows docs](/docs/agent-backed-scopes/overrides/) for info. Once enabled, the validation runs automatically on subsequent deployments. --- ## Recreate a non-functional scope URL: https://docs.nullplatform.com/docs/agent-backed-scopes/troubleshooting/recreate-scopes If an agent-backed scope becomes non-functional due to infrastructure issues, you can trigger a recreation to restore it. This works for any agent-backed scope, regardless of the underlying provider. :::warning Important: - Recreating a scope **causes downtime**. - It may leave behind **residual or unstable infrastructure**. - The recreated scope is **not automatically deployed**, so you need to deploy it manually after the process. ::: ## How it works Recreating a scope attempts to **delete the broken scope** and its associated infrastructure, and then **creates a new scope** using the same configuration. The newly created scope: - Uses the **same scope ID** and **slug**. - Retains the **same configuration** as the original. - Preserves its **custom domain and DNS settings**, if configured. ## Recreate a scope ### 1. Trigger the recreation Use the UI, CLI, or API to start the recreation. 1. Go to **Development > Scopes**. 2. Click **Actions > Recreate** for the scope you want to recreate. Patch the scope's status to `recreating`: ```bash np scope patch --id "$SCOPE_ID" --body '{"status": "recreating"}' ``` Send a [PATCH request](/docs/api/scope-update) to set the scope's status to `recreating`: ```bash curl -L -X PATCH 'https://api.nullplatform.com/scope/:id' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ' \ -d '{ "status": "recreating" }' ``` ### 2. Deploy the recreated scope Once the recreation completes, you need to redeploy the scope to make it functional: 1. Go to **Development > Scopes**. 2. Click **Deploy** for the scope you just recreated. --- ## Troubleshooting actions on running scopes URL: https://docs.nullplatform.com/docs/agent-backed-scopes/troubleshooting/troubleshooting-actions There are situations where you want to perform specific actions on a running scope. For example, if you see unusual behavior in production, you might want to kill a specific instance to see if the problem is related to that instance. One possible approach is to make a new deployment, but this can be time-consuming and might not be the best approach for this situation. These actions can be triggered through the Performance view in the UI. Any change they make is not persisted, which means that if you make a new deployment, those changes will be lost. ## Use case scenario Let’s suppose you have a traffic spike in production. Through the Performance view, you can quickly change the scaling behavior by adding more instances to the underlying Autoscaling Group. If you do a deployment, the changes you previously made will be overridden by the scope configuration. For example, you can have a scope with scaling enabled, with a min/max range of 2 and 5 instances, respectively. If there’s a traffic spike, you can quickly set the number of instances to 10 to handle the increase. If you do a deployment, the number of instances will be set back to the min/max range of 2 and 5, because that's what the scope configuration says. These troubleshooting actions are meant to quickly take action on a running scope, without the need to do a deployment. ## Current supported actions We have a set of actions you can perform on a running scope. If there are other actions you’d find useful, please [reach out](/docs/support)! Currently, these actions are: #### Instance Kill This action will terminate an instance or pod. You can kill a specific instance or a group of them at once. We need to be careful with this action as it may generate service degradation. #### Autoscaling Fix This is a quick way to scale your infrastructure by adding or removing instances/pods, depending on the desired number you set. Along with this number, you can also specify whether to leave autoscaling enabled or not. #### Autoscaling Stop If a scope has autoscaling enabled, you can stop it. This is useful if the scaling rules you previously set were too aggressive and you want to avoid further scaling. #### Autoscaling Start If a scope has autoscaling enabled and was previously stopped, you can enable it again. ## Setup *Applies only to **instance_kill** in server-based scopes; the rest do not require further setup.* In order to be able to kill an instance, nullplatform needs permission to terminate an instance. To do so, you need to follow two steps: - Create a new policy - Attach policy to the role we assume ### New Policy From the AWS Management Console, in IAM > Policies, click "Create policy" and select the JSON tab. Then add the following: ```json { "Version": "2012-10-17", "Statement": [ { "Sid": "VisualEditor0", "Effect": "Allow", "Action": "ec2:TerminateInstances", "Resource": "*" } ] } ``` After setting this JSON, name the policy `np-troubleshooting-manager`. Any new action you want to add in the future can be added to this policy. ### Attach Policy to Role From the AWS Management Console, in IAM > Roles, click the `null-scope-and-deploy-manager` role. Click "Attach policies" and search for the policy you just created. Attach it to the role. That's it. Now you're ready to use the troubleshooting actions. ## API Examples If you need to create a troubleshooting action without going through the UI, you can use the API. Please refer to our [API reference](/docs/api/scope-troubleshooting-action-create) for more details about usage. ### Creating an Action #### `instance_kill` You can kill a single instance/pod or multiple at once. The `instance_id` is for both server-based scopes and K8s-based scopes. For server-based scopes, you just need the instance ID, which you can find through the Performance View or through the EC2 Dashboard or AWS CLI using `describe-instances` command. For K8s-based scopes, you need the pod name, which you can find through the Performance View or through the EKS Dashboard (or equivalent) or `kubectl get pods` command. ```http POST /scope//action { "name": "instance_kill", "parameters": { "instance_id": "i-abcdef123456" } } ``` ```http POST /scope//action { "name": "instance_kill", "parameters": { "instance_id": [ "i-abcdef123456", "i-defghi654321" ] } } ``` #### `autoscaling_fix` With this action, you set the desired number of instances/pods. Under the hood, if you set 10 instances and you currently have 5, nullplatform will add 5 more instances. If you set 5 instances and you currently have 10, nullplatform will remove 5 instances. Along with the desired amount, we can also specify whether to leave the autoscaling enabled or not (in case the scope previously had scaling enabled through the scope details). ```http POST /scope//action { "name": "autoscaling_fix", "parameters": { "desired_instances": 10, "autoscaling_enabled": false } } ``` #### `autoscaling_stop` If the scope has autoscaling enabled, you can stop it. This is useful if the scaling rules you previously set were too aggressive and you want to avoid further scaling. ```http POST /scope//action { "name": "autoscaling_stop", "parameters": {} } ``` #### `autoscaling_start` If the scope has autoscaling enabled and was previously stopped, you can enable it again. ```http POST /scope//action { "name": "autoscaling_start", "parameters": {} } ``` ### Reading an Action ```http GET /scope//action/ { "id": 5678, "status": "failed", "entity": "scope", "entity_id": 1234, "name": "instance_kill", "parameters": { "instance_id": "i-abcdef123456" }, "messages": [ { "level": "ERROR", "message": "Instance id i-abcdef123456 not found" } ], "created_at": "2024-09-26T19:24:02.714Z", "updated_at": "2024-09-26T19:24:05.801Z", "nrn": "organization=1:account=2:namespace=3:application=4:scope=1234" } ``` --- ## Update the asset name of a scope URL: https://docs.nullplatform.com/docs/agent-backed-scopes/troubleshooting/update-asset-name There are cases where you may need to update a scope’s `asset_name`, such as: - The wrong asset was selected during the initial deployment. - A pipeline update changed the asset name. You can update the `asset_name` using either a [PATCH request to our API](/docs/api/scope-update) or the [CLI](/docs/cli/). The asset name can be up to 60 characters long. Here's a request example for each method: ```bash np scope patch --id 123 --body '{"asset_name": "new-asset-name"}' ``` ```bash curl -L -X PATCH 'https://api.nullplatform.com/scope/123' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -d '{ "asset_name": "new-asset-name" }' ``` --- ## What are scopes? URL: https://docs.nullplatform.com/docs/agent-backed-scopes/what-are-scopes A scope is an isolated deployment environment where your application runs. Think of it as the infrastructure behind a specific environment — like production, staging, or a temporary preview for a feature branch. Each application in nullplatform can have multiple scopes. This lets you deploy the same application to different environments, regions, or configurations without duplicating anything in your codebase. ## How scopes relate to applications Scopes live under applications in the nullplatform resource hierarchy: ``` Organization → Account → Namespace → Application → Scope ``` When you create a scope for an application, nullplatform provisions the infrastructure needed to run it. Once the scope is active, you can deploy builds to it, route traffic, and manage configuration — all from the same platform. For more on how this hierarchy works, see [NRN (Nullplatform Resource Name)](/docs/NRN). ## What you can do with scopes Scopes are flexible by design. Here are some common ways teams use them: - **Separate environments** — create scopes for development, staging, and production - **Distribute across regions** — deploy the same app to different geographic locations - **Isolate workloads** — separate read-heavy from write-heavy traffic - **Rightsize infrastructure** — allocate different resources to different scopes - **Preview changes** — spin up temporary scopes for feature branches You can create as many scopes as you need. Combine these patterns to match your infrastructure strategy. ## Scope lifecycle Every scope goes through a series of states as it's created, used, and eventually removed. ```mermaid stateDiagram-v2 [*] --> pending pending --> creating creating --> active creating --> failed active --> updating updating --> active active --> stopping stopping --> stopped stopped --> active active --> deleting stopped --> deleting deleting --> deleted failed --> recreating recreating --> active recreating --> failed deleted --> [*] ``` | State | What it means | |-------|---------------| | **pending** | The scope has been requested but provisioning hasn't started yet | | **creating** | Cloud resources are being provisioned | | **active** | The scope is ready — you can deploy to it and route traffic | | **updating** | Configuration changes are being applied | | **stopping** | Resources are being scaled down (not destroyed) | | **stopped** | The scope is suspended. Resources are paused but can be restarted | | **deleting** | Cloud resources are being torn down | | **deleted** | The scope has been fully removed | | **failed** | Something went wrong during provisioning or deletion | | **recreating** | A previously failed scope is being re-provisioned | :::note Only Kubernetes-based scopes can be recreated from a `failed` state. Other scope types need to be deleted and created again. ::: ## Public and private scopes When you create a scope, you choose whether it's **public** or **private**: - **Public scopes** are accessible from the internet. Use these for user-facing applications that receive external traffic. - **Private scopes** can only be reached by other applications inside your account. This is the right choice for internal services, APIs consumed only by other apps, and backend workers. :::tip Start with private scopes unless your application needs to receive direct traffic from the internet. ::: ## Next steps - [Scope types](/docs/agent-backed-scopes/scope-types): choose a scope type for your workload. - [Sample scopes](/docs/agent-backed-scopes/sample-scopes): every sample scope type, with its repository. - [Containers](/docs/agent-backed-scopes/containers): the default scope type for Kubernetes workloads. - [Scope configurations](/docs/agent-backed-scopes/scope-configurations): manage scope settings from the UI using providers. - [Scheduled tasks](/docs/agent-backed-scopes/scheduled-tasks): run periodic jobs on Kubernetes using CronJobs. - [Static files](/docs/agent-backed-scopes/static-files): deploy SPAs and static websites with CDN distribution. --- ## Scope service workflow URL: https://docs.nullplatform.com/docs/agent-backed-scopes/workflows-and-execution/base-scope-workflow This guide walks you through how to implement the workflows that power your agent-backed scope. Workflows define how your scope responds to actions like `create`, `start_blue_green`, or `delete`. You’ll define YAML workflow files and run them using the `np service workflow` CLI commands. > 💡 If you're using one of our production-ready scopes from the [Tutorials hub](/docs/tutorials), most of > this setup is already taken care of. Use this page if you're building your own workflows or want to customize > an existing one. ## 1. Create workflow files Each action your scope supports (e.g., `create`, `start_blue_green`) must have a matching workflow file. These workflows are written in `.yaml` and specify the commands or scripts that should run when an action is triggered. ### Where to place workflow files Use the following file paths for each workflow: | Action | Triggered when... | File path | | ------------------- | ------------------------------------------ | -------------------------------------------------------- | | Create scope | The scope is created | `$SERVICE_PATH/scope/workflows/create.yaml` | | Update scope | The scope is updated (currently unused) | `$SERVICE_PATH/scope/workflows/update.yaml` | | Delete scope | The scope is deleted | `$SERVICE_PATH/scope/workflows/delete.yaml` | | Start initial | First deployment or restart | `$SERVICE_PATH/deployment/workflows/initial.yaml` | | Start blue green | New blue-green deployment | `$SERVICE_PATH/deployment/workflows/blue_green.yaml` | | Switch traffic | Traffic is being shifted during blue-green | `$SERVICE_PATH/deployment/workflows/switch_traffic.yaml` | | Finalize deployment | Blue-green deployment is finalized | `$SERVICE_PATH/deployment/workflows/finalize.yaml` | | Rollback deployment | Blue-green deployment is canceled | `$SERVICE_PATH/deployment/workflows/rollback.yaml` | | Delete deployment | Deployment is removed | `$SERVICE_PATH/deployment/workflows/delete.yaml` | | Read logs | Logs are requested | `$SERVICE_PATH/log/workflows/log.yaml` | | Get metrics | Metrics are requested | `$SERVICE_PATH/metric/workflows/metric.yaml` | | List instances | Instance list is requested | `$SERVICE_PATH/instance/workflows/list.yaml` | > Need a refresher on the action types? See the > [action spec reference](/docs/agent-backed-scopes/craft-scopes/scope-action-spec). ## Workflow file format Here's a basic example: ```yaml steps: - name: execute a bash command type: command command: echo "executing a bash command..." - name: execute a script type: script file: path-to-script-to-execute - name: execute with sub-steps type: workflow steps: ... list of commands, scripts, or workflows ``` ### Example with configuration, pre, and post steps ```yaml steps: - name: deployment type: script file: deployment.sh configuration: env: prod template: my-deployment-template.yaml.tpl pre: type: command command: echo "setup deployment..." post: type: command command: echo "validate deployment is ready..." ``` You can further configure the steps in your workflow file with the following keys: - `configuration` - Defines key-value pairs for additional arguments/parameters to be passed to scripts. - `pre` - A command or script executed before the defined step. - `post` - A command or script executed after the defined step. ## 2. Run your scope service workflows Use the [CLI](/docs/cli/) to execute a workflow manually. ```bash np service workflow exec --workflow $path_to_workflow_file ``` This converts your `.yaml` file into a bash script and runs each step in sequence using `&&`. ### Available flags | Flag | Description | Required | | ----------------- | ------------------------------------------------------------------------------- | ----------- | | `--workflow` | Path to the workflow YAML file | Yes | | `--build-context` | Builds a context object with related entities (e.g. scope, deployment, release) | Recommended | | `--values` | Path to a YAML file with parameter values for all steps | Recommended | | `--overrides` | Merges or replaces steps in the workflow file using a separate override file | Optional | | `--dry-run` | Outputs the resulting YAML instead of executing the workflow | Optional | #### Example - Basic ```bash np service workflow exec --workflow ./deployment/workflows/create.yaml ``` - With overrides and shared parameters: ```bash np service workflow exec \ --workflow ./deployment/workflows/create.yaml \ --values ./values.yaml \ --overrides ./deployment/overrides.yaml ``` ## 3. Build the action context (optional) You can use the `--build-context` flag or set the `NP_ACTION_CONTEXT` environment variable to inject all related entities into your workflow. ```bash export NP_ACTION_CONTEXT=your_context_here ``` or ```bash np service workflow --build-context ``` This builds a JSON object containing all entities related to a scope or deployment action. ### Available flags | Flag | Description | Required | | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | | `--notification` | - Provides the notification payload that triggered the service action.- Use this if `NP_ACTION_CONTEXT` is **not** set. | Optional | | `--include-secrets` | - Includes secret values in the generated context. - **Required** The agent must have an API key with `agent`, `developer`, `ops`, and `secrets-reader` permissions. | Optional | ### Example Below are some examples, but you can check our [Event payloads docs](/docs/notifications/event-payloads) for more details.
Click to view example scope context ```json { "account": { "id": 2, "nrn": "organization=1:account=2", "name": "account-name", "slug": "account-name", "organization_id": 1, "status": "active", "metadata": {} }, "namespace": { "id": 3, "nrn": "organization=1:account=2:namespace=3", "name": "namespace-name", "slug": "namespace-name", "account_id": 2, "status": "active", "metadata": {} }, "application": { "id": 4, "nrn": "organization=1:account=2:namespace=3:application=4", "name": "application-name", "slug": "application-name", "status": "active", "auto_deploy_on_creation": false, "is_mono_repo": false, "metadata": {}, "namespace_id": 4, "repository_app_path": null, "repository_url": "https://github.com/github-account/application-name", "tags": {}, "template_id": 1234 }, "scope": { "id": 5, "nrn": "organization=1:account=2:namespace=3:application=4:scope=5", "name": "scope-name", "slug": "development-uruguay", "application_id": 4, "type": "custom", "status": "stopped", "capabilities": { ... }, "dimensions": { "country": "uruguay", "environment": "development" } } } ```
Click to view example deployment context ```json { "deployment": { "id": 6, "release_id": 9, "scope_id": 5, "nrn": "organization=1:account=2:namespace=3:application=4:scope=5:deployment=6", "status": "creating", "strategy": "blue_green", "strategy_data": { "desired_switched_traffic": 10, "switch_traffic_step": true, "switched_traffic": 0 }, }, "release": { "id": 9, "application_id": 4, "build_id": 7, "nrn": "organization=1:account=2:namespace=3:application=4:release=9", "semver": "0.0.1", "status": "active" }, "asset": { "id": 8, "build_id": 7, "name": "my-asset", "type": "docker-image", "url": "...", "platform": "arm", "nrn": "organization=1:account=2:namespace=3:application=4:build=7:asset=8" }, "parameters": { "paging": { "limit": 30, "offset": 0 }, "results": [ { "encoding": "plaintext", "id": 10, "name": "param-1", "nrn": "organization=1:account=2:namespace=3:application=4", "read_only": false, "secret": false, "type": "environment", "values": [ { "created_at": "2025-04-16T17:23:30.757Z", "dimensions": null, "external": null, "id": "11", "nrn": "organization=1:account=2:namespace=3:application=4", "value": "" } ], "variable": "PARAM VALUE", "version_id": 11 } ] } } ```
## What's next? Want to dynamically merge or modify your workflow files? Head over to [Override scope workflows](/docs/agent-backed-scopes/overrides). --- ## Workflows and execution URL: https://docs.nullplatform.com/docs/agent-backed-scopes/workflows-and-execution/ This section covers advanced implementation details for agent-backed scopes. You’ll learn how the nullplatform agent communicates with your runner, how to define workflows for your scope’s actions, and how to debug or override them. ## When should you use this? This content is especially useful if: - You're creating a **custom runner** for your agent-backed scope - You want full control over the **execution behavior** of actions like `create`, `start_blue_green`, or `delete` - You want to **merge, skip, or replace** steps in your workflow dynamically ## Scope repository examples See the [Scopes repository](https://github.com/nullplatform/scopes) for production‑oriented templates you can adopt and extend. The repository follows a layout similar to the one below to implement agent-backed scopes. Other sample scopes live in their own repositories; see [Sample scopes](/docs/agent-backed-scopes/sample-scopes). ### Typical layout ``` scopes/ ├── k8s/ # Kubernetes-specific implementation │ ├── scope/ # Scope lifecycle actions │ │ └── workflows/ │ │ ├── create.yaml │ │ ├── update.yaml │ │ └── delete.yaml │ ├── deployment/ # Deployment strategies (initial, blue_green, ...) │ │ └── workflows/ │ │ ├── initial.yaml │ │ ├── blue_green.yaml │ │ └── ... │ └── ... ├── service # Routes actions based on NP_ACTION_CONTEXT ├── agent/ # Agent deployment scripts ├── configure # Configuration script └── entrypoint # Main execution script ``` ## What's next? Looking for step-by-step examples or deeper customization? - [Tutorials hub](/docs/tutorials) - [Override scope workflows](/docs/agent-backed-scopes/overrides/index.md) --- ## How to override a base scope workflow URL: https://docs.nullplatform.com/docs/agent-backed-scopes/workflows-and-execution/override-scope-service-workflow To override or add configurations to your base workflow file, run the following command: ```bash np service workflow exec --workflow $path_to_workflow_file --overrides $path_to_overrides_file ``` Use the `--overrides` flag to modify the steps defined in the base workflow file: - **merge** - merges configurations with the base definition - **replace** - replaces a configuration of the base workflow file - **skip** - skips a step from the base workflow file - **add** - adds a step in the base workflow file :::tip You can include the `--dry-run` flag, which returns a final `.yaml` file instead of executing the command. This file shows the applied overrides to the base workflow. ::: ## Example: updating a base scope workflow We'll use the following base workflow file as an example: ```bash steps: - name: step 1 type: command command: echo "first step" - name: step 2 type: script file: final-step.sh configuration: env: production ``` The example above will be updated depending on the new configurations defined in the overrides file. Below are examples of override configurations that will either **merge**, **replace**, **skip**, or **add** steps to your base workflow file.
Merging configurations New configurations added in the overrides file: ```bash steps: - name: step 2 configuration: extra: configurations post: type: command command: echo "validate step" ``` Resulting configuration: ```bash steps: - name: step 1 type: command command: echo "first step" - name: step 2 type: script file: final-step.sh configuration: env: production extra: configurations // extra configuration post: // new post script type: command command: echo "validate step" ```
Replacing configurations New configurations added in the overrides file: :::warning You must provide the complete definition of the step you want to replace. ::: ```bash steps: - name: step 2 type: script action: replace file: other-step.sh configuration: extra: configurations post: type: command command: echo "validate step" ``` Resulting configuration: ```bash steps: - name: step 1 type: command command: echo "first step" - name: step 2 type: script file: other-step.sh // replaced step configuration: extra: configurations post: type: command command: echo "validate step" ```
Skipping steps New configurations added in the overrides file: ```bash steps: - name: step 2 action: skip ``` Resulting configuration: ```bash steps: - name: step 1 //only step one is executed type: command command: echo "first step" ```
Adding steps New configurations added in the overrides file: ```bash steps: - name: step 0 // new step type: command command: echo "I will be executed first!" before: step 1 // defines when in the workflow the step will be executed - name: step 3 type: command command: echo "I will be executed last!" after: step 2 ``` Resulting configuration: ```bash steps: - name: step 0 // new step executed first type: command command: echo "I will be executed first!" - name: step 1 type: command command: echo "first step" - name: step 2 type: script file: final-step.sh configuration: env: production - name: step 3 type: command command: echo "I will be executed last!" ```
--- ## Set up a notification channel URL: https://docs.nullplatform.com/docs/agent-backed-scopes/workflows-and-execution/set-up-notifications Set up a notification channel so nullplatform can send scope service actions to your runner. ## Create an agent notification channel To configure this, use our [CLI](/docs/cli/) or send a [POST request](/docs/api/notification-channel-create) with the endpoint details. **Example request** ```bash np notification channel create \ --body '{ "nrn": "organization=1:account=2:namespace=3:application=4", "source": ["service"], "type": "agent", "configuration": { "api_key": "AAAA.1234567890abcdef1234567890abcdefPTs=", "command": { "data": { "cmdline": "path-to-entrypoint-file/entrypoint --service-path=\"$SERVICE_PATH\"", "environment": { "NP_ACTION_CONTEXT": "${NOTIFICATION_CONTEXT}" } }, "type": "exec" }, "selector": {} }, "filters": { "service.specification.slug": "$service-spec-slug" } }' ``` ```bash curl -L -X POST 'https://api.nullplatform.com/notification/channel' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -d '{ "nrn": "organization=1:account=2:namespace=3:application=4", "source": ["service"], "type": "agent", "configuration": { "api_key": "AAAA.1234567890abcdef1234567890abcdefPTs=", "command": { "data": { "cmdline": "/root/.np/services/entrypoint --service-path=/root/.np/services/kubernetes", "environment": { "NP_ACTION_CONTEXT": "${NOTIFICATION_CONTEXT}" } }, "type": "exec" }, "selector": {} }, "filters": { "service.specification.slug": "$service-spec-slug" } }' ``` :::note Request details - `nrn`: The NRN of the resource (including child resources) where the action applies. - `source`: The system emitting the notification. For agent-backed scopes, set this to `service`. - `type`: The type of notification channel. For agent-backed scopes, use `agent`. - `configuration`: Configuration options for the agent. - `api_key`: The API key for the agent. - `command`: Describes the command the agent will execute. - `filters`: MongoDB-style query that filters which service notifications trigger the runner. See the [API docs](/docs/api/notification-channel-create) for more information on this request. ::: Once your channel is set up, nullplatform will start relaying events to your runner when relevant service actions are triggered. --- ## AI usage and quotas URL: https://docs.nullplatform.com/docs/ai/ai-usage :::info 🚀 Early release AI Ops features are in early release and may not be available in your organization yet. If you'd like to try them or want to know more, [reach out to us](https://docs.nullplatform.com/docs/support) and we'll get you set up. ::: The **AI Usage** page in Platform Settings shows how much of the AI assistant quota your organization and individual users have consumed. Use it to check current consumption against daily and monthly limits and to spot when anyone is approaching or has exceeded their quota. To open it, go to **Platform Settings > AI Usage**. ## What gets tracked Three metrics track current and historical consumption across daily and monthly time windows: | Metric | What it counts | |---|---| | **Sessions** | Total AI assistant sessions started in the window | | **Tokens** | Total tokens processed (input + output) across all AI sessions in the window | | **Concurrent** | Active AI sessions running at the same time, right now | Each metric shows current usage versus the configured limit (for example, `586K / 10M`) and a percentage progress bar that changes color as consumption grows. ## Organization and user views Use the **Organization** and **User** tabs to switch between the shared org quota and your individual quota. If either is exceeded, the AI assistant is blocked until the window resets. :::note Limits are configured by your organization admin. If you need your quotas adjusted, contact your admin. ::: ### Status indicators The banner and progress bars change color as consumption grows: green when within limits, yellow when approaching (~80%), and orange/red when a limit is reached. When **Limit reached** is shown, the AI assistant is blocked until the window resets or an admin raises the limit. ### Usage trends Below the current limits, the **Usage Trends** chart shows historical consumption over 7, 30, or 90 days. Use the toggle buttons at the top right of the chart to switch between windows. The chart plots tokens and sessions over time, which helps identify peak usage periods and plan capacity accordingly. ## Rate limits in the AI chat The AI assistant chat panel also surfaces a live summary of remaining quota. At the bottom of the chat, a status indicator shows the metric closest to its limit and the percentage remaining. Expand the indicator to see Daily and Monthly limits side by side, with color-coded progress bars for each metric. This lets you check how much quota remains without leaving your current view. --- ## AI assistant URL: https://docs.nullplatform.com/docs/ai-assistant/overview :::info 🚀 Early release AI Ops features are in early release and may not be available in your organization yet. If you'd like to try them or want to know more, [reach out to us](https://docs.nullplatform.com/docs/support) and we'll get you set up. ::: The **nullplatform AI assistant** is a chat panel embedded directly in the console. You can ask questions about your applications, deployments, and scopes, generate `np` CLI commands on demand, and investigate failures, all without leaving your current view. ## What you can do > 💡 The assistant responds in the same language you write in, including English, Spanish, and more. - **Explore resources**: applications, deployments, scopes, and builds - **Investigate failures**: current state, recent changes, and audit logs - **Generate CLI commands**: ready-to-run `np` commands and shell scripts :::warning The assistant is read-only It **can't create, update, or delete** resources directly. For write operations, it generates the command or script for you to run. ::: Click the **AI assistant** icon in nullplatform to open the chat panel and ask a question. ## You don't need to provide IDs or context The **assistant knows what entity you're currently viewing.** If you're on a deployment page, a question like "Why did this fail?" is enough. No need to specify the deployment ID or application name. > *"Why did this fail?"* > *"Show me recent deployments"* > *"¿Qué pasó con este scope?"* ## See it in action ### Investigate a failed deployment **Prompt**: *"Why did my deployment fail?"*
Assistant response Let me investigate deployment #1847362 for your payment-service application. **🔍 Current state** | Field | Value | |---|---| | Application | payment-service | | Environment | production | | Build | #284 (commit a7f3d91) | | Status | Failed | | Duration | 5m 27s | **🚨 Root cause analysis** The deployment failed during the health check phase: 1. **Build & push**: ✅ Successful — image built and pushed to registry 2. **Container deployment**: ✅ Successful — container started on `prod-cluster-us-east-1` 3. **Health check**: ❌ Failed — `http://payment-service.internal:8080/health` timed out after 10 attempts **📋 What I found** The container started but couldn't respond to health checks because of a database connection failure: ``` FATAL: password authentication failed for user "payment_svc" Connection to postgres-prod.internal:5432 refused ``` The `DATABASE_PASSWORD` parameter is missing from the deployment: | Parameter | Status | |---|---| | `DATABASE_HOST` | ✅ Set | | `DATABASE_USER` | ✅ Set | | `DATABASE_PASSWORD` | ❌ Not configured | | `DATABASE_NAME` | ✅ Set | **💡 Timeline** - **Apr 27, 10:15 UTC** — `DATABASE_PASSWORD` was removed from the production scope by `admin@company.com` during a credential rotation - **Apr 27, 14:23 UTC** — your deployment started without the updated parameter **✅ How to fix** Add the missing parameter and redeploy: ```bash # Set the parameter np scope parameter create \ --scope-id 3847291056 \ --key "DATABASE_PASSWORD" \ --value "your-secure-password" \ --secret true # Trigger a new deployment np build deploy \ --application-id 5739284610 \ --build-id 284 ```
### Build and deploy all applications **Prompt**: *"Create a script that builds and deploys all my apps in the production namespace"*
Assistant response ```bash #!/bin/bash # Build and deploy all applications in production namespace # Usage: ./deploy-all-apps.sh set -e NAMESPACE_ID="2847139502" echo "🔍 Fetching applications..." APPS=$(np application list --namespace_id "${NAMESPACE_ID}" --format json) echo "$APPS" | jq -r '.[].id' | while read -r APP_ID; do APP_NAME=$(echo "$APPS" | jq -r ".[] | select(.id==$APP_ID) | .name") echo "" echo "🚀 Deploying ${APP_NAME}..." # Start build BUILD_ID=$(np build start --application_id "${APP_ID}" --format json | jq -r '.id') echo " ⏳ Build ${BUILD_ID} in progress..." # Wait for completion while true; do STATUS=$(np build read --id "${BUILD_ID}" --format json | jq -r '.status') [ "$STATUS" == "success" ] && break [ "$STATUS" == "failed" ] && { echo " ❌ Failed"; exit 1; } sleep 10 done # Deploy np deployment create --body "{\"application_id\": ${APP_ID}, \"build_id\": ${BUILD_ID}}" > /dev/null echo " ✅ Deployed successfully" done echo "" echo "🎉 All applications deployed!" ```
Once you copy the script and run it, you'll see something like this: ``` 🔍 Fetching applications... 🚀 Deploying api-gateway... ⏳ Build 7392841056 in progress... ✅ Deployed successfully 🚀 Deploying user-service... ⏳ Build 7392841057 in progress... ✅ Deployed successfully 🚀 Deploying payment-service... ⏳ Build 7392841058 in progress... ✅ Deployed successfully 🎉 All applications deployed! ``` ### Service specification with PostgreSQL and Redis **Prompt**: *"I need to add PostgreSQL and Redis to my application. Show me the service spec"*
Assistant response Let me check what service specifications are already available. **📋 Available service specifications** | ID | Name | Type | |---|---|---| | `3f8a1d2e-7c4b-4e9f-a832-1d5e6f7a8b9c` | MySQL Database | dependency | | `9b2c4e6d-1a3f-4d7e-b891-2c3d4e5f6a7b` | SQS Queue | dependency | No PostgreSQL or Redis specifications found. Here are the commands to create them: ```bash np service specification create --body '{ "name": "PostgreSQL Database", "type": "dependency", "visible_to": ["organization=1234567890"], "use_default_actions": true, "selectors": { "category": "Database", "provider": "AWS", "sub_category": "Relational Database" }, "attributes": { "schema": { "type": "object", "properties": { "version": { "type": "string", "default": "15" }, "storage_gb": { "type": "number", "default": 20 }, "instance_class": { "type": "string", "default": "db.t3.micro" } } }, "values": {} } }' ``` ```bash np service specification create --body '{ "name": "Redis Cache", "type": "dependency", "visible_to": ["organization=1234567890"], "use_default_actions": true, "selectors": { "category": "Cache", "provider": "AWS", "sub_category": "In-Memory" }, "attributes": { "schema": { "type": "object", "properties": { "version": { "type": "string", "default": "7.0" }, "memory_gb": { "type": "number", "default": 2 }, "node_type": { "type": "string", "default": "cache.t3.micro" } } }, "values": {} } }' ```
Once the specifications are created, go to your application in the nullplatform UI, open the **Services** tab, and click **Add service** to create an instance from each specification. Nullplatform automatically injects these environment variables into your deployments once the service is provisioned: - `POSTGRES_HOST`, `POSTGRES_PORT`, `POSTGRES_DATABASE`, `POSTGRES_USER`, `POSTGRES_PASSWORD` - `REDIS_HOST`, `REDIS_PORT` ### Rollback to the previous deployment **Prompt**: *"Create a script that rolls back my application to the previous deployment if the current one is failing"*
Assistant response ```bash #!/bin/bash # Rollback to previous deployment # Usage: ./rollback.sh set -e APP_ID="${1}" if [ -z "$APP_ID" ]; then echo "Usage: $0 " exit 1 fi echo "🔍 Fetching deployment history..." DEPLOYMENTS=$(np deployment list \ --application_id "${APP_ID}" \ --limit 2 \ --format json) CURRENT_ID=$(echo "$DEPLOYMENTS" | jq -r '.[0].id') CURRENT_STATUS=$(echo "$DEPLOYMENTS" | jq -r '.[0].status') PREVIOUS_BUILD=$(echo "$DEPLOYMENTS" | jq -r '.[1].build_id') echo "📊 Current deployment: ${CURRENT_ID} (${CURRENT_STATUS})" if [ "$CURRENT_STATUS" == "healthy" ]; then echo "✅ Current deployment is healthy. No rollback needed." exit 0 fi echo "⚠️ Current deployment is ${CURRENT_STATUS}" echo "🔄 Rolling back..." NEW_DEPLOYMENT=$(np deployment create --body "{ \"application_id\": ${APP_ID}, \"build_id\": ${PREVIOUS_BUILD} }" --format json) NEW_ID=$(echo "$NEW_DEPLOYMENT" | jq -r '.id') echo "✅ Rollback deployment created: ${NEW_ID}" ```
Once you copy the script and run it, you'll see something like this: ``` $ ./rollback.sh 5739284610 🔍 Fetching deployment history... 📊 Current deployment: 9374628510 (failed) ⚠️ Current deployment is failed 🔄 Rolling back... ✅ Rollback deployment created: 9374628587 ``` ### Summary | Prompt | What the assistant generates | |---|---| | "Why did my deployment fail?" | Root cause analysis with timeline, logs, and a fix with ready-to-run CLI commands | | "Build and deploy all apps" | Script that iterates applications and deploys each one | | "PostgreSQL + Redis spec" | Service spec JSON and the `np service create` command | | "Rollback script" | Script that reverts to the previous deployment if the current one is failing | ## Next steps - [AI usage and quotas](/docs/ai/ai-usage): monitor token and session consumption for your organization and individual users - [AI Ops](/docs/ai-ops/): overview of the AI Ops features in nullplatform, including workflows, checklists, Insights, and action items --- ## Set up the AI plugins URL: https://docs.nullplatform.com/docs/ai-ops/ai-plugins The nullplatform AI plugins let you operate nullplatform from your coding assistant (Claude Code, Kiro, Cursor, and others). You describe what you want in plain language and the plugin queries entities, paginates results, and calls the right API endpoints for you. Every AI Ops feature starts from a plugin prompt, so set the plugins up once before following the rest of these docs. :::info The plugins are open source and maintained at [github.com/nullplatform/ai-plugins](https://github.com/nullplatform/ai-plugins). The repository is the source of truth for the full plugin catalog. ::: ## Before you start - A coding assistant that supports plugins. The commands below use Claude Code. For Kiro, follow the installer steps in the [repository README](https://github.com/nullplatform/ai-plugins#kiro-cli) - A [nullplatform API key](/docs/authorization/api-keys). The [role](/docs/authorization/roles) it needs depends on what you plan to do: **Developer** covers day-to-day operations like deployments and scope changes, while organization-level configuration (categories, checklists, workflows) needs **Admin** or **Ops** ## 1. Add the nullplatform marketplace The repository is a plugin marketplace called `nullplatform`. Add it once so your assistant knows where to find the plugins: ```bash claude plugin marketplace add nullplatform/ai-plugins ``` Adding the marketplace doesn't install anything yet. It only makes the nullplatform plugins available to install. ## 2. Install the plugins Install the two plugins that the AI Ops docs use: ```bash claude plugin install np-developer@nullplatform claude plugin install np-governance@nullplatform ``` - **np-developer**: queries, deployments, scope operations, and diagnostics - **np-governance**: action items, checklists, workflows, and reports :::tip The marketplace has more plugins for other jobs, like crafting services or managing the catalog. Browse the [plugin catalog on GitHub](https://github.com/nullplatform/ai-plugins) for the complete, up-to-date list and install the ones you need the same way. ::: Restart your coding assistant session after installing so it loads the new plugins. ## 3. Configure your API key Export your API key so the plugins can authenticate: ```bash export NP_API_KEY= ``` Add the same line to your shell profile (`~/.zshrc` or `~/.bashrc`) so new sessions pick it up. Your API key needs access to the accounts and namespaces you want to operate on. See [API keys](/docs/authorization/api-keys) to create one. > 💡 **Tip:** If you already use the [nullplatform CLI](/docs/cli/), you can skip the API key. When `NP_API_KEY` isn't set, the plugins reuse the session from `np login`. ## 4. Verify the setup Check that both plugins are installed: ```bash claude plugin list ``` You should see `np-developer@nullplatform` and `np-governance@nullplatform` in the list. Then start a session and ask: ``` Show my latest deployments in nullplatform ``` You don't need a special command. Your assistant picks the right plugin skill from what you ask. The plugin queries your organization and lists the most recent deployments, with their application, scope, and status. An empty list is a valid response if nothing has been deployed yet. If you get a list (or an empty list), the plugins are working. ## Keep the plugins up to date The plugins change often. Refresh the marketplace and update the plugins from time to time: ```bash claude plugin marketplace update nullplatform claude plugin update np-developer@nullplatform claude plugin update np-governance@nullplatform ``` ## Troubleshooting **`Plugin "." not found in any configured marketplace`**: you followed an older version of this guide. The repository is a marketplace, not a single plugin, so install by name as shown in steps 1 and 2. **The `nullplatform` marketplace already exists**: you added the previous nullplatform marketplace (`nullplatform/claude-plugins`, now archived). Remove it and add the current one: ```bash claude plugin marketplace remove nullplatform claude plugin marketplace add nullplatform/ai-plugins ``` **Authentication error**: check that `NP_API_KEY` is exported in the same shell where you started the session, and that the key has access to the accounts you're querying. ## Next steps - [Connect an AI assistant to nullplatform](/docs/mcp-server): add the hosted MCP server to any assistant, with browser sign-in instead of an API key - [Your first workflow](/docs/workflows/getting-started): describe a hello world workflow, run it, and activate it - [Your first checklist](/docs/approvals/your-first-checklist): describe a two-item deploy gate and watch it govern a real deploy - [Detect stale applications with AI](/docs/tutorials/governance-detect-stale-apps): find apps that haven't been deployed in over 90 days and open action items for them - [Track a runtime migration with Governance and Insights](/docs/tutorials/governance-track-migration): flag every affected service and follow the migration in a dashboard - [Action items](/docs/action-items/): what the plugins create and how teams work through it --- ## AI Ops URL: https://docs.nullplatform.com/docs/ai-ops/ **AI Ops** is a set of nullplatform features that take recurring operations work off your plate: things like catching an outdated runtime before it becomes an incident, holding a risky deploy until the right checks pass, or following a migration across every team. Everything **runs on your own data and under the rules your organization defines**. :::info 🚀 Early release AI Ops features are in early release and may not be available in your organization yet. If you'd like to try them or want to know more, [reach out to us](https://docs.nullplatform.com/docs/support) and we'll get you set up. ::: ## What's included Each feature works on its own: you can gate deploys with checklists without ever writing a workflow, or use Insights without action items. They also combine well. For example, to roll out an AMI update: 1. A **workflow** scans your scopes on a schedule, finds the ones running the outdated image, and opens an **action item** for each one, assigned to the team that owns it. 2. A **checklist** gates the deploy that applies the fix: it checks rules, asks an external system, or waits for a human sign-off before the deploy runs. 3. An **Insights** dashboard shows how many scopes are still pending across the organization. You build each piece the same way: install the AI plugins in your coding assistant, describe what you need in plain language, review the result in the UI, and activate it. ## Ready-to-use suites Nullplatform runs these suites on its own production environment. Each one is open source in [nullplatform/workflows](https://github.com/nullplatform/workflows) and comes with a tutorial of about 30 minutes. | Suite | What it does | Start here | |---|---|---| | Cost intelligence | Prices every Kubernetes scope daily, opens right-sizing suggestions where it finds waste, and verifies the savings after the change | [Right-size Kubernetes workloads](/docs/tutorials/cost-optimization) | | Runtime lifecycle | Keeps a catalog of AWS Lambda runtimes from the AWS docs and opens a migration item per affected scope | [Migrate deprecated Lambda runtimes](/docs/tutorials/lambda-runtime-lifecycle) | | AMI rollout | Detects scopes running an outdated image and tracks the rollout with action items that close themselves when the scope is updated | [Detect and roll out AMI updates](/docs/tutorials/ami-drift) | | Smart checklists | Gates deploys on a Jira ticket, an AI architecture audit, or a risk score that decides who approves | [Gate deploys on a Jira ticket](/docs/tutorials/jira-approval-gate), [Audit every deploy against your architecture rules](/docs/tutorials/ai-architecture-compliance), [Match approval friction to deploy risk](/docs/tutorials/deploy-risk-matrix) | | Deploy analysis and library inventory | Scores the risk of every deployment and records what each deployed asset depends on | The `deploy-analysis` and `lib-inventory` suites in [nullplatform/workflows](https://github.com/nullplatform/workflows) | ## Learning paths ## Building blocks Workflows and checklists are built on these features. Each one works on its own, and most workflows use at least one of them. Also see [AI usage and quotas](/docs/ai/ai-usage) to monitor AI consumption across your organization. --- ## Create or renew your access tokens URL: https://docs.nullplatform.com/docs/api/access-token-create Create or renew your access token. :::info If you need more info on tokens, check our [user guides](/docs/authorization/api-keys). ::: Choose either **Create a new token** or **Renew your token** to see the required parameters.\n","content":{"application/json":{"schema":{"oneOf":[{"title":"Create a new token","type":"object","description":"Provide either an `api_key` **or** the trio `username`, `password`, and `organization_id`.\nExactly one option must be supplied.\n","oneOf":[{"type":"object","title":"Using API key","required":["api_key"],"properties":{"api_key":{"type":"string","description":"The API key used to obtain your **new** `access_token` for the first time.\n","example":"AAAA.1234567890abcdef1234567890abcdefPTs="}},"additionalProperties":false},{"type":"object","title":"Using username and password","required":["username","password","organization_id"],"properties":{"username":{"type":"string","description":"Your username for authentication.\n\n> Include it in your request to **create** your `access_token`.\n","example":"alex.doe@email.com"},"password":{"type":"string","format":"password","description":"Your password for authentication.\n\n> Include it in your request to **create** your `access_token`.\n","example":"your_password"},"organization_id":{"type":"string","description":"The unique identifier for your organization.\n\n> Include it in your request to **create** your `access_token`.\n","example":"1234"}},"additionalProperties":false}]},{"title":"Renew your token","type":"object","required":["refresh_token","organization_id"],"properties":{"refresh_token":{"type":"string","description":"The token obtained during the initial authentication using your API key.\n\n> Include it in your request to **renew** your `access_token`.\n","example":"abJjdHki..."},"organization_id":{"type":"string","description":"The unique identifier for your organization.\n\n> Include it in your request to **renew** your `access_token`.\n","example":"1234"}}}],"title":"AuthTokenRequest"},"examples":{"New token by API key":{"value":{"api_key":"AAAA.1234567890abcdef1234567890abcdefPTs="}},"New token by username and password":{"value":{"username":"alex.doe@email.com","password":"your_password","organization_id":"1234"}},"Renew token":{"value":{"refresh_token":"abJjdHki...","organization_id":"1234"}}}}}}} > --- ## Create an account URL: https://docs.nullplatform.com/docs/api/account-create Creates a new account. --- ## Delete an account URL: https://docs.nullplatform.com/docs/api/account-delete Deletes a specific account by its ID. This action **cannot be undone**. :::info Before deleting a account, ensure that all associated namespaces have been deleted. ::: --- ## List accounts URL: https://docs.nullplatform.com/docs/api/account-list Lists accounts within an organization. --- ## Read an account URL: https://docs.nullplatform.com/docs/api/account-read Reads a specific account. --- ## Update an account URL: https://docs.nullplatform.com/docs/api/account-update Updates a specific account. ℹ️ **Note:** Changing the accounts's name **does not change the slug**, \n as doing so could break connections with existing applications and services.\n","example":"Crypto Kong"},"status":{"type":"string","enum":["active","inactive"],"description":"The status of the account.","example":"active"}},"title":"account_write"}}}}} > --- ## Read an action URL: https://docs.nullplatform.com/docs/api/action-any-read Read an action of a service or link. --- ## Generate the payload of a notification URL: https://docs.nullplatform.com/docs/api/action-context-generate Generate the payload of a notification --- ## Assign an action item URL: https://docs.nullplatform.com/docs/api/action-item-assign Assign an action item to a user of the organization, making that person its owner. An item has a single owner, so assigning an item that already has one reassigns it. Only items that still represent work can be assigned: `open`, `deferred`, `pending_deferral`, `pending_verification`, and `pending_rejection`. Assigning an item in `resolved`, `rejected`, or `closed` returns a `400`. --- ## Create a category URL: https://docs.nullplatform.com/docs/api/action-item-category-create Create a new action item category. Categories classify action items and define governance rules. --- ## Delete a category URL: https://docs.nullplatform.com/docs/api/action-item-category-delete Delete an action item category. > **Note:** A category cannot be deleted if it has subcategories or action items. Remove or reassign those first. --- ## List categories URL: https://docs.nullplatform.com/docs/api/action-item-category-list List action item categories filtered by NRN, name, parent, or status. Categories follow the NRN hierarchy. When querying for a specific NRN (without wildcards), the response includes categories from all ancestor NRN scopes, with the most specific category taking precedence when names overlap. --- ## Read a category URL: https://docs.nullplatform.com/docs/api/action-item-category-read Get a single action item category by ID. Includes parent and children if applicable. --- ## Update a category URL: https://docs.nullplatform.com/docs/api/action-item-category-update Update an action item category. Only the fields included in the request body are updated. --- ## Close an action item URL: https://docs.nullplatform.com/docs/api/action-item-close Close an action item without resolution. Only the original creator of the action item can close it. --- ## Add a comment URL: https://docs.nullplatform.com/docs/api/action-item-comment-create Add a comment to an action item. The comment author is always set from the authenticated user's bearer token. --- ## List comments URL: https://docs.nullplatform.com/docs/api/action-item-comment-list List all comments on an action item. --- ## Create an action item URL: https://docs.nullplatform.com/docs/api/action-item-create Create a new action item. The item is created in `open` status. By default, the creator is set from the authenticated user's bearer token. The `created_by` field is only honored for callers with delegation rights (for example, agent integrations authorized to create items on behalf of other users). --- ## Defer an action item URL: https://docs.nullplatform.com/docs/api/action-item-defer Defer an action item to a future date. If an approval action gates `action_item:defer` for this item, it moves to `pending_deferral` until the approval request is decided. Otherwise it transitions straight to `deferred`. The item automatically reopens when the deferred date passes. --- ## Delete an action item URL: https://docs.nullplatform.com/docs/api/action-item-delete Delete an action item and all its suggestions. --- ## List action items URL: https://docs.nullplatform.com/docs/api/action-item-list List action items with filtering, sorting, and pagination. Default sort order is by score descending, then by creation date descending. --- ## Read an action item URL: https://docs.nullplatform.com/docs/api/action-item-read Get a single action item by ID, including its category. --- ## Reject an action item URL: https://docs.nullplatform.com/docs/api/action-item-reject Reject an action item as not applicable. If an approval action gates `action_item:reject` for this item, it moves to `pending_rejection` until the approval request is decided. Otherwise it transitions straight to `rejected`. --- ## Reopen an action item URL: https://docs.nullplatform.com/docs/api/action-item-reopen Reopen a deferred, rejected, or closed action item. The item returns to `open` status. --- ## Resolve an action item URL: https://docs.nullplatform.com/docs/api/action-item-resolve Mark an action item as resolved. If an approval action gates `action_item:resolve` for this item, it moves to `pending_verification` until the approval request is decided. Otherwise it transitions straight to `resolved`. --- ## Unassign an action item URL: https://docs.nullplatform.com/docs/api/action-item-unassign Remove the owner from an action item, returning it to the unassigned pool. The `assignee_id`, `assigned_at`, and `assigned_by` fields are cleared. The status rule of `action_item:assign` applies here too. Unassigning an item that nobody owns returns a `400`. --- ## Update an action item URL: https://docs.nullplatform.com/docs/api/action-item-update Update an action item. Only the fields included in the request body are updated. --- ## List action specifications URL: https://docs.nullplatform.com/docs/api/action-specification-list List action specifications. --- ## Read an action specification URL: https://docs.nullplatform.com/docs/api/action-specification-read Read an action specification of a service or link. --- ## List snapshots of an action specification URL: https://docs.nullplatform.com/docs/api/action-specification-snapshots-list List the append-only snapshot history of an action specification, newest first. Snapshot ids are what package components pin as resource_revision_id. --- ## Read a snapshot of an action specification URL: https://docs.nullplatform.com/docs/api/action-specification-snapshots-read Read a single snapshot of an action specification, including its full content. --- ## Execute command URL: https://docs.nullplatform.com/docs/api/agent-command-execute Executes a command on a specific agent. . `exec`: Executes a command line with optional environment variables and arguments.\n>\n> . `refresh-sources`: Refreshes the agent's sources. This command does not require additional data.\n","example":"exec"},"data":{"type":"object","description":"The command line and environment variables.","properties":{"cmdline":{"type":"string","description":"The command line to be executed.\n> **Note**: The path to the command line should point to the folder where the agent is installed.\n","example":"path-to-cmd-file/cmd"},"environment":{"type":"object","description":"The environment variables for the command.\n> **Note**: The environment variables should be in the format `KEY: VALUE`.\n","example":{"NP_ACTION_CONTEXT":"'${NOTIFICATION_CONTEXT}"}},"arguments":{"type":"array","items":{"type":"string"},"description":"The arguments to be passed to the command line.\n","example":["arg1","arg2"]}}}}},"selector":{"type":"object","description":"The selector for the agent.\n","example":{"environment":"local"}}},"title":"agentCommandRequest"},"examples":{"example: exec command":{"summary":"Example command execution request","value":{"command":{"type":"exec","data":{"cmdline":"path-to-cmd-file/cmd","environment":{"NP_ACTION_CONTEXT":"'${NOTIFICATION_CONTEXT}"},"arguments":["arg1","arg2"]}},"selector":{"environment":"local"}}},"example: refresh sources":{"summary":"Example command to refresh agent sources","value":{"command":{"type":"refresh-sources","data":{}},"selector":{"cluster":"aec13961-1ad1-4c88-a57a-935f8848c911"}}}}}}}} > **Note**: The path to the command line should point to the folder where the agent is installed.\n","example":"path-to-cmd-file/cmd"},"environment":{"type":"object","description":"The environment variables for the command.\n> **Note**: The environment variables should be in the format `key: value`.\n","example":{"NP_ACTION_CONTEXT":"'${NOTIFICATION_CONTEXT}'"}},"arguments":{"type":"array","items":{"type":"string"},"description":"The arguments to be passed to the command line.\n","example":["arg1","arg2"]}}}}}},"title":"agentCommandResponse"}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## List agents URL: https://docs.nullplatform.com/docs/api/agent-list Lists agents within an organization. --- ## Read an agent URL: https://docs.nullplatform.com/docs/api/agent-read Retrieve a specific agent by its ID. --- ## Create an API key URL: https://docs.nullplatform.com/docs/api/api-key-create Creates a new API key. **Note**: Use `role_id` in every grant of a key, or `role_slug` in every grant,\n> never one in some and the other in others: every grant of a key is resolved the\n> same way.\n>\n","items":{"type":"object","oneOf":[{"type":"object","required":["nrn","role_slug"],"properties":{"nrn":{"type":"string","description":"The NRN of the resource where the API key's role is assigned.","example":"organization=1:account=1:namespace=1:application=4"},"role_slug":{"type":"string","description":"The slug of the role assigned to the API key for this NRN.\n\n> Note: See [Roles](/docs/authorization/roles) for more info.\n>\n","example":"machine:ci"}},"title":"grantsRoleSlug"},{"type":"object","required":["nrn","role_id"],"properties":{"nrn":{"type":"string","description":"The NRN of the resource where the API key's role is assigned.","example":"organization=1:account=1:namespace=1:application=4"},"role_id":{"type":"integer","description":"The ID of the role assigned to the API key for this NRN.\n\n> Note: See [Roles](/docs/authorization/roles) for more info.\n>\n","example":700317756}},"title":"grantsRoleId"},{"type":"object","required":["nrn","actions"],"properties":{"nrn":{"type":"string","description":"The NRN of the resource the grant applies to.","example":"organization=1:account=1:namespace=1"},"actions":{"type":"array","description":"The actions the API key may call at this NRN, without naming an existing role.\nnullplatform creates a role private to the key carrying exactly these actions.\n\n> Note: You can only grant actions you could hand out at that NRN yourself: the ones\n> you hold there, plus the ones carried by roles you may assign to an API key there.\n>\n","items":{"type":"string"},"example":["application:read","deployment:create"]}},"title":"grantsActions"},{"type":"object","required":["nrn","inherits"],"properties":{"nrn":{"type":"string","description":"The NRN of the resource the grant applies to.","example":"organization=1:account=1"},"inherits":{"type":"array","description":"Existing roles, by slug or ID, merged into a single role private to this key.\nInheriting two roles in one grant is not the same as granting them separately:\nit produces one role, which `actions.add` and `actions.remove` can then adjust.\nYou must be allowed to assign each inherited role to an API key at this NRN, the\nsame check a grant by `role_id` passes. The key follows the roles it inherits: an\naction later added to one of them reaches the key too.\n\n> Note: A role private to another API key cannot be inherited.\n>\n","items":{"oneOf":[{"type":"string"},{"type":"integer"}]},"example":["ops","developer"]},"actions":{"type":"object","description":"How the union of the inherited roles is adjusted.","properties":{"add":{"type":"array","description":"Actions granted on top of what the inherited roles provide. Measured against what you can hand out at this NRN.","items":{"type":"string"},"example":["application:delete"]},"remove":{"type":"array","description":"Inherited actions excluded from this grant. Each must be carried by one of the inherited roles, and none may also be in `add`.","items":{"type":"string"},"example":["deployment:create"]}}}},"title":"grantsInherits"}]}},"tags":{"type":"array","description":"Tags associated with your API key.","items":{"type":"object","properties":{"key":{"type":"string","description":"Tag key associated with your API key.","example":"CI"},"value":{"type":"string","description":"Tag value associated with your API key.","example":"main"}}}}},"title":"api_keyNew"},"examples":{"Create API key using role_slug":{"value":{"name":"my-machine-process-that-will-access-nullplatform","grants":[{"nrn":"organization=1:account=1:namespace=1:application=4","role_slug":"machine:ci"}],"tags":[{"key":"machine","value":"true"}]}},"Create API key using role_id":{"value":{"name":"my-machine-process-that-will-access-nullplatform","grants":[{"nrn":"organization=1:account=1:namespace=1:application=4","role_id":1855672260}],"tags":[{"key":"machine","value":"true"}]}}}}}}} > \n> ⚠️ **Save your API key securely.**\n>\n>The API key value will be displayed only once. Make sure to store it in a secure location as it cannot be retrieved later.","example":"AAAA.1234567890abcdef1234567890abcdefPTs="},"masked_api_key":{"type":"string","description":"Your masked API key.","example":"AAAA.xxxxxxxxxxxxxxxxxxxxxPTs=","nullable":true},"tags":{"type":"array","description":"Array of tags associated with your API key.\n\nℹ️ If no tags are found, we return an empty array.\n","items":{"type":"object","required":["key","value"],"properties":{"key":{"type":"string","description":"Tag key associated with your API key.","example":"CI"},"value":{"type":"string","description":"Tag value associated with your API key.","example":"main"}}}},"grants":{"type":"array","description":"Defines access grants for an API key. A grant created from `actions` or `inherits`\nreads back as the action names it resolved to; the role behind it is private to the\nkey and carries no meaning of its own.\n","items":{"type":"object","required":["nrn","role_id","role_slug"],"properties":{"nrn":{"type":"string","description":"The NRN of the resource where the API key's role is assigned.","example":"organization=1:account=1:namespace=1:application=4"},"role_id":{"type":"integer","description":"The ID of the role assigned to the API key for this NRN. For a grant created\nfrom `actions` or `inherits` this names a role private to the key, recreated\nwhenever the grants are replaced, so do not store it.\n","example":1855672260},"role_slug":{"type":"string","description":"The slug of the role assigned to the API key for this NRN. Private to the key,\nand not reusable, for a grant created from `actions` or `inherits`.\n","example":"machine:ci"},"actions":{"type":"array","description":"The action names this grant resolves to, present when the grant was created\nfrom `actions` or `inherits`. An action that has an alias is listed under the\nalias, not under the name it is stored as.\n","items":{"type":"string"},"example":["application:read","deployment:create"]},"inherits":{"type":"array","description":"The roles this grant merged. Present only for the `inherits` shape.","items":{"type":"object","properties":{"id":{"type":"integer","example":1855672260},"slug":{"type":"string","example":"ops"},"organization_id":{"type":"integer","example":1}}}},"added":{"type":"array","description":"The actions granted on top of what was inherited, as sent in `actions.add`.","items":{"type":"string"},"example":["application:delete"]},"removed":{"type":"array","description":"The inherited actions excluded, as sent in `actions.remove`.","items":{"type":"string"},"example":["deployment:create"]}}}},"owner_id":{"type":"integer","description":"The unique ID of the user that creates the API key.","example":1595},"last_used_at":{"type":"string","description":"The ISO-8601 UTC timestamp of when the API key was last used.","nullable":true,"format":"date-time","example":"2021-01-02T00:00:00Z"},"created_at":{"type":"string","description":"The ISO-8601 UTC timestamp of when the API key was created.","format":"date-time","example":"2021-01-02T00:00:00Z"},"updated_at":{"type":"string","description":"The ISO-8601 UTC timestamp of when the API key was last updated.","format":"date-time","example":"2021-01-02T00:00:00Z"}},"title":"apiKeyPostResponse"}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## Remove an API key URL: https://docs.nullplatform.com/docs/api/api-key-delete Removes an API key by ID. --- ## List API keys URL: https://docs.nullplatform.com/docs/api/api-key-list Lists all API keys. --- ## Read an API key URL: https://docs.nullplatform.com/docs/api/api-key-read Reads an API key by ID. --- ## Update an API key URL: https://docs.nullplatform.com/docs/api/api-key-update Updates an API key by ID. **Note**: Use `role_id` in every grant of a key, or `role_slug` in every grant,\n> never one in some and the other in others: every grant of a key is resolved the\n> same way.\n>\n","items":{"type":"object","oneOf":[{"type":"object","required":["nrn","role_slug"],"properties":{"nrn":{"type":"string","description":"The NRN of the resource where the API key's role is assigned.","example":"organization=1:account=1:namespace=1:application=4"},"role_slug":{"type":"string","description":"The slug of the role assigned to the API key for this NRN.\n\n> Note: See [Roles](/docs/authorization/roles) for more info.\n>\n","example":"machine:ci"}},"title":"grantsRoleSlug"},{"type":"object","required":["nrn","role_id"],"properties":{"nrn":{"type":"string","description":"The NRN of the resource where the API key's role is assigned.","example":"organization=1:account=1:namespace=1:application=4"},"role_id":{"type":"integer","description":"The ID of the role assigned to the API key for this NRN.\n\n> Note: See [Roles](/docs/authorization/roles) for more info.\n>\n","example":700317756}},"title":"grantsRoleId"},{"type":"object","required":["nrn","actions"],"properties":{"nrn":{"type":"string","description":"The NRN of the resource the grant applies to.","example":"organization=1:account=1:namespace=1"},"actions":{"type":"array","description":"The actions the API key may call at this NRN, without naming an existing role.\nnullplatform creates a role private to the key carrying exactly these actions.\n\n> Note: You can only grant actions you could hand out at that NRN yourself: the ones\n> you hold there, plus the ones carried by roles you may assign to an API key there.\n>\n","items":{"type":"string"},"example":["application:read","deployment:create"]}},"title":"grantsActions"},{"type":"object","required":["nrn","inherits"],"properties":{"nrn":{"type":"string","description":"The NRN of the resource the grant applies to.","example":"organization=1:account=1"},"inherits":{"type":"array","description":"Existing roles, by slug or ID, merged into a single role private to this key.\nInheriting two roles in one grant is not the same as granting them separately:\nit produces one role, which `actions.add` and `actions.remove` can then adjust.\nYou must be allowed to assign each inherited role to an API key at this NRN, the\nsame check a grant by `role_id` passes. The key follows the roles it inherits: an\naction later added to one of them reaches the key too.\n\n> Note: A role private to another API key cannot be inherited.\n>\n","items":{"oneOf":[{"type":"string"},{"type":"integer"}]},"example":["ops","developer"]},"actions":{"type":"object","description":"How the union of the inherited roles is adjusted.","properties":{"add":{"type":"array","description":"Actions granted on top of what the inherited roles provide. Measured against what you can hand out at this NRN.","items":{"type":"string"},"example":["application:delete"]},"remove":{"type":"array","description":"Inherited actions excluded from this grant. Each must be carried by one of the inherited roles, and none may also be in `add`.","items":{"type":"string"},"example":["deployment:create"]}}}},"title":"grantsInherits"}]}},"tags":{"type":"array","description":"Tags associated with your API key.","items":{"type":"object","properties":{"key":{"type":"string","description":"Tag key associated with your API key.","example":"CI"},"value":{"type":"string","description":"Tag value associated with your API key.","example":"main"}}}}},"title":"api_keyWrite"},"examples":{"API key (grants using role_slug)":{"value":{"name":"my-machine-process-that-will-access-nullplatform","grants":[{"nrn":"organization=1:account=1:namespace=1:application=4","role_slug":"machine:ci"}],"tags":[{"key":"machine","value":"true"}]}},"API key (grants using role_id)":{"value":{"name":"my-machine-process-that-will-access-nullplatform","grants":[{"nrn":"organization=1:account=1:namespace=1:application=4","role_id":1855672260}],"tags":[{"key":"machine","value":"true"}]}}}}}}} > --- ## Create an application URL: https://docs.nullplatform.com/docs/api/application-create Creates a new application in an existing namespace. --- ## List applications URL: https://docs.nullplatform.com/docs/api/application-list Lists applications within a namespace. --- ## Read an application URL: https://docs.nullplatform.com/docs/api/application-read Provides information about the application. --- ## Update an application URL: https://docs.nullplatform.com/docs/api/application-update Updates information for an application. Applications accept partial updates only: `PUT` on this path answers `405 Method Not Allowed`. ℹ️ **Note:** Changing the application's name **does not change the slug**, \nas doing so could break connections with existing applications and services.\n","example":"Billing API"},"status":{"type":"string","enum":["pending","creating","updating","active","inactive","failed","deleting"],"description":"The status of the application.","example":"active"},"repository_url":{"type":"string","maxLength":250,"description":"The URL of the repository that holds this application.","example":"https://github.com/crypto-inc/accounting-billing-api"},"template_id":{"type":"integer","description":"The ID of the template used to create this application. \n\n > ℹ️ **Note:** If you patch the template, make sure that the new template is compatible with the\n repository URL, especially if it includes rules.\n","example":3456},"tags":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","example":"key"},"value":{"type":"string","example":"value"}}}},"settings":{"type":"object","nullable":true,"description":"Configuration settings for the application. When updating via PATCH, follows RFC 7396 (JSON Merge Patch): keys with null values are deleted, keys with non-null values are updated/created, omitted keys are preserved.","properties":{"defaults":{"type":"object","nullable":true,"description":"Default values nullplatform falls back to when a branch or environment isn't specified explicitly.","properties":{"branch":{"type":"string","nullable":true,"description":"Default branch name for the application.","example":"main"},"environment":{"type":"string","nullable":true,"description":"Default environment name for the application.","example":"staging"}}},"asset":{"type":"object","nullable":true,"description":"Settings describing where the application's build assets are stored.","properties":{"docker_server":{"type":"object","nullable":true,"description":"Settings for the application's Docker server assets.","properties":{"uri":{"type":"string","nullable":true,"description":"Location where the application's Docker server assets are stored.","example":"registry.example.com/acme-corp/billing-api"}}},"ecr":{"type":"object","nullable":true,"description":"Amazon ECR repository where the application's assets are stored.","properties":{"uri":{"type":"string","nullable":true,"description":"ECR repository URI for the application's assets.","example":"123456789012.dkr.ecr.us-east-1.amazonaws.com/billing-api"},"arn":{"type":"string","nullable":true,"description":"ECR repository ARN for the application's assets.","example":"arn:aws:ecr:us-east-1:123456789012:repository/billing-api"}}}}},"code_repository":{"type":"object","nullable":true,"description":"Configuration of the code repository provider the application is linked to.","properties":{"specification_id":{"type":"string","format":"uuid","nullable":true,"description":"Unique identifier of the code repository specification.","example":"7f3c910e-8138-4d65-9d09-b97d89f2c509"},"specification_slug":{"type":"string","nullable":true,"description":"Slug of the code repository specification.","example":"github-configuration"},"specification_name":{"type":"string","nullable":true,"description":"Display name of the code repository specification.","example":"GitHub"},"specification_icon":{"type":"string","nullable":true,"description":"Icon identifier for the code repository specification.","example":"github"},"provider_id":{"type":"string","format":"uuid","nullable":true,"description":"Unique identifier of the code repository provider.","example":"82eee42c-46c9-4bb7-a53d-c08b027817f3"}}}},"title":"applicationSettings"}},"title":"application_write"}}}}} > --- ## Associate policy with action URL: https://docs.nullplatform.com/docs/api/approval-action-associate-policy Associates a policy with the specified approval action. --- ## Create an approval action URL: https://docs.nullplatform.com/docs/api/approval-action-create Creates a new approval action. **Note:** If omitted, this value defaults to **manual**.\n","example":"approve"},"on_policy_fail":{"type":"string","enum":["manual","deny"],"description":"The action to be taken when the policy check fails.\n\n> **Note:** If omitted, this value defaults to **manual**.\n","example":"deny"},"on_checklist_fail":{"type":"string","enum":["deny","pending","manual"],"description":"What happens to the request when its checklist run resolves as `fail`. Only read in checklist mode; policy-mode actions use `on_policy_fail` instead.\n\n- `deny`: the request is denied outright. Reviewers are never notified and manual review can't be requested.\n- `pending`: the request stays open. The requester fixes the cause and requests again, cancels, or asks for a manual review, which is when reviewers are notified.\n- `manual`: the request goes to manual review right away and reviewers are notified.\n\n> **Note:** If omitted, this value defaults to **pending**. An invalid value returns `400 INVALID_CHECKLIST_FAIL_PATH`.\n","example":"manual","title":"on_checklist_fail"},"time_to_reply":{"type":"integer","description":"How long (in milliseconds) approvers have to respond before the request expires. If the window closes without a decision, the request status moves to `expired`.","example":259200000},"allowed_time_to_execute":{"type":"integer","description":"How long (in milliseconds) after approval the action can be executed. If execution doesn't happen within this window, the `execution_status` moves to `expired`.","example":86400000}},"example":{"nrn":"organization=1:account=2:namespace=3:application=4","entity":"deployment","action":"deployment:create","dimensions":{"environment":"production","country":"us"},"on_policy_success":"approve","on_policy_fail":"deny","on_checklist_fail":"pending","time_to_reply":259200000,"allowed_time_to_execute":86400000},"title":"approval_actionCreate"}}}}} > **Note:** If omitted, this value defaults to **manual**.\n","example":"approve"},"on_policy_fail":{"type":"string","enum":["manual","deny"],"description":"The action to be taken when the policy check fails.\n\n> **Note:** If omitted, this value defaults to **manual**.\n","example":"deny"},"on_checklist_fail":{"type":"string","enum":["deny","pending","manual"],"description":"What happens to the request when its checklist run resolves as `fail`. Only read in checklist mode; policy-mode actions use `on_policy_fail` instead.\n\n- `deny`: the request is denied outright. Reviewers are never notified and manual review can't be requested.\n- `pending`: the request stays open. The requester fixes the cause and requests again, cancels, or asks for a manual review, which is when reviewers are notified.\n- `manual`: the request goes to manual review right away and reviewers are notified.\n\n> **Note:** If omitted, this value defaults to **pending**. An invalid value returns `400 INVALID_CHECKLIST_FAIL_PATH`.\n","example":"manual","title":"on_checklist_fail"},"time_to_reply":{"type":"integer","description":"How long (in milliseconds) approvers have to respond before the request expires. If the window closes without a decision, the request status moves to `expired`.","example":259200000},"allowed_time_to_execute":{"type":"integer","description":"How long (in milliseconds) after approval the action can be executed. If execution doesn't happen within this window, the `execution_status` moves to `expired`.","example":86400000}},"title":"approval_action"}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## Delete an action URL: https://docs.nullplatform.com/docs/api/approval-action-delete Permanently removes an approval action. This action cannot be undone." --- ## Disassociate policy from action URL: https://docs.nullplatform.com/docs/api/approval-action-disassociate-policy Disassociates a policy from the specified approval action. --- ## Link checklist specification to action URL: https://docs.nullplatform.com/docs/api/approval-action-link-checklist-specification Points an approval action at a checklist specification version. :::info An action runs on a checklist **or** on policies, never both. If the action still has policies attached, this call fails with `409` and a message naming how many policies are in the way. Reach out to the nullplatform team to move an action that already runs on policies onto a checklist. ::: --- ## List actions URL: https://docs.nullplatform.com/docs/api/approval-action-list List approval actions filtered by NRN, entity, action, or dimensions. **Note:** This is a hierarchical setting, so if you set approvals for a namespace, then all applications and scopes inside the namespace will match against the action.\n","example":"organization=1:account=2:namespace=4:application=4:scope=5"},{"in":"query","name":"entity","schema":{"type":"string","description":"The entity that requires approval.","enum":["deployment","scope","service:action","parameter","action_item"],"example":"deployment","title":"entity"},"required":true,"description":"The entity that requires approval.","example":"deployment"},{"in":"query","name":"action","schema":{"type":"string","description":"The action that requires approval.","enum":["deployment:create","scope:create","scope:recreate","scope:write","scope:delete","scope:stop","service:action:create","parameter:read-secrets","action_item:resolve","action_item:defer","action_item:reject"],"example":"deployment:create","title":"action"},"required":true,"description":"The action that requires approval.","example":"deployment:create"},{"in":"query","name":"dimensions","schema":{"type":"string"},"required":false,"description":"Specify the [dimensions](/docs/dimensions) you have configured.You can include multiple key-value pairs, separated by commas","example":"environment:production,country:us"},{"schema":{"type":"boolean","default":false},"in":"query","name":"show_descendants","required":false,"description":"Specifies whether to show entities defined in the lower level of the NRN hierarchy"}]} > **Note:** If omitted, this value defaults to **manual**.\n","example":"approve"},"on_policy_fail":{"type":"string","enum":["manual","deny"],"description":"The action to be taken when the policy check fails.\n\n> **Note:** If omitted, this value defaults to **manual**.\n","example":"deny"},"on_checklist_fail":{"type":"string","enum":["deny","pending","manual"],"description":"What happens to the request when its checklist run resolves as `fail`. Only read in checklist mode; policy-mode actions use `on_policy_fail` instead.\n\n- `deny`: the request is denied outright. Reviewers are never notified and manual review can't be requested.\n- `pending`: the request stays open. The requester fixes the cause and requests again, cancels, or asks for a manual review, which is when reviewers are notified.\n- `manual`: the request goes to manual review right away and reviewers are notified.\n\n> **Note:** If omitted, this value defaults to **pending**. An invalid value returns `400 INVALID_CHECKLIST_FAIL_PATH`.\n","example":"manual","title":"on_checklist_fail"},"time_to_reply":{"type":"integer","description":"How long (in milliseconds) approvers have to respond before the request expires. If the window closes without a decision, the request status moves to `expired`.","example":259200000},"allowed_time_to_execute":{"type":"integer","description":"How long (in milliseconds) after approval the action can be executed. If execution doesn't happen within this window, the `execution_status` moves to `expired`.","example":86400000}},"title":"approval_action"}}},"title":"actionListResponse"}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## Read an approval action URL: https://docs.nullplatform.com/docs/api/approval-action-read Reads an approval action. **Note:** If omitted, this value defaults to **manual**.\n","example":"approve"},"on_policy_fail":{"type":"string","enum":["manual","deny"],"description":"The action to be taken when the policy check fails.\n\n> **Note:** If omitted, this value defaults to **manual**.\n","example":"deny"},"on_checklist_fail":{"type":"string","enum":["deny","pending","manual"],"description":"What happens to the request when its checklist run resolves as `fail`. Only read in checklist mode; policy-mode actions use `on_policy_fail` instead.\n\n- `deny`: the request is denied outright. Reviewers are never notified and manual review can't be requested.\n- `pending`: the request stays open. The requester fixes the cause and requests again, cancels, or asks for a manual review, which is when reviewers are notified.\n- `manual`: the request goes to manual review right away and reviewers are notified.\n\n> **Note:** If omitted, this value defaults to **pending**. An invalid value returns `400 INVALID_CHECKLIST_FAIL_PATH`.\n","example":"manual","title":"on_checklist_fail"},"time_to_reply":{"type":"integer","description":"How long (in milliseconds) approvers have to respond before the request expires. If the window closes without a decision, the request status moves to `expired`.","example":259200000},"allowed_time_to_execute":{"type":"integer","description":"How long (in milliseconds) after approval the action can be executed. If execution doesn't happen within this window, the `execution_status` moves to `expired`.","example":86400000}},"title":"approval_action"}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## Unlink checklist specification from action URL: https://docs.nullplatform.com/docs/api/approval-action-unlink-checklist-specification Detaches the checklist specification from an approval action. --- ## Update an action URL: https://docs.nullplatform.com/docs/api/approval-action-update Updates the `on_policy_success`, `on_policy_fail`, `on_checklist_fail`, `time_to_reply`, and `allowed_time_to_execute` fields of an existing approval action. **Note:** If omitted, this value defaults to **pending**. An invalid value returns `400 INVALID_CHECKLIST_FAIL_PATH`.\n","example":"manual","title":"on_checklist_fail"},"time_to_reply":{"type":"integer","description":"How long (in milliseconds) approvers have to respond before the request expires. If the window closes without a decision, the request status moves to `expired`.","example":259200000},"allowed_time_to_execute":{"type":"integer","description":"How long (in milliseconds) after approval the action can be executed. If execution doesn't happen within this window, the `execution_status` moves to `expired`.","example":86400000}},"title":"approval_actionWrite"}}}}} > **Note:** If omitted, this value defaults to **manual**.\n","example":"approve"},"on_policy_fail":{"type":"string","enum":["manual","deny"],"description":"The action to be taken when the policy check fails.\n\n> **Note:** If omitted, this value defaults to **manual**.\n","example":"deny"},"on_checklist_fail":{"type":"string","enum":["deny","pending","manual"],"description":"What happens to the request when its checklist run resolves as `fail`. Only read in checklist mode; policy-mode actions use `on_policy_fail` instead.\n\n- `deny`: the request is denied outright. Reviewers are never notified and manual review can't be requested.\n- `pending`: the request stays open. The requester fixes the cause and requests again, cancels, or asks for a manual review, which is when reviewers are notified.\n- `manual`: the request goes to manual review right away and reviewers are notified.\n\n> **Note:** If omitted, this value defaults to **pending**. An invalid value returns `400 INVALID_CHECKLIST_FAIL_PATH`.\n","example":"manual","title":"on_checklist_fail"},"time_to_reply":{"type":"integer","description":"How long (in milliseconds) approvers have to respond before the request expires. If the window closes without a decision, the request status moves to `expired`.","example":259200000},"allowed_time_to_execute":{"type":"integer","description":"How long (in milliseconds) after approval the action can be executed. If execution doesn't happen within this window, the `execution_status` moves to `expired`.","example":86400000}},"title":"approval_action"}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## Approve a request URL: https://docs.nullplatform.com/docs/api/approval-approve-create Approves a request using its ID. :::note This endpoint is executed according to the URL you received in the approval notification. ::: --- ## Deny a request URL: https://docs.nullplatform.com/docs/api/approval-deny-create Denies a request by ID. :::note This endpoint is executed according to the URL you received in the approval notification. ::: --- ## Dry-run an approval URL: https://docs.nullplatform.com/docs/api/approval-dry-run Resolves the approval action a real request would resolve and previews the result without creating a request. In checklist mode, conditions are evaluated for real while manual and external items report what they would wait for. --- ## List approvals URL: https://docs.nullplatform.com/docs/api/approval-list Lists approvals. **Note:** This follows a hierarchical structure, meaning if approvals are set at the namespace level, all applications and scopes within that namespace will inherit the rule.\n","example":"organization=1:account=2:namespace=4:application=4:scope=5"},{"in":"query","name":"entity_name","schema":{"type":"string","description":"The entity that requires approval.","enum":["deployment","scope","service:action","parameter","action_item"],"example":"deployment","title":"entity"},"required":false,"description":"The entity that requires approval.","example":"deployment"},{"in":"query","name":"entity_action","schema":{"type":"string","description":"The action that requires approval.","enum":["deployment:create","scope:create","scope:recreate","scope:write","scope:delete","scope:stop","service:action:create","parameter:read-secrets","action_item:resolve","action_item:defer","action_item:reject"],"example":"deployment:create","title":"action"},"required":false,"description":"The action that requires approval.","example":"deployment:create"},{"in":"query","name":"status","schema":{"type":"string","description":"The current status of the approval request.","enum":["pending","approved","auto_approved","auto_denied","denied","cancelled","expired"],"example":"pending","title":"approval_status"},"required":false,"description":"The status of the approval.","example":"pending"},{"in":"query","name":"approval_action_id","schema":{"type":"integer"},"required":false,"description":"The ID of the action that triggered this approval.","example":3456},{"in":"query","name":"dimensions","schema":{"type":"string"},"required":false,"description":"Filter approvals by the [dimensions](/docs/dimensions) of the request, as comma-separated `key:value` pairs. Returns the approvals whose dimensions are included in the specified set; approvals that don't specify dimensions always match.","example":"environment:production"}]} > --- ## Read an approval URL: https://docs.nullplatform.com/docs/api/approval-read Reads an approval. --- ## Check if approval is required URL: https://docs.nullplatform.com/docs/api/approval-required-read Check if an approval is required for the specified NRN, entity, action, and dimensions. **Note:** This follows a hierarchical structure, meaning if approvals are set at the namespace level, all applications and scopes within that namespace will inherit the rule.\n","example":"organization=1:account=2:namespace=4:application=4:scope=5"},{"in":"query","name":"entity","schema":{"type":"string","description":"The entity that requires approval.","enum":["deployment","scope","service:action","parameter","action_item"],"example":"deployment","title":"entity"},"required":true,"description":"The entity that requires approval.","example":"deployment"},{"in":"query","name":"action","schema":{"type":"string","description":"The action that requires approval.","enum":["deployment:create","scope:create","scope:recreate","scope:write","scope:delete","scope:stop","service:action:create","parameter:read-secrets","action_item:resolve","action_item:defer","action_item:reject"],"example":"deployment:create","title":"action"},"required":true,"description":"The action that requires approval.","example":"deployment:create"},{"in":"query","name":"dimensions","schema":{"type":"string"},"required":false,"description":"Specify the [dimensions](/docs/dimensions) you have configured.You can include multiple key-value pairs, separated by commas","example":"environment:production,country:us"}]} > --- ## List platform artifacts URL: https://docs.nullplatform.com/docs/api/artifact-list List artifacts. Prefer the `visible_to` filter for consumption. It matches artifacts owned at the NRN, shared by ancestors or trailing-wildcard scopes, and global artifacts published with `organization=*`, which an owner `nrn` filter never returns. `id` accepts up to 200 comma-separated ids as a batched read. Every requested artifact must be readable by the caller. ` instead, and `visible_to` keeps only your own entries plus `organization=*`.","example":"organization=1:account=2"},"identity_meta":{"type":"object","additionalProperties":true,"description":"The identity subset of the meta blob: registry and repository, url (plus path when set), or location, depending on type.","example":{"registry":"123456789012.dkr.ecr.us-east-1.amazonaws.com","repository":"my-org/my-scope"}},"visible_to":{"type":"array","items":{"type":"string"},"description":"NRNs allowed to consume (read and link) this artifact. Supports trailing-wildcard scopes (for example, \"organization=1:account=*\") and the global wildcard \"organization=*\". Defaults to [nrn]. Modifications stay gated on the owning NRN."},"revision_count":{"type":"integer","minimum":0,"description":"How many revisions the artifact has.","example":3},"latest_revision_id":{"type":"string","format":"uuid","nullable":true,"description":"The newest revision, or null when the artifact has none.","example":"2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e"},"created_at":{"type":"string","format":"date-time","description":"When the artifact was first registered.","example":"2026-09-14T12:00:00.000Z"}},"title":"artifact"}}},"title":"artifactListResponse"}}}},"4XX":{"description":"Client error responses due to invalid input, missing parameters or insufficient permissions.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"integer","description":"HTTP status code.","example":400},"code":{"type":"string","description":"Machine-readable error code.","example":"bad_request"},"error":{"type":"string","description":"Short error name.","example":"Bad Request"},"message":{"type":"string","description":"Human-readable explanation.","example":"meta.digest must match sha256:<64-hex>"}},"title":"errorResponse"}}}},"5XX":{"description":"Server error responses.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"integer","description":"HTTP status code.","example":400},"code":{"type":"string","description":"Machine-readable error code.","example":"bad_request"},"error":{"type":"string","description":"Short error name.","example":"Bad Request"},"message":{"type":"string","description":"Human-readable explanation.","example":"meta.digest must match sha256:<64-hex>"}},"title":"errorResponse"}}}}}} > --- ## Read a platform artifact URL: https://docs.nullplatform.com/docs/api/artifact-read Read one artifact: its identity meta, visibility, revision count and latest revision id. ` instead, and `visible_to` keeps only your own entries plus `organization=*`.","example":"organization=1:account=2"},"identity_meta":{"type":"object","additionalProperties":true,"description":"The identity subset of the meta blob: registry and repository, url (plus path when set), or location, depending on type.","example":{"registry":"123456789012.dkr.ecr.us-east-1.amazonaws.com","repository":"my-org/my-scope"}},"visible_to":{"type":"array","items":{"type":"string"},"description":"NRNs allowed to consume (read and link) this artifact. Supports trailing-wildcard scopes (for example, \"organization=1:account=*\") and the global wildcard \"organization=*\". Defaults to [nrn]. Modifications stay gated on the owning NRN."},"revision_count":{"type":"integer","minimum":0,"description":"How many revisions the artifact has.","example":3},"latest_revision_id":{"type":"string","format":"uuid","nullable":true,"description":"The newest revision, or null when the artifact has none.","example":"2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e"},"created_at":{"type":"string","format":"date-time","description":"When the artifact was first registered.","example":"2026-09-14T12:00:00.000Z"}},"title":"artifact"}}}},"4XX":{"description":"Client error responses due to invalid input, missing parameters or insufficient permissions.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"integer","description":"HTTP status code.","example":400},"code":{"type":"string","description":"Machine-readable error code.","example":"bad_request"},"error":{"type":"string","description":"Short error name.","example":"Bad Request"},"message":{"type":"string","description":"Human-readable explanation.","example":"meta.digest must match sha256:<64-hex>"}},"title":"errorResponse"}}}},"5XX":{"description":"Server error responses.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"integer","description":"HTTP status code.","example":400},"code":{"type":"string","description":"Machine-readable error code.","example":"bad_request"},"error":{"type":"string","description":"Short error name.","example":"Bad Request"},"message":{"type":"string","description":"Human-readable explanation.","example":"meta.digest must match sha256:<64-hex>"}},"title":"errorResponse"}}}}}} > --- ## Register a platform artifact URL: https://docs.nullplatform.com/docs/api/artifact-register Idempotent upsert. The owner `nrn`, the `type` and the identity subset of `meta` identify the artifact; each unique full `meta` blob mints (or reuses) one revision. The response carries the artifact and revision ids that package components pin. Returns `201` when the call created the artifact or a new revision, and `200` when both already existed. On an artifact that already exists: - A `visible_to` in the body replaces the artifact's visibility. - `annotations` aren't part of the revision identity. When the body carries annotations, they replace the stored set as a whole (last write wins), so always send the complete set. Omitting them keeps the stored set. > ℹ️ **Note:** For `oci_image`, `tag` is part of the revision meta: registering an existing digest under a new tag mints a new revision. ℹ️ **Note:** A `git_repository` artifact is a pointer only. nullplatform never stores the repository content, so never commit secrets to a referenced repository.\n","example":{"registry":"123456789012.dkr.ecr.us-east-1.amazonaws.com","repository":"my-org/my-scope","digest":"sha256:4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945","tag":"v1.4.0"},"title":"artifactMeta"},"annotations":{"type":"object","additionalProperties":true,"description":"Descriptive data stored on the revision, never part of its identity. The curated `changelog` key must be a non-empty markdown string of up to 65,536 characters and is rendered as release notes in the console. Any other key is free-form (reverse-DNS names are encouraged) and stored verbatim. The whole object is capped at 262,144 characters once serialized as JSON.","example":{"changelog":"## 1.4.0\n\n- Adds blue-green support","org.opencontainers.image.source":"https://github.com/my-org/my-scope","org.opencontainers.image.version":"v1.4.0"},"title":"artifactAnnotations"},"visible_to":{"type":"array","items":{"type":"string"},"description":"Defaults to [nrn]. Use [\"organization=*\"] to publish globally.","example":["organization=1:account=2"]}},"title":"artifactRegistration"}}}}} > ℹ️ **Note:** A `git_repository` artifact is a pointer only. nullplatform never stores the repository content, so never commit secrets to a referenced repository.\n","example":{"registry":"123456789012.dkr.ecr.us-east-1.amazonaws.com","repository":"my-org/my-scope","digest":"sha256:4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945","tag":"v1.4.0"},"title":"artifactMeta"},"annotations":{"type":"object","additionalProperties":true,"nullable":true,"description":"The revision's annotations (see the registration body), or null when it has none."},"nrn":{"type":"string","description":"The parent artifact's owning NRN.","example":"organization=1:account=2"},"visible_to":{"type":"array","items":{"type":"string"},"description":"The parent artifact's visibility.","example":["organization=1:account=2"]},"created_at":{"type":"string","format":"date-time","description":"When the revision was registered.","example":"2026-09-14T12:00:00.000Z"}},"title":"artifactRevision"}}}},"201":{"description":"The call created the artifact or a new revision.","content":{"application/json":{"schema":{"type":"object","description":"One immutable revision, identified by the ids a package component pins.","properties":{"resource_id":{"type":"string","format":"uuid","description":"The parent artifact id (a package component's resource_id).","example":"5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f"},"resource_revision_id":{"type":"string","format":"uuid","description":"The revision id (a package component's resource_revision_id).","example":"2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e"},"type":{"type":"string","enum":["oci_image","oras_artifact","git_repository","blob"],"description":"The parent artifact's type.","example":"oci_image"},"meta":{"type":"object","additionalProperties":true,"description":"Flat meta blob. Its shape depends on `type`:\n\n- `oci_image`: `registry` and `repository` (strings), `digest` (`sha256:` followed by 64 lowercase hex characters), and an optional `tag` (an OCI tag, up to 128 characters).\n- `oras_artifact`: `registry`, `repository` and `digest` (same rules as `oci_image`), and an optional `artifact_media_type` string.\n- `git_repository`: `url` and `reference` (any non-empty ref, such as a commit SHA, tag or branch), and an optional `path`: the directory of a monorepo the artifact stands for. When present, `path` is part of the artifact identity, so the same `url` under different paths registers different artifacts, each with its own revisions. Omit it for the repository root. It must be relative to the repository root, use `/` as separator, have no `.` or `..` segments and no trailing slash; a path that doesn't follow these rules is rejected, never normalized. The url must not embed credentials: a password is rejected in any form, a user is rejected over http(s) (a user such as `git@` is allowed over ssh and the scp-like `user@host:path` form), and a url with a scheme must not carry a query string or fragment.\n- `blob`: `location` (starting with `s3://`, `gs://`, `https://` or `http://`), `sha256` (64 lowercase hex characters), and an optional `size_bytes` (non-negative integer).\n\nAny key not listed for the type is rejected, and so is an explicit `null` value: omit the key instead.\n\n> ℹ️ **Note:** A `git_repository` artifact is a pointer only. nullplatform never stores the repository content, so never commit secrets to a referenced repository.\n","example":{"registry":"123456789012.dkr.ecr.us-east-1.amazonaws.com","repository":"my-org/my-scope","digest":"sha256:4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945","tag":"v1.4.0"},"title":"artifactMeta"},"annotations":{"type":"object","additionalProperties":true,"nullable":true,"description":"The revision's annotations (see the registration body), or null when it has none."},"nrn":{"type":"string","description":"The parent artifact's owning NRN.","example":"organization=1:account=2"},"visible_to":{"type":"array","items":{"type":"string"},"description":"The parent artifact's visibility.","example":["organization=1:account=2"]},"created_at":{"type":"string","format":"date-time","description":"When the revision was registered.","example":"2026-09-14T12:00:00.000Z"}},"title":"artifactRevision"}}}},"4XX":{"description":"Client error responses due to invalid input, missing parameters or insufficient permissions.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"integer","description":"HTTP status code.","example":400},"code":{"type":"string","description":"Machine-readable error code.","example":"bad_request"},"error":{"type":"string","description":"Short error name.","example":"Bad Request"},"message":{"type":"string","description":"Human-readable explanation.","example":"meta.digest must match sha256:<64-hex>"}},"title":"errorResponse"}}}},"5XX":{"description":"Server error responses.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"integer","description":"HTTP status code.","example":400},"code":{"type":"string","description":"Machine-readable error code.","example":"bad_request"},"error":{"type":"string","description":"Short error name.","example":"Bad Request"},"message":{"type":"string","description":"Human-readable explanation.","example":"meta.digest must match sha256:<64-hex>"}},"title":"errorResponse"}}}}}} > --- ## Read artifact revisions by id, batched URL: https://docs.nullplatform.com/docs/api/artifact-revision-list-by-ids Resolve up to 200 artifact revision ids in one request, for example every artifact pin of a package bill of materials. Each revision is authorized against its parent artifact like the direct read, and the whole request fails when any parent isn't readable. Unknown ids are omitted from the results. ℹ️ **Note:** A `git_repository` artifact is a pointer only. nullplatform never stores the repository content, so never commit secrets to a referenced repository.\n","example":{"registry":"123456789012.dkr.ecr.us-east-1.amazonaws.com","repository":"my-org/my-scope","digest":"sha256:4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945","tag":"v1.4.0"},"title":"artifactMeta"},"annotations":{"type":"object","additionalProperties":true,"nullable":true,"description":"The revision's annotations (see the registration body), or null when it has none."},"nrn":{"type":"string","description":"The parent artifact's owning NRN.","example":"organization=1:account=2"},"visible_to":{"type":"array","items":{"type":"string"},"description":"The parent artifact's visibility.","example":["organization=1:account=2"]},"created_at":{"type":"string","format":"date-time","description":"When the revision was registered.","example":"2026-09-14T12:00:00.000Z"}},"title":"artifactRevision"}}},"title":"artifactRevisionListResponse"}}}},"4XX":{"description":"Client error responses due to invalid input, missing parameters or insufficient permissions.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"integer","description":"HTTP status code.","example":400},"code":{"type":"string","description":"Machine-readable error code.","example":"bad_request"},"error":{"type":"string","description":"Short error name.","example":"Bad Request"},"message":{"type":"string","description":"Human-readable explanation.","example":"meta.digest must match sha256:<64-hex>"}},"title":"errorResponse"}}}},"5XX":{"description":"Server error responses.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"integer","description":"HTTP status code.","example":400},"code":{"type":"string","description":"Machine-readable error code.","example":"bad_request"},"error":{"type":"string","description":"Short error name.","example":"Bad Request"},"message":{"type":"string","description":"Human-readable explanation.","example":"meta.digest must match sha256:<64-hex>"}},"title":"errorResponse"}}}}}} > --- ## Read an artifact revision directly by id URL: https://docs.nullplatform.com/docs/api/artifact-revision-read-standalone Read an artifact revision when you only have its id, for example a package component's resource_revision_id. Authorized against the parent artifact. ℹ️ **Note:** A `git_repository` artifact is a pointer only. nullplatform never stores the repository content, so never commit secrets to a referenced repository.\n","example":{"registry":"123456789012.dkr.ecr.us-east-1.amazonaws.com","repository":"my-org/my-scope","digest":"sha256:4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945","tag":"v1.4.0"},"title":"artifactMeta"},"annotations":{"type":"object","additionalProperties":true,"nullable":true,"description":"The revision's annotations (see the registration body), or null when it has none."},"nrn":{"type":"string","description":"The parent artifact's owning NRN.","example":"organization=1:account=2"},"visible_to":{"type":"array","items":{"type":"string"},"description":"The parent artifact's visibility.","example":["organization=1:account=2"]},"created_at":{"type":"string","format":"date-time","description":"When the revision was registered.","example":"2026-09-14T12:00:00.000Z"}},"title":"artifactRevision"}}}},"4XX":{"description":"Client error responses due to invalid input, missing parameters or insufficient permissions.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"integer","description":"HTTP status code.","example":400},"code":{"type":"string","description":"Machine-readable error code.","example":"bad_request"},"error":{"type":"string","description":"Short error name.","example":"Bad Request"},"message":{"type":"string","description":"Human-readable explanation.","example":"meta.digest must match sha256:<64-hex>"}},"title":"errorResponse"}}}},"5XX":{"description":"Server error responses.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"integer","description":"HTTP status code.","example":400},"code":{"type":"string","description":"Machine-readable error code.","example":"bad_request"},"error":{"type":"string","description":"Short error name.","example":"Bad Request"},"message":{"type":"string","description":"Human-readable explanation.","example":"meta.digest must match sha256:<64-hex>"}},"title":"errorResponse"}}}}}} > --- ## Read an artifact revision URL: https://docs.nullplatform.com/docs/api/artifact-revision-read Read one revision of an artifact: its full meta blob and annotations. ℹ️ **Note:** A `git_repository` artifact is a pointer only. nullplatform never stores the repository content, so never commit secrets to a referenced repository.\n","example":{"registry":"123456789012.dkr.ecr.us-east-1.amazonaws.com","repository":"my-org/my-scope","digest":"sha256:4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945","tag":"v1.4.0"},"title":"artifactMeta"},"annotations":{"type":"object","additionalProperties":true,"nullable":true,"description":"The revision's annotations (see the registration body), or null when it has none."},"nrn":{"type":"string","description":"The parent artifact's owning NRN.","example":"organization=1:account=2"},"visible_to":{"type":"array","items":{"type":"string"},"description":"The parent artifact's visibility.","example":["organization=1:account=2"]},"created_at":{"type":"string","format":"date-time","description":"When the revision was registered.","example":"2026-09-14T12:00:00.000Z"}},"title":"artifactRevision"}}}},"4XX":{"description":"Client error responses due to invalid input, missing parameters or insufficient permissions.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"integer","description":"HTTP status code.","example":400},"code":{"type":"string","description":"Machine-readable error code.","example":"bad_request"},"error":{"type":"string","description":"Short error name.","example":"Bad Request"},"message":{"type":"string","description":"Human-readable explanation.","example":"meta.digest must match sha256:<64-hex>"}},"title":"errorResponse"}}}},"5XX":{"description":"Server error responses.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"integer","description":"HTTP status code.","example":400},"code":{"type":"string","description":"Machine-readable error code.","example":"bad_request"},"error":{"type":"string","description":"Short error name.","example":"Bad Request"},"message":{"type":"string","description":"Human-readable explanation.","example":"meta.digest must match sha256:<64-hex>"}},"title":"errorResponse"}}}}}} > --- ## List revisions of an artifact URL: https://docs.nullplatform.com/docs/api/artifact-revisions-list List the revisions of one artifact, newest first. Each revision carries the full meta blob it pins. ℹ️ **Note:** A `git_repository` artifact is a pointer only. nullplatform never stores the repository content, so never commit secrets to a referenced repository.\n","example":{"registry":"123456789012.dkr.ecr.us-east-1.amazonaws.com","repository":"my-org/my-scope","digest":"sha256:4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945","tag":"v1.4.0"},"title":"artifactMeta"},"annotations":{"type":"object","additionalProperties":true,"nullable":true,"description":"The revision's annotations (see the registration body), or null when it has none."},"nrn":{"type":"string","description":"The parent artifact's owning NRN.","example":"organization=1:account=2"},"visible_to":{"type":"array","items":{"type":"string"},"description":"The parent artifact's visibility.","example":["organization=1:account=2"]},"created_at":{"type":"string","format":"date-time","description":"When the revision was registered.","example":"2026-09-14T12:00:00.000Z"}},"title":"artifactRevision"}}},"title":"artifactRevisionListResponse"}}}},"4XX":{"description":"Client error responses due to invalid input, missing parameters or insufficient permissions.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"integer","description":"HTTP status code.","example":400},"code":{"type":"string","description":"Machine-readable error code.","example":"bad_request"},"error":{"type":"string","description":"Short error name.","example":"Bad Request"},"message":{"type":"string","description":"Human-readable explanation.","example":"meta.digest must match sha256:<64-hex>"}},"title":"errorResponse"}}}},"5XX":{"description":"Server error responses.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"integer","description":"HTTP status code.","example":400},"code":{"type":"string","description":"Machine-readable error code.","example":"bad_request"},"error":{"type":"string","description":"Short error name.","example":"Bad Request"},"message":{"type":"string","description":"Human-readable explanation.","example":"meta.digest must match sha256:<64-hex>"}},"title":"errorResponse"}}}}}} > --- ## Create an asset URL: https://docs.nullplatform.com/docs/api/asset-create Creates a new asset. • For docker-image assets, this should be the local tagged image or image name.\n>\n> • For lambda assets, this should be the file path of the lambda package.\n","example":"https://example.com/my-asset.jpg"},"name":{"type":"string","description":"The name of the asset.","example":"My asset"},"build_id":{"type":"integer","description":"The ID of the build that the asset belongs to.","example":345},"application_id":{"type":"integer","description":"The ID of the application that the asset belongs to.","example":234},"commit-sha":{"type":"string","description":"The commit SHA that the asset was built from.","example":"1234"},"metadata":{"type":"object","description":"The metadata loaded via the metadata catalog API.","example":{"key":"value"},"properties":{"key":{"type":"string","description":"The key of the metadata.","example":"key"},"value":{"type":"string","description":"The value of the metadata.","example":null},"additional_properties":{"type":"object","description":"Additional metadata properties, including schemas and tags.","properties":{"schemas":{"type":"array","description":"The JSON schemas of the metadata.","items":{"type":"object"}},"tags":{"type":"array","description":"Tags associated with the metadata.","items":{"type":"object","properties":{"key":{"type":"string","example":"key"},"value":{"type":"string","example":"value"}}}}}}},"title":"MetadataComponent"},"platform":{"type":"string","description":"The platform of the asset.","enum":["x86_64","arm_64"],"example":"arm_64"}},"title":"assetCreate"}}}}} > •For docker-image assets, this should be the local tagged image or image name.\n> \n> •For lambda assets, this should be the file path of the lambda package.\n","example":"https://example.com/my-asset.jpg"},"nrn":{"type":"string","description":"The resource name (NRN) uniquely identifying the asset.","example":"organization=1:account=2:namespace=4:application=4:build=345:asset=123"},"branch":{"type":"string","description":"The branch that the asset was built from.","example":"main"},"commit":{"type":"object","properties":{"id":{"type":"integer","description":"The ID of the commit that the asset was built from.","example":234},"permalink":{"type":"string","description":"The permalink to the commit that the asset was built from.","example":"https://example.com/1234"}}}},"title":"assetResponse"}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## List assets URL: https://docs.nullplatform.com/docs/api/asset-list Lists assets by `build_id`. •For docker-image assets, this should be the local tagged image or image name.\n> \n> •For lambda assets, this should be the file path of the lambda package.\n","example":"https://example.com/my-asset.jpg"},"nrn":{"type":"string","description":"The resource name (NRN) uniquely identifying the asset.","example":"organization=1:account=2:namespace=4:application=4:build=345:asset=123"},"branch":{"type":"string","description":"The branch that the asset was built from.","example":"main"},"commit":{"type":"object","properties":{"id":{"type":"integer","description":"The ID of the commit that the asset was built from.","example":234},"permalink":{"type":"string","description":"The permalink to the commit that the asset was built from.","example":"https://example.com/1234"}}}},"title":"assetResponse"}}},"title":"assetListResponse"}}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## Read an asset URL: https://docs.nullplatform.com/docs/api/asset-read Reads an asset by ID. •For docker-image assets, this should be the local tagged image or image name.\n> \n> •For lambda assets, this should be the file path of the lambda package.\n","example":"https://example.com/my-asset.jpg"},"nrn":{"type":"string","description":"The resource name (NRN) uniquely identifying the asset.","example":"organization=1:account=2:namespace=4:application=4:build=345:asset=123"},"branch":{"type":"string","description":"The branch that the asset was built from.","example":"main"},"commit":{"type":"object","properties":{"id":{"type":"integer","description":"The ID of the commit that the asset was built from.","example":234},"permalink":{"type":"string","description":"The permalink to the commit that the asset was built from.","example":"https://example.com/1234"}}}},"title":"assetResponse"}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## Update an asset URL: https://docs.nullplatform.com/docs/api/asset-update Updates an asset. •For docker-image assets, this should be the local tagged image or image name.\n> \n> •For lambda assets, this should be the file path of the lambda package.\n","example":"https://example.com/my-asset.jpg"},"nrn":{"type":"string","description":"The resource name (NRN) uniquely identifying the asset.","example":"organization=1:account=2:namespace=4:application=4:build=345:asset=123"},"branch":{"type":"string","description":"The branch that the asset was built from.","example":"main"},"commit":{"type":"object","properties":{"id":{"type":"integer","description":"The ID of the commit that the asset was built from.","example":234},"permalink":{"type":"string","description":"The permalink to the commit that the asset was built from.","example":"https://example.com/1234"}}}},"title":"assetResponse"}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## Create a build URL: https://docs.nullplatform.com/docs/api/build-create Create a new build. --- ## List builds URL: https://docs.nullplatform.com/docs/api/build-list Lists builds for an application. --- ## Read a build URL: https://docs.nullplatform.com/docs/api/build-read Read a build by ID. --- ## Update a build URL: https://docs.nullplatform.com/docs/api/build-update Updates a build. --- ## Create catalog instance (multiple keys) URL: https://docs.nullplatform.com/docs/api/catalog-instance-multiple-create Creates a catalog data for one or more metadata keys on the specified entity. Each key must have a corresponding catalog specification. --- ## Read catalog instance (all keys) URL: https://docs.nullplatform.com/docs/api/catalog-instance-multiple-read Retrieves all catalog metadata keys and values defined for the specified entity. --- ## Update catalog instance (multiple keys) URL: https://docs.nullplatform.com/docs/api/catalog-instance-multiple-update Updates one or more catalog metadata keys for the specified entity. Changes are validated against the related catalog specifications. --- ## Create catalog instance (single key) URL: https://docs.nullplatform.com/docs/api/catalog-instance-single-create Creates catalog data for a single metadata key on the specified entity. The key must correspond to an existing catalog specification. --- ## Delete catalog instance (single key) URL: https://docs.nullplatform.com/docs/api/catalog-instance-single-delete Deletes catalog data for a single metadata key on the specified entity. The key must reference an existing catalog specification. --- ## Read catalog instance (single key) URL: https://docs.nullplatform.com/docs/api/catalog-instance-single-read Retrieves catalog data for a single metadata key on the specified entity. --- ## Update catalog instance (single key) URL: https://docs.nullplatform.com/docs/api/catalog-instance-single-update Updates catalog data for a single metadata key on the specified entity. Changes must comply with the associated catalog specification. --- ## Create a catalog specification URL: https://docs.nullplatform.com/docs/api/catalog-specification-create Creates a catalog specification for an entity. --- ## Delete a catalog specification URL: https://docs.nullplatform.com/docs/api/catalog-specification-delete Deletes a catalog specification. --- ## List catalog specifications URL: https://docs.nullplatform.com/docs/api/catalog-specification-list Lists all catalog specifications. --- ## Read a catalog specification URL: https://docs.nullplatform.com/docs/api/catalog-specification-read Reads a catalog specification for an entity. --- ## Update a catalog specification URL: https://docs.nullplatform.com/docs/api/catalog-specification-update Updates a catalog specification. --- ## Read a checklist run URL: https://docs.nullplatform.com/docs/api/checklist-run-read Returns the checklist run for an approval request: its aggregate status, every item with its state, and a summary. **Note:** If omitted, this value defaults to **pending**. An invalid value returns `400 INVALID_CHECKLIST_FAIL_PATH`.\n","example":"manual","title":"on_checklist_fail"}}},"execution":{"type":"object","description":"What approving the request does with the action behind it. Before the run resolves it says what will happen; afterwards, next to the request's `execution_status`, what did. Always present.","properties":{"trigger":{"type":"string","enum":["explicit","automatic_approval","any_approval"],"description":"The `execution_trigger` the run read from its specification snapshot. `explicit` when the specification doesn't set one.","example":"automatic_approval"},"automatic_run":{"type":"boolean","description":"Whether no person or external system took part in the run, the way `automatic_approval` counts it.","example":true},"on_approval":{"type":"string","enum":["execute","wait"],"description":"`execute` when approving starts the action on its own, `wait` when the action waits for an explicit execute.","example":"execute"}}}}}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## Create checklist specification URL: https://docs.nullplatform.com/docs/api/checklist-specification-create Creates the first version of a checklist specification. **Note:** If omitted or `null`, this value defaults to **explicit**. Any other value returns `422` with `rule: execution_trigger.invalid`.\n","example":"automatic_approval","title":"execution_trigger"}},"example":{"execution_trigger":"automatic_approval","items":[{"id":"coverage_gate","type":"condition","behavior":"gate","title":"Build coverage above 80%","query":{"build.metadata.coverage.lines.percent":{"$gte":80}}}]}}}}}}}} > **Note:** If omitted or `null`, this value defaults to **explicit**. Any other value returns `422` with `rule: execution_trigger.invalid`.\n","example":"automatic_approval","title":"execution_trigger"}}},"derived_expression":{"type":"string","description":"The boolean expression nullplatform computes the outcome with.","example":"( coverage_gate.passed AND security_signoff.passed ) OR cab_override.passed"},"created_by":{"type":"string","description":"ID of the user who published this version.","example":"4321"},"created_at":{"type":"string","format":"date-time","description":"When this version was published.","example":"2026-08-28T14:12:03.000Z"},"updated_at":{"type":"string","format":"date-time","description":"When this version was last modified.","example":"2026-08-28T14:12:03.000Z"},"warnings":{"type":"array","description":"Non-blocking notes the validator raised when the version was saved.","items":{"type":"string"},"example":[]},"versions":{"type":"array","description":"Every version published under the same name and NRN. Only on a read with `include_versions=true`.","items":"circular(ChecklistSpecification)"},"associated_actions":{"type":"array","description":"Approval actions currently pointing at this version. Only on a read of a single specification, and absent rather than empty when it was not resolved.","items":{"type":"object"},"example":[{"id":1842,"nrn":"organization=1:account=2:namespace=3","entity":"deployment","action":"deployment:create"}]}},"title":"ChecklistSpecification"}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## Delete checklist specification URL: https://docs.nullplatform.com/docs/api/checklist-specification-delete Soft-deletes a checklist specification. :::info Fails with `409` when an approval action still points at it. Unlink the action first, or point it at another specification. ::: --- ## List checklist specifications URL: https://docs.nullplatform.com/docs/api/checklist-specification-list Lists the checklist specifications visible at an NRN. **Note:** If omitted or `null`, this value defaults to **explicit**. Any other value returns `422` with `rule: execution_trigger.invalid`.\n","example":"automatic_approval","title":"execution_trigger"}}},"derived_expression":{"type":"string","description":"The boolean expression nullplatform computes the outcome with.","example":"( coverage_gate.passed AND security_signoff.passed ) OR cab_override.passed"},"created_by":{"type":"string","description":"ID of the user who published this version.","example":"4321"},"created_at":{"type":"string","format":"date-time","description":"When this version was published.","example":"2026-08-28T14:12:03.000Z"},"updated_at":{"type":"string","format":"date-time","description":"When this version was last modified.","example":"2026-08-28T14:12:03.000Z"},"warnings":{"type":"array","description":"Non-blocking notes the validator raised when the version was saved.","items":{"type":"string"},"example":[]},"versions":{"type":"array","description":"Every version published under the same name and NRN. Only on a read with `include_versions=true`.","items":"circular(ChecklistSpecification)"},"associated_actions":{"type":"array","description":"Approval actions currently pointing at this version. Only on a read of a single specification, and absent rather than empty when it was not resolved.","items":{"type":"object"},"example":[{"id":1842,"nrn":"organization=1:account=2:namespace=3","entity":"deployment","action":"deployment:create"}]}},"title":"ChecklistSpecification"}},"total":{"type":"integer","description":"Total number of specifications matching the query.","example":3},"offset":{"type":"integer","description":"Number of results skipped.","example":0},"limit":{"type":"integer","description":"Maximum number of results returned.","example":20}}}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## Read checklist specification URL: https://docs.nullplatform.com/docs/api/checklist-specification-read Returns one version of a checklist specification, including the expression derived from its items. **Note:** If omitted or `null`, this value defaults to **explicit**. Any other value returns `422` with `rule: execution_trigger.invalid`.\n","example":"automatic_approval","title":"execution_trigger"}}},"derived_expression":{"type":"string","description":"The boolean expression nullplatform computes the outcome with.","example":"( coverage_gate.passed AND security_signoff.passed ) OR cab_override.passed"},"created_by":{"type":"string","description":"ID of the user who published this version.","example":"4321"},"created_at":{"type":"string","format":"date-time","description":"When this version was published.","example":"2026-08-28T14:12:03.000Z"},"updated_at":{"type":"string","format":"date-time","description":"When this version was last modified.","example":"2026-08-28T14:12:03.000Z"},"warnings":{"type":"array","description":"Non-blocking notes the validator raised when the version was saved.","items":{"type":"string"},"example":[]},"versions":{"type":"array","description":"Every version published under the same name and NRN. Only on a read with `include_versions=true`.","items":"circular(ChecklistSpecification)"},"associated_actions":{"type":"array","description":"Approval actions currently pointing at this version. Only on a read of a single specification, and absent rather than empty when it was not resolved.","items":{"type":"object"},"example":[{"id":1842,"nrn":"organization=1:account=2:namespace=3","entity":"deployment","action":"deployment:create"}]}},"title":"ChecklistSpecification"}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## Publish a new specification version URL: https://docs.nullplatform.com/docs/api/checklist-specification-update Publishes a new version rather than editing in place. The response is a new specification ID under the same name and NRN, with the version number bumped. Approval actions keep pointing at the version they already reference until you move them. **Note:** If omitted or `null`, this value defaults to **explicit**. Any other value returns `422` with `rule: execution_trigger.invalid`.\n","example":"automatic_approval","title":"execution_trigger"}}},"derived_expression":{"type":"string","description":"The boolean expression nullplatform computes the outcome with.","example":"( coverage_gate.passed AND security_signoff.passed ) OR cab_override.passed"},"created_by":{"type":"string","description":"ID of the user who published this version.","example":"4321"},"created_at":{"type":"string","format":"date-time","description":"When this version was published.","example":"2026-08-28T14:12:03.000Z"},"updated_at":{"type":"string","format":"date-time","description":"When this version was last modified.","example":"2026-08-28T14:12:03.000Z"},"warnings":{"type":"array","description":"Non-blocking notes the validator raised when the version was saved.","items":{"type":"string"},"example":[]},"versions":{"type":"array","description":"Every version published under the same name and NRN. Only on a read with `include_versions=true`.","items":"circular(ChecklistSpecification)"},"associated_actions":{"type":"array","description":"Approval actions currently pointing at this version. Only on a read of a single specification, and absent rather than empty when it was not resolved.","items":{"type":"object"},"example":[{"id":1842,"nrn":"organization=1:account=2:namespace=3","entity":"deployment","action":"deployment:create"}]}},"title":"ChecklistSpecification"}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## Create an asset for a build URL: https://docs.nullplatform.com/docs/api/ci-application-asset-build-create Create a new asset for a specific build. --- ## Create a build for an application URL: https://docs.nullplatform.com/docs/api/ci-application-build-create Create a new build for an application with the provided details. --- ## Update the status of a build URL: https://docs.nullplatform.com/docs/api/ci-application-build-update Update the status of a specific build. --- ## Retrieve parameters for a specific application URL: https://docs.nullplatform.com/docs/api/ci-application-parameter-list Retrieve a list of parameters for a specific application by providing its ID. --- ## Retrieve a specific parameter for an application URL: https://docs.nullplatform.com/docs/api/ci-application-parameter-read Retrieve a specific parameter for an application by providing its ID and the parameter name. --- ## Retrieve metadata for applications URL: https://docs.nullplatform.com/docs/api/ci-application-read Retrieve metadata for applications based on specified query parameters. --- ## Authenticate and receive an access token URL: https://docs.nullplatform.com/docs/api/ci-login-create Authenticate using an API key and receive an access token for further API requests. --- ## Close the notifications of a thread URL: https://docs.nullplatform.com/docs/api/communications-notification-close Closes every open notification in your organization that was created with this `group_key`. Use it when the situation a thread announced is over, for example once an incident is resolved. --- ## Create a notification URL: https://docs.nullplatform.com/docs/api/communications-notification-create Creates a notification without an inbox rule: an announcement (`kind: broadcast`) for everyone in your organization, or a personal notification for a list of users. Send a `key` to make retries safe: creating again with the same key answers `200` with the existing notification and creates nothing. --- ## Delete an announcement URL: https://docs.nullplatform.com/docs/api/communications-notification-delete Deletes an announcement for everyone, together with the Slack messages it posted, as if it had never gone out. This can't be undone. Only announcements (`kind: broadcast`) can be deleted. To stop showing a personal notification, close it instead. --- ## List notifications URL: https://docs.nullplatform.com/docs/api/communications-notification-list Lists your organization's announcements and personal notifications, newest first, as the **Announcements** page shows them. --- ## Update a notification URL: https://docs.nullplatform.com/docs/api/communications-notification-patch Changes the fields you send and keeps the rest. Send at least one field. Set `status` to `closed` to close it for everyone, or to `open` to reopen it. `kind`, `nrn`, `key`, `group_key`, and `recipients` can't change. On a notification nullplatform sent your organization (`publisher: nullplatform`), only `status` can change. --- ## Read a notification URL: https://docs.nullplatform.com/docs/api/communications-notification-read Returns one of your organization's notifications or announcements. --- ## Create a deployment URL: https://docs.nullplatform.com/docs/api/deployment-create Create a new deployment in an existing scope --- ## Create deployment group URL: https://docs.nullplatform.com/docs/api/deployment-group-create Create a new deployment group with multiple deployments. **Note**: See [Deployments](/docs/deployments) for more information.\n>\n","example":"running"},"deployments":{"type":"array","items":{"type":"object","required":["scope_id","strategy","strategy_data"],"properties":{"scope_id":{"type":"string","description":"The ID of the scope where the deployment belongs.","example":12},"strategy":{"type":"string","description":"The deployment strategy being used.","enum":["initial","blue_green"],"example":"blue_green"},"strategy_data":{"type":"object","description":"Configuration data for the deployment strategy.","properties":{"switched_traffic":{"type":"integer","description":"Current percentage of traffic on the new version (0-100).","example":30},"desired_switched_traffic":{"type":"integer","description":"Target traffic percentage for the new version.","example":50},"amount_instances_to_wait":{"type":"integer","description":"Required number of instances before proceeding to the next step.","example":5},"healthy_instances":{"type":"integer","description":"Current count of healthy instances.","example":3},"launched_instances":{"type":"integer","description":"Total number of instances launched.","example":5},"switch_traffic_step":{"type":"boolean","description":"Indicates whether the deployment is in the active traffic switching phase.","example":true}}}},"title":"deploymentGroupObject"}},"scopes":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"The ID of the scope where the deployment belongs.","example":12},"name":{"type":"string","description":"The name for the scope.","example":"AR - Staging"}},"title":"scopeDeploymentGroup"}},"strategy_data":{"type":"object","description":"Configuration data for the deployment strategy.","properties":{"switched_traffic":{"type":"integer","description":"Current percentage of traffic on the new version (0-100).","example":30},"desired_switched_traffic":{"type":"integer","description":"Target traffic percentage for the new version.","example":50},"amount_instances_to_wait":{"type":"integer","description":"Required number of instances before proceeding to the next step.","example":5},"healthy_instances":{"type":"integer","description":"Current count of healthy instances.","example":3},"launched_instances":{"type":"integer","description":"Total number of instances launched.","example":5},"switch_traffic_step":{"type":"boolean","description":"Indicates whether the deployment is in the active traffic switching phase.","example":true}}},"application_id":{"type":"string","description":"The ID of the application the deployment group belongs to.","example":"6789"},"release_id":{"type":"string","description":"The ID of the release associated with the deployment.","example":"5678"},"deployments_amount":{"type":"integer","description":"Number of deployments in the group","example":5},"created_by":{"type":"string","description":"The user ID of the creator of the deployment group.","example":991},"updated_by":{"type":"string","description":"The user ID of the last person who updated the deployment group.","example":992},"created_at":{"type":"string","format":"date-time","description":"The ISO-8601 UTC timestamp of when the deployment group was created.","example":"2023-10-01T12:34:56Z"},"updated_at":{"type":"string","format":"date-time","description":"The ISO-8601 UTC timestamp of when the deployment group was last updated.","example":"2023-10-01T12:34:56Z"}},"title":"deploymentGroupResponse"}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## List deployment groups URL: https://docs.nullplatform.com/docs/api/deployment-group-list List all deployment groups for a specific application. **Note**: See [Deployments](/docs/deployments) for more information.\n>\n","example":"running"},"deployments":{"type":"array","items":{"type":"object","required":["scope_id","strategy","strategy_data"],"properties":{"scope_id":{"type":"string","description":"The ID of the scope where the deployment belongs.","example":12},"strategy":{"type":"string","description":"The deployment strategy being used.","enum":["initial","blue_green"],"example":"blue_green"},"strategy_data":{"type":"object","description":"Configuration data for the deployment strategy.","properties":{"switched_traffic":{"type":"integer","description":"Current percentage of traffic on the new version (0-100).","example":30},"desired_switched_traffic":{"type":"integer","description":"Target traffic percentage for the new version.","example":50},"amount_instances_to_wait":{"type":"integer","description":"Required number of instances before proceeding to the next step.","example":5},"healthy_instances":{"type":"integer","description":"Current count of healthy instances.","example":3},"launched_instances":{"type":"integer","description":"Total number of instances launched.","example":5},"switch_traffic_step":{"type":"boolean","description":"Indicates whether the deployment is in the active traffic switching phase.","example":true}}}},"title":"deploymentGroupObject"}},"scopes":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"The ID of the scope where the deployment belongs.","example":12},"name":{"type":"string","description":"The name for the scope.","example":"AR - Staging"}},"title":"scopeDeploymentGroup"}},"strategy_data":{"type":"object","description":"Configuration data for the deployment strategy.","properties":{"switched_traffic":{"type":"integer","description":"Current percentage of traffic on the new version (0-100).","example":30},"desired_switched_traffic":{"type":"integer","description":"Target traffic percentage for the new version.","example":50},"amount_instances_to_wait":{"type":"integer","description":"Required number of instances before proceeding to the next step.","example":5},"healthy_instances":{"type":"integer","description":"Current count of healthy instances.","example":3},"launched_instances":{"type":"integer","description":"Total number of instances launched.","example":5},"switch_traffic_step":{"type":"boolean","description":"Indicates whether the deployment is in the active traffic switching phase.","example":true}}},"application_id":{"type":"string","description":"The ID of the application the deployment group belongs to.","example":"6789"},"release_id":{"type":"string","description":"The ID of the release associated with the deployment.","example":"5678"},"deployments_amount":{"type":"integer","description":"Number of deployments in the group","example":5},"created_by":{"type":"string","description":"The user ID of the creator of the deployment group.","example":991},"updated_by":{"type":"string","description":"The user ID of the last person who updated the deployment group.","example":992},"created_at":{"type":"string","format":"date-time","description":"The ISO-8601 UTC timestamp of when the deployment group was created.","example":"2023-10-01T12:34:56Z"},"updated_at":{"type":"string","format":"date-time","description":"The ISO-8601 UTC timestamp of when the deployment group was last updated.","example":"2023-10-01T12:34:56Z"}},"title":"deploymentGroupResponse"}}},"title":"deploymentGroupPaginatedResponse"}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## Read deployment group URL: https://docs.nullplatform.com/docs/api/deployment-group-read Retrieves information about a specific deployment group. **Note**: See [Deployments](/docs/deployments) for more information.\n>\n","example":"running"},"deployments":{"type":"array","items":{"type":"object","required":["scope_id","strategy","strategy_data"],"properties":{"scope_id":{"type":"string","description":"The ID of the scope where the deployment belongs.","example":12},"strategy":{"type":"string","description":"The deployment strategy being used.","enum":["initial","blue_green"],"example":"blue_green"},"strategy_data":{"type":"object","description":"Configuration data for the deployment strategy.","properties":{"switched_traffic":{"type":"integer","description":"Current percentage of traffic on the new version (0-100).","example":30},"desired_switched_traffic":{"type":"integer","description":"Target traffic percentage for the new version.","example":50},"amount_instances_to_wait":{"type":"integer","description":"Required number of instances before proceeding to the next step.","example":5},"healthy_instances":{"type":"integer","description":"Current count of healthy instances.","example":3},"launched_instances":{"type":"integer","description":"Total number of instances launched.","example":5},"switch_traffic_step":{"type":"boolean","description":"Indicates whether the deployment is in the active traffic switching phase.","example":true}}}},"title":"deploymentGroupObject"}},"scopes":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"The ID of the scope where the deployment belongs.","example":12},"name":{"type":"string","description":"The name for the scope.","example":"AR - Staging"}},"title":"scopeDeploymentGroup"}},"strategy_data":{"type":"object","description":"Configuration data for the deployment strategy.","properties":{"switched_traffic":{"type":"integer","description":"Current percentage of traffic on the new version (0-100).","example":30},"desired_switched_traffic":{"type":"integer","description":"Target traffic percentage for the new version.","example":50},"amount_instances_to_wait":{"type":"integer","description":"Required number of instances before proceeding to the next step.","example":5},"healthy_instances":{"type":"integer","description":"Current count of healthy instances.","example":3},"launched_instances":{"type":"integer","description":"Total number of instances launched.","example":5},"switch_traffic_step":{"type":"boolean","description":"Indicates whether the deployment is in the active traffic switching phase.","example":true}}},"application_id":{"type":"string","description":"The ID of the application the deployment group belongs to.","example":"6789"},"release_id":{"type":"string","description":"The ID of the release associated with the deployment.","example":"5678"},"deployments_amount":{"type":"integer","description":"Number of deployments in the group","example":5},"created_by":{"type":"string","description":"The user ID of the creator of the deployment group.","example":991},"updated_by":{"type":"string","description":"The user ID of the last person who updated the deployment group.","example":992},"created_at":{"type":"string","format":"date-time","description":"The ISO-8601 UTC timestamp of when the deployment group was created.","example":"2023-10-01T12:34:56Z"},"updated_at":{"type":"string","format":"date-time","description":"The ISO-8601 UTC timestamp of when the deployment group was last updated.","example":"2023-10-01T12:34:56Z"}},"title":"deploymentGroupResponse"}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## Update deployment group URL: https://docs.nullplatform.com/docs/api/deployment-group-update Updates a deployment group's status or strategy data. **Note**: See [Deployments](/docs/deployments) for more information.\n>\n","example":"running"},"strategy_data":{"type":"object","description":"Configuration data for the deployment strategy.","properties":{"switched_traffic":{"type":"integer","description":"Current percentage of traffic on the new version (0-100).","example":30},"desired_switched_traffic":{"type":"integer","description":"Target traffic percentage for the new version.","example":50},"amount_instances_to_wait":{"type":"integer","description":"Required number of instances before proceeding to the next step.","example":5},"healthy_instances":{"type":"integer","description":"Current count of healthy instances.","example":3},"launched_instances":{"type":"integer","description":"Total number of instances launched.","example":5},"switch_traffic_step":{"type":"boolean","description":"Indicates whether the deployment is in the active traffic switching phase.","example":true}}}},"title":"deploymentGroupUpdateRequest"}}}}} > **Note**: See [Deployments](/docs/deployments) for more information.\n>\n","example":"running"},"deployments":{"type":"array","items":{"type":"object","required":["scope_id","strategy","strategy_data"],"properties":{"scope_id":{"type":"string","description":"The ID of the scope where the deployment belongs.","example":12},"strategy":{"type":"string","description":"The deployment strategy being used.","enum":["initial","blue_green"],"example":"blue_green"},"strategy_data":{"type":"object","description":"Configuration data for the deployment strategy.","properties":{"switched_traffic":{"type":"integer","description":"Current percentage of traffic on the new version (0-100).","example":30},"desired_switched_traffic":{"type":"integer","description":"Target traffic percentage for the new version.","example":50},"amount_instances_to_wait":{"type":"integer","description":"Required number of instances before proceeding to the next step.","example":5},"healthy_instances":{"type":"integer","description":"Current count of healthy instances.","example":3},"launched_instances":{"type":"integer","description":"Total number of instances launched.","example":5},"switch_traffic_step":{"type":"boolean","description":"Indicates whether the deployment is in the active traffic switching phase.","example":true}}}},"title":"deploymentGroupObject"}},"scopes":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"The ID of the scope where the deployment belongs.","example":12},"name":{"type":"string","description":"The name for the scope.","example":"AR - Staging"}},"title":"scopeDeploymentGroup"}},"strategy_data":{"type":"object","description":"Configuration data for the deployment strategy.","properties":{"switched_traffic":{"type":"integer","description":"Current percentage of traffic on the new version (0-100).","example":30},"desired_switched_traffic":{"type":"integer","description":"Target traffic percentage for the new version.","example":50},"amount_instances_to_wait":{"type":"integer","description":"Required number of instances before proceeding to the next step.","example":5},"healthy_instances":{"type":"integer","description":"Current count of healthy instances.","example":3},"launched_instances":{"type":"integer","description":"Total number of instances launched.","example":5},"switch_traffic_step":{"type":"boolean","description":"Indicates whether the deployment is in the active traffic switching phase.","example":true}}},"application_id":{"type":"string","description":"The ID of the application the deployment group belongs to.","example":"6789"},"release_id":{"type":"string","description":"The ID of the release associated with the deployment.","example":"5678"},"deployments_amount":{"type":"integer","description":"Number of deployments in the group","example":5},"created_by":{"type":"string","description":"The user ID of the creator of the deployment group.","example":991},"updated_by":{"type":"string","description":"The user ID of the last person who updated the deployment group.","example":992},"created_at":{"type":"string","format":"date-time","description":"The ISO-8601 UTC timestamp of when the deployment group was created.","example":"2023-10-01T12:34:56Z"},"updated_at":{"type":"string","format":"date-time","description":"The ISO-8601 UTC timestamp of when the deployment group was last updated.","example":"2023-10-01T12:34:56Z"}},"title":"deploymentGroupResponse"}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## List deployments URL: https://docs.nullplatform.com/docs/api/deployment-list Lists all deployments that belong to an application or scope. --- ## Read a deployment URL: https://docs.nullplatform.com/docs/api/deployment-read Read a specific deployment --- ## Create deployment strategy URL: https://docs.nullplatform.com/docs/api/deployment-strategy-create Creates a new deployment strategy --- ## Delete deployment strategy URL: https://docs.nullplatform.com/docs/api/deployment-strategy-delete Deletes a deployment strategy by its ID. --- ## List deployment strategies URL: https://docs.nullplatform.com/docs/api/deployment-strategy-list Retrieves a paginated list of deployment strategies --- ## Read a deployment strategy by ID URL: https://docs.nullplatform.com/docs/api/deployment-strategy-read Retrieves a specific deployment strategy by its ID. --- ## Update deployment strategy URL: https://docs.nullplatform.com/docs/api/deployment-strategy-update Updates an existing deployment strategy --- ## Update a deployment URL: https://docs.nullplatform.com/docs/api/deployment-update Updates information for a deployment --- ## Create a dimension URL: https://docs.nullplatform.com/docs/api/dimension-create Creates a dimension. --- ## Remove a dimension URL: https://docs.nullplatform.com/docs/api/dimension-delete Remove a dimension by ID. --- ## List dimensions URL: https://docs.nullplatform.com/docs/api/dimension-list List dimensions by NRN. --- ## Read a dimension URL: https://docs.nullplatform.com/docs/api/dimension-read Read a dimension by its ID. --- ## Create a dimension value URL: https://docs.nullplatform.com/docs/api/dimension-value-create Create dimension value. --- ## Remove a dimension value URL: https://docs.nullplatform.com/docs/api/dimension-value-delete Remove a dimension value by ID. --- ## List dimension values URL: https://docs.nullplatform.com/docs/api/dimension-value-list List dimension values by NRN. --- ## Read a dimension value URL: https://docs.nullplatform.com/docs/api/dimension-value-read Read a dimension value by its ID. --- ## Update a dimension value URL: https://docs.nullplatform.com/docs/api/dimension-value-write Updates a dimension value. --- ## Update a dimension URL: https://docs.nullplatform.com/docs/api/dimension-write Updates an dimension. --- ## Create an entity hook action URL: https://docs.nullplatform.com/docs/api/entity-hook-action-create Create an action for an entity hook. **Note:** Currently, only \"hook\" is supported.\n","example":"hook"},"on":{"type":"string","enum":["create","update","delete"],"description":"Specifies the type of action that triggers the hook.","example":"create"}},"title":"entityHook_create"}}}}} > **Note:** Currently, only \"manual\" is supported.\n","example":"manual"},"on_policy_fail":{"type":"string","enum":["manual"],"description":"The action to be taken on entity hook failure.\"\n\n > **Note:** Currently, only \"manual\" is supported.\n","example":"manual"},"when":{"type":"string","enum":["before","after"],"description":"Specifies whether the hook occurs before or after nullplatform's internal logic.\n","example":"before"},"type":{"type":"string","enum":["hook"],"description":"The type of entity hook action.\n\n> **Note:** Currently, only \"hook\" is supported.\n","example":"hook"},"on":{"type":"string","enum":["create","update","delete"],"description":"Specifies the type of action that triggers the hook.","example":"create"},"policies":{"type":"array","description":"The policies applied to the entity.\n\n> ℹ️ If no policies are found, we return an empty array.\n","items":{"type":"object","properties":{"id":{"type":"integer","description":"A unique identifier for the policy.","example":5678},"nrn":{"type":"string","description":"A resource identifier (NRN) specifying where the policy applies.","example":"organization=1:account=2:namespace=3:application=4"},"name":{"type":"string","description":"The name of the policy.","example":"Crypto Kong Policies"},"conditions":{"type":"object","description":"A JSON object defining the conditions under which this policy applies.","example":{"scope.requested_spec.memory_in_gb":{"$lte":4}}}}}}},"title":"entityHook"}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## Delete an entity hook action URL: https://docs.nullplatform.com/docs/api/entity-hook-action-delete Delete an entity hook action. --- ## Find entity hook actions matching a request URL: https://docs.nullplatform.com/docs/api/entity-hook-action-find Returns the entity hook actions that apply to a given NRN, action, dimensions, and an optional `request_body`. Use this endpoint to discover, before mutating an entity, which hooks should fire and how to dispatch them (their `when`/`type` differ). For each matching hook, the API runs a dry-run notification against the notifications service with `context.request_body` set to the supplied payload. Hooks whose dry-run finds no channel willing to deliver (channels with no `filters` match, or no subscribed channel at all) are excluded from the response. Hooks where at least one channel would deliver are included. Response shape is the same as `GET /entity_hook/action`. **Note:** Currently, only \"manual\" is supported.\n","example":"manual"},"on_policy_fail":{"type":"string","enum":["manual"],"description":"The action to be taken on entity hook failure.\"\n\n > **Note:** Currently, only \"manual\" is supported.\n","example":"manual"},"when":{"type":"string","enum":["before","after"],"description":"Specifies whether the hook occurs before or after nullplatform's internal logic.\n","example":"before"},"type":{"type":"string","enum":["hook"],"description":"The type of entity hook action.\n\n> **Note:** Currently, only \"hook\" is supported.\n","example":"hook"},"on":{"type":"string","enum":["create","update","delete"],"description":"Specifies the type of action that triggers the hook.","example":"create"},"policies":{"type":"array","description":"The policies applied to the entity.\n\n> ℹ️ If no policies are found, we return an empty array.\n","items":{"type":"object","properties":{"id":{"type":"integer","description":"A unique identifier for the policy.","example":5678},"nrn":{"type":"string","description":"A resource identifier (NRN) specifying where the policy applies.","example":"organization=1:account=2:namespace=3:application=4"},"name":{"type":"string","description":"The name of the policy.","example":"Crypto Kong Policies"},"conditions":{"type":"object","description":"A JSON object defining the conditions under which this policy applies.","example":{"scope.requested_spec.memory_in_gb":{"$lte":4}}}}}}},"title":"entityHook"}}},"title":"entityHookListResponse"}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## List entity hook actions URL: https://docs.nullplatform.com/docs/api/entity-hook-action-list Lists entity hook actions within an organization. **Note:** Currently, only \"manual\" is supported.\n","example":"manual"},"on_policy_fail":{"type":"string","enum":["manual"],"description":"The action to be taken on entity hook failure.\"\n\n > **Note:** Currently, only \"manual\" is supported.\n","example":"manual"},"when":{"type":"string","enum":["before","after"],"description":"Specifies whether the hook occurs before or after nullplatform's internal logic.\n","example":"before"},"type":{"type":"string","enum":["hook"],"description":"The type of entity hook action.\n\n> **Note:** Currently, only \"hook\" is supported.\n","example":"hook"},"on":{"type":"string","enum":["create","update","delete"],"description":"Specifies the type of action that triggers the hook.","example":"create"},"policies":{"type":"array","description":"The policies applied to the entity.\n\n> ℹ️ If no policies are found, we return an empty array.\n","items":{"type":"object","properties":{"id":{"type":"integer","description":"A unique identifier for the policy.","example":5678},"nrn":{"type":"string","description":"A resource identifier (NRN) specifying where the policy applies.","example":"organization=1:account=2:namespace=3:application=4"},"name":{"type":"string","description":"The name of the policy.","example":"Crypto Kong Policies"},"conditions":{"type":"object","description":"A JSON object defining the conditions under which this policy applies.","example":{"scope.requested_spec.memory_in_gb":{"$lte":4}}}}}}},"title":"entityHook"}}},"title":"entityHookListResponse"}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## Read an entity hook action URL: https://docs.nullplatform.com/docs/api/entity-hook-action-read Read an entity hook action. **Note:** Currently, only \"manual\" is supported.\n","example":"manual"},"on_policy_fail":{"type":"string","enum":["manual"],"description":"The action to be taken on entity hook failure.\"\n\n > **Note:** Currently, only \"manual\" is supported.\n","example":"manual"},"when":{"type":"string","enum":["before","after"],"description":"Specifies whether the hook occurs before or after nullplatform's internal logic.\n","example":"before"},"type":{"type":"string","enum":["hook"],"description":"The type of entity hook action.\n\n> **Note:** Currently, only \"hook\" is supported.\n","example":"hook"},"on":{"type":"string","enum":["create","update","delete"],"description":"Specifies the type of action that triggers the hook.","example":"create"},"policies":{"type":"array","description":"The policies applied to the entity.\n\n> ℹ️ If no policies are found, we return an empty array.\n","items":{"type":"object","properties":{"id":{"type":"integer","description":"A unique identifier for the policy.","example":5678},"nrn":{"type":"string","description":"A resource identifier (NRN) specifying where the policy applies.","example":"organization=1:account=2:namespace=3:application=4"},"name":{"type":"string","description":"The name of the policy.","example":"Crypto Kong Policies"},"conditions":{"type":"object","description":"A JSON object defining the conditions under which this policy applies.","example":{"scope.requested_spec.memory_in_gb":{"$lte":4}}}}}}},"title":"entityHook"}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## Update an entity hook action URL: https://docs.nullplatform.com/docs/api/entity-hook-action-update Updates an entity hook action. **Note:** Currently, only \"hook\" is supported.\n","example":"hook"},"on":{"type":"string","enum":["create","update","delete"],"description":"Specifies the type of action that triggers the hook.","example":"create"}},"title":"entityHook_write"}}}}} > **Note:** Currently, only \"manual\" is supported.\n","example":"manual"},"on_policy_fail":{"type":"string","enum":["manual"],"description":"The action to be taken on entity hook failure.\"\n\n > **Note:** Currently, only \"manual\" is supported.\n","example":"manual"},"when":{"type":"string","enum":["before","after"],"description":"Specifies whether the hook occurs before or after nullplatform's internal logic.\n","example":"before"},"type":{"type":"string","enum":["hook"],"description":"The type of entity hook action.\n\n> **Note:** Currently, only \"hook\" is supported.\n","example":"hook"},"on":{"type":"string","enum":["create","update","delete"],"description":"Specifies the type of action that triggers the hook.","example":"create"},"policies":{"type":"array","description":"The policies applied to the entity.\n\n> ℹ️ If no policies are found, we return an empty array.\n","items":{"type":"object","properties":{"id":{"type":"integer","description":"A unique identifier for the policy.","example":5678},"nrn":{"type":"string","description":"A resource identifier (NRN) specifying where the policy applies.","example":"organization=1:account=2:namespace=3:application=4"},"name":{"type":"string","description":"The name of the policy.","example":"Crypto Kong Policies"},"conditions":{"type":"object","description":"A JSON object defining the conditions under which this policy applies.","example":{"scope.requested_spec.memory_in_gb":{"$lte":4}}}}}}},"title":"entityHook"}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## Read an entity hook request URL: https://docs.nullplatform.com/docs/api/entity-hook-request-read Reads an entity hook request: one firing of an entity hook action for a specific entity. The `id` is the one you receive in the hook notification (`notification.id`), and the URL matches its `callback_url`. --- ## Respond to an entity hook URL: https://docs.nullplatform.com/docs/api/entity-hook-request-update Responds to an entity hook. Send this request to the `callback_url` from the hook notification once your logic has run. The `status` you send decides how nullplatform resumes the entity operation. Add `callback_body` to hand fields back to the entity: they are merged into the update nullplatform sends when it resumes, so a before-hook on `application:create` can, for example, decide the repository the application is created in. > ℹ️ **Note:** Only pending hooks can be updated. Fields nullplatform sets itself, such as the entity `status`, take precedence over `callback_body`. --- ## Grant access to a user URL: https://docs.nullplatform.com/docs/api/grants-create Give users access to resources (NRN) in your organization. You can **use either the role slug or the role ID** to specify the role you want to assign to the user. Note: See [Roles](/docs/authorization/roles) for more info.\n>\n","example":"developer"}}},{"title":"Using role ID","type":"object","required":["nrn","user_id","role_id"],"properties":{"nrn":{"type":"string","description":"The NRN of the resource where the API key's role is assigned.","example":"organization=1:account=1:namespace=1:application=4"},"user_id":{"type":"integer","description":"The ID of the user that **receives** the grant.\n","example":700317756},"role_id":{"type":"integer","description":"The ID of the role assigned to the API key for this NRN.\n\n> Note: See [Roles](/docs/authorization/roles) for more info.\n>\n","example":700317756}}}],"title":"grantNewBase"},"examples":{"Using role slug":{"value":{"nrn":"organization=1:account=2","user_id":1234,"role_slug":"developer"}},"Using role ID":{"value":{"nrn":"organization=1:account=2","user_id":1234,"role_id":700317756}}}}}}} > Note: See [Roles](/docs/authorization/roles) for more info.\n>\n","example":"developer"}}},{"title":"Using role ID","type":"object","required":["nrn","user_id","role_id"],"properties":{"nrn":{"type":"string","description":"The NRN of the resource where the API key's role is assigned.","example":"organization=1:account=1:namespace=1:application=4"},"user_id":{"type":"integer","description":"The ID of the user that **receives** the grant.\n","example":700317756},"role_id":{"type":"integer","description":"The ID of the role assigned to the API key for this NRN.\n\n> Note: See [Roles](/docs/authorization/roles) for more info.\n>\n","example":700317756}}}],"title":"grantNewBase"}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## Create an inbox rule URL: https://docs.nullplatform.com/docs/api/inbox-rule-create Creates an inbox rule for your organization from its definition. Sending the ID of a built-in rule edits that rule for your organization instead of creating a new one. To start over, reset it to default from the UI. An organization can have up to 50 active inbox rules of its own, edited built-in rules included, and up to 10 active rules on the same entity. ℹ️ **Note:** It can't start with `platform.`. Using a built-in rule's ID edits that rule for your organization.\n","example":"acme.security_finding"},"status":{"type":"string","enum":["active","deprecated"],"description":"Whether the rule is active. Defaults to `active`. `deprecated` means deactivated.","example":"active"},"source":{"description":"What makes the rule fire:\n\n- `{ \"kind\": \"invoked\" }`: your code calls the emission endpoint.\n- A source name, like `\"deployment\"`: the entity changes status in nullplatform. See `GET /communications/inbox_rule/source`.\n- `{ \"kind\": \"stale\", ... }`: an entity stays in a status for too long.\n","oneOf":[{"type":"string"},{"type":"object","additionalProperties":true}],"example":{"kind":"invoked"}},"context_schema":{"type":"object","additionalProperties":true,"description":"A JSON Schema for the data each send carries. An invoked rule must require `id`, because sends are idempotent by it.","example":{"type":"object","required":["id","service","owner_id"],"properties":{"id":{"type":"string"},"service":{"type":"string"},"summary":{"type":"string"},"owner_id":{"type":"integer"}}}},"category":{"type":"object","description":"How the rule shows up in inbox settings. All six fields are required.","required":["group","label","description","configurable","allows_all","default_mode"],"properties":{"group":{"type":"string","description":"The group the rule is listed under in inbox settings.","example":"security"},"label":{"type":"string","description":"The rule's name in inbox settings.","example":"Security findings"},"description":{"type":"string","maxLength":70,"description":"One line on what the rule announces.","example":"Vulnerabilities your CI scanners report."},"configurable":{"type":"boolean","description":"Whether people can change their mode for this rule.","example":true},"allows_all":{"type":"boolean","description":"Whether people can choose **Everything** for this rule under a resource. Requires `configurable`.","example":true},"default_mode":{"type":"string","enum":["off","involved","all"],"description":"What people get until they change it.","example":"involved"}}},"severity":{"type":"string","enum":["informational","maintenance","degraded","outage"],"description":"Sets the icon and the color of the notification.","example":"degraded"},"audience":{"type":"array","description":"One entry per reading. Each entry is a separate notification with its own wording.","items":{"type":"object","required":["as","action","content"],"additionalProperties":true,"properties":{"as":{"type":"string","description":"The name of this reading.","example":"owner"},"who":{"type":"object","additionalProperties":true,"description":"Who receives this reading: `{ \"user\": \"${{ context.owner_id }}\" }` for a user from the context, or `{ \"can\": \"approval_request:approve\" }` for everyone who holds that action on the resource. Without `who`, only the people who chose **Everything** receive it.","example":{"user":"${{ context.owner_id }}"}},"action":{"type":"string","description":"The rule's ID, or an ID that starts with it.","example":"acme.security_finding"},"content":{"type":"object","required":["title"],"properties":{"title":{"type":"string","description":"The notification's title. Accepts templates like `${{ context.service }}` and inline Markdown.","example":"Security finding on ${{ context.service }}"},"body":{"type":"string","description":"The notification's body. Accepts templates, inline Markdown, links, and lists.","example":"${{ context.summary }}"}}},"fallback":{"type":"object","additionalProperties":true,"description":"Another reading, with the same fields, for when `who` resolves nobody."}}}},"visible_to":{"type":"array","maxItems":1,"items":{"type":"string"},"description":"The one NRN the rule applies under. Defaults to your organization. Sends for resources outside it are refused.","example":["organization=1:account=2"]}}}}}}} > ℹ️ **Note:** It can't start with `platform.`. Using a built-in rule's ID edits that rule for your organization.\n","example":"acme.security_finding"},"status":{"type":"string","enum":["active","deprecated"],"description":"Whether the rule is active. Defaults to `active`. `deprecated` means deactivated.","example":"active"},"source":{"description":"What makes the rule fire:\n\n- `{ \"kind\": \"invoked\" }`: your code calls the emission endpoint.\n- A source name, like `\"deployment\"`: the entity changes status in nullplatform. See `GET /communications/inbox_rule/source`.\n- `{ \"kind\": \"stale\", ... }`: an entity stays in a status for too long.\n","oneOf":[{"type":"string"},{"type":"object","additionalProperties":true}],"example":{"kind":"invoked"}},"context_schema":{"type":"object","additionalProperties":true,"description":"A JSON Schema for the data each send carries. An invoked rule must require `id`, because sends are idempotent by it.","example":{"type":"object","required":["id","service","owner_id"],"properties":{"id":{"type":"string"},"service":{"type":"string"},"summary":{"type":"string"},"owner_id":{"type":"integer"}}}},"category":{"type":"object","description":"How the rule shows up in inbox settings. All six fields are required.","required":["group","label","description","configurable","allows_all","default_mode"],"properties":{"group":{"type":"string","description":"The group the rule is listed under in inbox settings.","example":"security"},"label":{"type":"string","description":"The rule's name in inbox settings.","example":"Security findings"},"description":{"type":"string","maxLength":70,"description":"One line on what the rule announces.","example":"Vulnerabilities your CI scanners report."},"configurable":{"type":"boolean","description":"Whether people can change their mode for this rule.","example":true},"allows_all":{"type":"boolean","description":"Whether people can choose **Everything** for this rule under a resource. Requires `configurable`.","example":true},"default_mode":{"type":"string","enum":["off","involved","all"],"description":"What people get until they change it.","example":"involved"}}},"severity":{"type":"string","enum":["informational","maintenance","degraded","outage"],"description":"Sets the icon and the color of the notification.","example":"degraded"},"audience":{"type":"array","description":"One entry per reading. Each entry is a separate notification with its own wording.","items":{"type":"object","required":["as","action","content"],"additionalProperties":true,"properties":{"as":{"type":"string","description":"The name of this reading.","example":"owner"},"who":{"type":"object","additionalProperties":true,"description":"Who receives this reading: `{ \"user\": \"${{ context.owner_id }}\" }` for a user from the context, or `{ \"can\": \"approval_request:approve\" }` for everyone who holds that action on the resource. Without `who`, only the people who chose **Everything** receive it.","example":{"user":"${{ context.owner_id }}"}},"action":{"type":"string","description":"The rule's ID, or an ID that starts with it.","example":"acme.security_finding"},"content":{"type":"object","required":["title"],"properties":{"title":{"type":"string","description":"The notification's title. Accepts templates like `${{ context.service }}` and inline Markdown.","example":"Security finding on ${{ context.service }}"},"body":{"type":"string","description":"The notification's body. Accepts templates, inline Markdown, links, and lists.","example":"${{ context.summary }}"}}},"fallback":{"type":"object","additionalProperties":true,"description":"Another reading, with the same fields, for when `who` resolves nobody."}}}},"visible_to":{"type":"array","maxItems":1,"items":{"type":"string"},"description":"The one NRN the rule applies under. Defaults to your organization. Sends for resources outside it are refused.","example":["organization=1:account=2"]}}},"created_at":{"type":"string","format":"date-time","description":"When the rule was created.","example":"2026-10-20T14:03:11.000Z"},"updated_at":{"type":"string","format":"date-time","description":"When the rule was last updated.","example":"2026-10-20T14:03:11.000Z"},"organization_state":{"title":"OrganizationState","type":"object","description":"Only on built-in rules: how the rule stands in your organization.","properties":{"enabled":{"type":"boolean","description":"Whether the rule is on for your organization.","example":true},"customized":{"type":"boolean","description":"Whether your organization edited the rule.","example":false},"linked":{"type":"array","items":{"type":"string"},"description":"Built-in rules that turn on and off together with this one.","example":[]},"replaced_by":{"type":"string","nullable":true,"description":"The ID of your rule that takes this one's place, if any.","example":null}}}}}}}},"400":{"description":"The definition, the context, or the NRN is invalid. Each entry in `errors` points to one problem.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","description":"A summary of every problem found.","example":"invalid notification type: /category/description must be at most 70 characters"},"errors":{"type":"array","description":"One entry per problem.","items":{"type":"object","properties":{"path":{"type":"string","description":"A JSON pointer to the invalid field in the request body.","example":"/category/description"},"message":{"type":"string","description":"What's wrong with that field.","example":"must be at most 70 characters"}}}}}}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## Reset a built-in inbox rule to default URL: https://docs.nullplatform.com/docs/api/inbox-rule-customization-delete Discards your organization's edits to a built-in inbox rule, so it runs again as nullplatform ships it. It's the same as **Reset to default** in the UI. Answers `404` if your organization didn't edit the rule. ℹ️ **Note:** It can't start with `platform.`. Using a built-in rule's ID edits that rule for your organization.\n","example":"acme.security_finding"},"status":{"type":"string","enum":["active","deprecated"],"description":"Whether the rule is active. Defaults to `active`. `deprecated` means deactivated.","example":"active"},"source":{"description":"What makes the rule fire:\n\n- `{ \"kind\": \"invoked\" }`: your code calls the emission endpoint.\n- A source name, like `\"deployment\"`: the entity changes status in nullplatform. See `GET /communications/inbox_rule/source`.\n- `{ \"kind\": \"stale\", ... }`: an entity stays in a status for too long.\n","oneOf":[{"type":"string"},{"type":"object","additionalProperties":true}],"example":{"kind":"invoked"}},"context_schema":{"type":"object","additionalProperties":true,"description":"A JSON Schema for the data each send carries. An invoked rule must require `id`, because sends are idempotent by it.","example":{"type":"object","required":["id","service","owner_id"],"properties":{"id":{"type":"string"},"service":{"type":"string"},"summary":{"type":"string"},"owner_id":{"type":"integer"}}}},"category":{"type":"object","description":"How the rule shows up in inbox settings. All six fields are required.","required":["group","label","description","configurable","allows_all","default_mode"],"properties":{"group":{"type":"string","description":"The group the rule is listed under in inbox settings.","example":"security"},"label":{"type":"string","description":"The rule's name in inbox settings.","example":"Security findings"},"description":{"type":"string","maxLength":70,"description":"One line on what the rule announces.","example":"Vulnerabilities your CI scanners report."},"configurable":{"type":"boolean","description":"Whether people can change their mode for this rule.","example":true},"allows_all":{"type":"boolean","description":"Whether people can choose **Everything** for this rule under a resource. Requires `configurable`.","example":true},"default_mode":{"type":"string","enum":["off","involved","all"],"description":"What people get until they change it.","example":"involved"}}},"severity":{"type":"string","enum":["informational","maintenance","degraded","outage"],"description":"Sets the icon and the color of the notification.","example":"degraded"},"audience":{"type":"array","description":"One entry per reading. Each entry is a separate notification with its own wording.","items":{"type":"object","required":["as","action","content"],"additionalProperties":true,"properties":{"as":{"type":"string","description":"The name of this reading.","example":"owner"},"who":{"type":"object","additionalProperties":true,"description":"Who receives this reading: `{ \"user\": \"${{ context.owner_id }}\" }` for a user from the context, or `{ \"can\": \"approval_request:approve\" }` for everyone who holds that action on the resource. Without `who`, only the people who chose **Everything** receive it.","example":{"user":"${{ context.owner_id }}"}},"action":{"type":"string","description":"The rule's ID, or an ID that starts with it.","example":"acme.security_finding"},"content":{"type":"object","required":["title"],"properties":{"title":{"type":"string","description":"The notification's title. Accepts templates like `${{ context.service }}` and inline Markdown.","example":"Security finding on ${{ context.service }}"},"body":{"type":"string","description":"The notification's body. Accepts templates, inline Markdown, links, and lists.","example":"${{ context.summary }}"}}},"fallback":{"type":"object","additionalProperties":true,"description":"Another reading, with the same fields, for when `who` resolves nobody."}}}},"visible_to":{"type":"array","maxItems":1,"items":{"type":"string"},"description":"The one NRN the rule applies under. Defaults to your organization. Sends for resources outside it are refused.","example":["organization=1:account=2"]}}},"created_at":{"type":"string","format":"date-time","description":"When the rule was created.","example":"2026-10-20T14:03:11.000Z"},"updated_at":{"type":"string","format":"date-time","description":"When the rule was last updated.","example":"2026-10-20T14:03:11.000Z"},"organization_state":{"title":"OrganizationState","type":"object","description":"Only on built-in rules: how the rule stands in your organization.","properties":{"enabled":{"type":"boolean","description":"Whether the rule is on for your organization.","example":true},"customized":{"type":"boolean","description":"Whether your organization edited the rule.","example":false},"linked":{"type":"array","items":{"type":"string"},"description":"Built-in rules that turn on and off together with this one.","example":[]},"replaced_by":{"type":"string","nullable":true,"description":"The ID of your rule that takes this one's place, if any.","example":null}}}}}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## Deactivate an inbox rule URL: https://docs.nullplatform.com/docs/api/inbox-rule-deactivate Deactivates one of your organization's inbox rules: it stops sending right away and frees one of your slots. Notifications it already sent stay in people's inboxes. To activate it again, update it with `"status": "active"`. To delete it for good, use `DELETE /communications/inbox_rule/{id}/permanent` once it's deactivated. Built-in rules and the built-in rules your organization edited can't be deactivated this way. Turn a built-in rule off with `PUT /communications/inbox_rule/{id}/organization_state`, or discard your edits with `DELETE /communications/inbox_rule/{id}/customization`. ℹ️ **Note:** It can't start with `platform.`. Using a built-in rule's ID edits that rule for your organization.\n","example":"acme.security_finding"},"status":{"type":"string","enum":["active","deprecated"],"description":"Whether the rule is active. Defaults to `active`. `deprecated` means deactivated.","example":"active"},"source":{"description":"What makes the rule fire:\n\n- `{ \"kind\": \"invoked\" }`: your code calls the emission endpoint.\n- A source name, like `\"deployment\"`: the entity changes status in nullplatform. See `GET /communications/inbox_rule/source`.\n- `{ \"kind\": \"stale\", ... }`: an entity stays in a status for too long.\n","oneOf":[{"type":"string"},{"type":"object","additionalProperties":true}],"example":{"kind":"invoked"}},"context_schema":{"type":"object","additionalProperties":true,"description":"A JSON Schema for the data each send carries. An invoked rule must require `id`, because sends are idempotent by it.","example":{"type":"object","required":["id","service","owner_id"],"properties":{"id":{"type":"string"},"service":{"type":"string"},"summary":{"type":"string"},"owner_id":{"type":"integer"}}}},"category":{"type":"object","description":"How the rule shows up in inbox settings. All six fields are required.","required":["group","label","description","configurable","allows_all","default_mode"],"properties":{"group":{"type":"string","description":"The group the rule is listed under in inbox settings.","example":"security"},"label":{"type":"string","description":"The rule's name in inbox settings.","example":"Security findings"},"description":{"type":"string","maxLength":70,"description":"One line on what the rule announces.","example":"Vulnerabilities your CI scanners report."},"configurable":{"type":"boolean","description":"Whether people can change their mode for this rule.","example":true},"allows_all":{"type":"boolean","description":"Whether people can choose **Everything** for this rule under a resource. Requires `configurable`.","example":true},"default_mode":{"type":"string","enum":["off","involved","all"],"description":"What people get until they change it.","example":"involved"}}},"severity":{"type":"string","enum":["informational","maintenance","degraded","outage"],"description":"Sets the icon and the color of the notification.","example":"degraded"},"audience":{"type":"array","description":"One entry per reading. Each entry is a separate notification with its own wording.","items":{"type":"object","required":["as","action","content"],"additionalProperties":true,"properties":{"as":{"type":"string","description":"The name of this reading.","example":"owner"},"who":{"type":"object","additionalProperties":true,"description":"Who receives this reading: `{ \"user\": \"${{ context.owner_id }}\" }` for a user from the context, or `{ \"can\": \"approval_request:approve\" }` for everyone who holds that action on the resource. Without `who`, only the people who chose **Everything** receive it.","example":{"user":"${{ context.owner_id }}"}},"action":{"type":"string","description":"The rule's ID, or an ID that starts with it.","example":"acme.security_finding"},"content":{"type":"object","required":["title"],"properties":{"title":{"type":"string","description":"The notification's title. Accepts templates like `${{ context.service }}` and inline Markdown.","example":"Security finding on ${{ context.service }}"},"body":{"type":"string","description":"The notification's body. Accepts templates, inline Markdown, links, and lists.","example":"${{ context.summary }}"}}},"fallback":{"type":"object","additionalProperties":true,"description":"Another reading, with the same fields, for when `who` resolves nobody."}}}},"visible_to":{"type":"array","maxItems":1,"items":{"type":"string"},"description":"The one NRN the rule applies under. Defaults to your organization. Sends for resources outside it are refused.","example":["organization=1:account=2"]}}},"created_at":{"type":"string","format":"date-time","description":"When the rule was created.","example":"2026-10-20T14:03:11.000Z"},"updated_at":{"type":"string","format":"date-time","description":"When the rule was last updated.","example":"2026-10-20T14:03:11.000Z"},"organization_state":{"title":"OrganizationState","type":"object","description":"Only on built-in rules: how the rule stands in your organization.","properties":{"enabled":{"type":"boolean","description":"Whether the rule is on for your organization.","example":true},"customized":{"type":"boolean","description":"Whether your organization edited the rule.","example":false},"linked":{"type":"array","items":{"type":"string"},"description":"Built-in rules that turn on and off together with this one.","example":[]},"replaced_by":{"type":"string","nullable":true,"description":"The ID of your rule that takes this one's place, if any.","example":null}}}}}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## Delete an inbox rule permanently URL: https://docs.nullplatform.com/docs/api/inbox-rule-delete-permanently Deletes one of your organization's inbox rules for good, together with everyone's inbox settings for it. This can't be undone. Deactivate the rule first: an active rule answers an error. Notifications it already sent stay in people's inboxes. To discard your edits to a built-in rule, reset it to default instead. ℹ️ **Note:** It can't start with `platform.`. Using a built-in rule's ID edits that rule for your organization.\n","example":"acme.security_finding"},"status":{"type":"string","enum":["active","deprecated"],"description":"Whether the rule is active. Defaults to `active`. `deprecated` means deactivated.","example":"active"},"source":{"description":"What makes the rule fire:\n\n- `{ \"kind\": \"invoked\" }`: your code calls the emission endpoint.\n- A source name, like `\"deployment\"`: the entity changes status in nullplatform. See `GET /communications/inbox_rule/source`.\n- `{ \"kind\": \"stale\", ... }`: an entity stays in a status for too long.\n","oneOf":[{"type":"string"},{"type":"object","additionalProperties":true}],"example":{"kind":"invoked"}},"context_schema":{"type":"object","additionalProperties":true,"description":"A JSON Schema for the data each send carries. An invoked rule must require `id`, because sends are idempotent by it.","example":{"type":"object","required":["id","service","owner_id"],"properties":{"id":{"type":"string"},"service":{"type":"string"},"summary":{"type":"string"},"owner_id":{"type":"integer"}}}},"category":{"type":"object","description":"How the rule shows up in inbox settings. All six fields are required.","required":["group","label","description","configurable","allows_all","default_mode"],"properties":{"group":{"type":"string","description":"The group the rule is listed under in inbox settings.","example":"security"},"label":{"type":"string","description":"The rule's name in inbox settings.","example":"Security findings"},"description":{"type":"string","maxLength":70,"description":"One line on what the rule announces.","example":"Vulnerabilities your CI scanners report."},"configurable":{"type":"boolean","description":"Whether people can change their mode for this rule.","example":true},"allows_all":{"type":"boolean","description":"Whether people can choose **Everything** for this rule under a resource. Requires `configurable`.","example":true},"default_mode":{"type":"string","enum":["off","involved","all"],"description":"What people get until they change it.","example":"involved"}}},"severity":{"type":"string","enum":["informational","maintenance","degraded","outage"],"description":"Sets the icon and the color of the notification.","example":"degraded"},"audience":{"type":"array","description":"One entry per reading. Each entry is a separate notification with its own wording.","items":{"type":"object","required":["as","action","content"],"additionalProperties":true,"properties":{"as":{"type":"string","description":"The name of this reading.","example":"owner"},"who":{"type":"object","additionalProperties":true,"description":"Who receives this reading: `{ \"user\": \"${{ context.owner_id }}\" }` for a user from the context, or `{ \"can\": \"approval_request:approve\" }` for everyone who holds that action on the resource. Without `who`, only the people who chose **Everything** receive it.","example":{"user":"${{ context.owner_id }}"}},"action":{"type":"string","description":"The rule's ID, or an ID that starts with it.","example":"acme.security_finding"},"content":{"type":"object","required":["title"],"properties":{"title":{"type":"string","description":"The notification's title. Accepts templates like `${{ context.service }}` and inline Markdown.","example":"Security finding on ${{ context.service }}"},"body":{"type":"string","description":"The notification's body. Accepts templates, inline Markdown, links, and lists.","example":"${{ context.summary }}"}}},"fallback":{"type":"object","additionalProperties":true,"description":"Another reading, with the same fields, for when `who` resolves nobody."}}}},"visible_to":{"type":"array","maxItems":1,"items":{"type":"string"},"description":"The one NRN the rule applies under. Defaults to your organization. Sends for resources outside it are refused.","example":["organization=1:account=2"]}}},"created_at":{"type":"string","format":"date-time","description":"When the rule was created.","example":"2026-10-20T14:03:11.000Z"},"updated_at":{"type":"string","format":"date-time","description":"When the rule was last updated.","example":"2026-10-20T14:03:11.000Z"},"organization_state":{"title":"OrganizationState","type":"object","description":"Only on built-in rules: how the rule stands in your organization.","properties":{"enabled":{"type":"boolean","description":"Whether the rule is on for your organization.","example":true},"customized":{"type":"boolean","description":"Whether your organization edited the rule.","example":false},"linked":{"type":"array","items":{"type":"string"},"description":"Built-in rules that turn on and off together with this one.","example":[]},"replaced_by":{"type":"string","nullable":true,"description":"The ID of your rule that takes this one's place, if any.","example":null}}}}}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## Send a notification from an inbox rule URL: https://docs.nullplatform.com/docs/api/inbox-rule-emit Sends a notification from one of your **invoked** inbox rules: the resource the event is about (`nrn`) and the data the rule's templates read (`context`). Sends are idempotent by `context.id`. Sending the same `id` for the same `nrn` again answers `200`, marks each entry in `emitted` with `existing: true`, and creates nothing. Sending it for a different `nrn` answers `409`. Recipients who can't see the resource are left out and counted in `rejected`, and people who turned the rule off for that resource don't receive it. --- ## List inbox rules URL: https://docs.nullplatform.com/docs/api/inbox-rule-list Lists the built-in inbox rules and your organization's own, active and deactivated. Built-in rules also carry `organization_state`: whether the rule is on for your organization and whether you edited it. ℹ️ **Note:** It can't start with `platform.`. Using a built-in rule's ID edits that rule for your organization.\n","example":"acme.security_finding"},"status":{"type":"string","enum":["active","deprecated"],"description":"Whether the rule is active. Defaults to `active`. `deprecated` means deactivated.","example":"active"},"source":{"description":"What makes the rule fire:\n\n- `{ \"kind\": \"invoked\" }`: your code calls the emission endpoint.\n- A source name, like `\"deployment\"`: the entity changes status in nullplatform. See `GET /communications/inbox_rule/source`.\n- `{ \"kind\": \"stale\", ... }`: an entity stays in a status for too long.\n","oneOf":[{"type":"string"},{"type":"object","additionalProperties":true}],"example":{"kind":"invoked"}},"context_schema":{"type":"object","additionalProperties":true,"description":"A JSON Schema for the data each send carries. An invoked rule must require `id`, because sends are idempotent by it.","example":{"type":"object","required":["id","service","owner_id"],"properties":{"id":{"type":"string"},"service":{"type":"string"},"summary":{"type":"string"},"owner_id":{"type":"integer"}}}},"category":{"type":"object","description":"How the rule shows up in inbox settings. All six fields are required.","required":["group","label","description","configurable","allows_all","default_mode"],"properties":{"group":{"type":"string","description":"The group the rule is listed under in inbox settings.","example":"security"},"label":{"type":"string","description":"The rule's name in inbox settings.","example":"Security findings"},"description":{"type":"string","maxLength":70,"description":"One line on what the rule announces.","example":"Vulnerabilities your CI scanners report."},"configurable":{"type":"boolean","description":"Whether people can change their mode for this rule.","example":true},"allows_all":{"type":"boolean","description":"Whether people can choose **Everything** for this rule under a resource. Requires `configurable`.","example":true},"default_mode":{"type":"string","enum":["off","involved","all"],"description":"What people get until they change it.","example":"involved"}}},"severity":{"type":"string","enum":["informational","maintenance","degraded","outage"],"description":"Sets the icon and the color of the notification.","example":"degraded"},"audience":{"type":"array","description":"One entry per reading. Each entry is a separate notification with its own wording.","items":{"type":"object","required":["as","action","content"],"additionalProperties":true,"properties":{"as":{"type":"string","description":"The name of this reading.","example":"owner"},"who":{"type":"object","additionalProperties":true,"description":"Who receives this reading: `{ \"user\": \"${{ context.owner_id }}\" }` for a user from the context, or `{ \"can\": \"approval_request:approve\" }` for everyone who holds that action on the resource. Without `who`, only the people who chose **Everything** receive it.","example":{"user":"${{ context.owner_id }}"}},"action":{"type":"string","description":"The rule's ID, or an ID that starts with it.","example":"acme.security_finding"},"content":{"type":"object","required":["title"],"properties":{"title":{"type":"string","description":"The notification's title. Accepts templates like `${{ context.service }}` and inline Markdown.","example":"Security finding on ${{ context.service }}"},"body":{"type":"string","description":"The notification's body. Accepts templates, inline Markdown, links, and lists.","example":"${{ context.summary }}"}}},"fallback":{"type":"object","additionalProperties":true,"description":"Another reading, with the same fields, for when `who` resolves nobody."}}}},"visible_to":{"type":"array","maxItems":1,"items":{"type":"string"},"description":"The one NRN the rule applies under. Defaults to your organization. Sends for resources outside it are refused.","example":["organization=1:account=2"]}}},"created_at":{"type":"string","format":"date-time","description":"When the rule was created.","example":"2026-10-20T14:03:11.000Z"},"updated_at":{"type":"string","format":"date-time","description":"When the rule was last updated.","example":"2026-10-20T14:03:11.000Z"},"organization_state":{"title":"OrganizationState","type":"object","description":"Only on built-in rules: how the rule stands in your organization.","properties":{"enabled":{"type":"boolean","description":"Whether the rule is on for your organization.","example":true},"customized":{"type":"boolean","description":"Whether your organization edited the rule.","example":false},"linked":{"type":"array","items":{"type":"string"},"description":"Built-in rules that turn on and off together with this one.","example":[]},"replaced_by":{"type":"string","nullable":true,"description":"The ID of your rule that takes this one's place, if any.","example":null}}}}}}}}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## Read an inbox rule URL: https://docs.nullplatform.com/docs/api/inbox-rule-read Returns one inbox rule. For a built-in rule your organization edited, it returns your edited version. ℹ️ **Note:** It can't start with `platform.`. Using a built-in rule's ID edits that rule for your organization.\n","example":"acme.security_finding"},"status":{"type":"string","enum":["active","deprecated"],"description":"Whether the rule is active. Defaults to `active`. `deprecated` means deactivated.","example":"active"},"source":{"description":"What makes the rule fire:\n\n- `{ \"kind\": \"invoked\" }`: your code calls the emission endpoint.\n- A source name, like `\"deployment\"`: the entity changes status in nullplatform. See `GET /communications/inbox_rule/source`.\n- `{ \"kind\": \"stale\", ... }`: an entity stays in a status for too long.\n","oneOf":[{"type":"string"},{"type":"object","additionalProperties":true}],"example":{"kind":"invoked"}},"context_schema":{"type":"object","additionalProperties":true,"description":"A JSON Schema for the data each send carries. An invoked rule must require `id`, because sends are idempotent by it.","example":{"type":"object","required":["id","service","owner_id"],"properties":{"id":{"type":"string"},"service":{"type":"string"},"summary":{"type":"string"},"owner_id":{"type":"integer"}}}},"category":{"type":"object","description":"How the rule shows up in inbox settings. All six fields are required.","required":["group","label","description","configurable","allows_all","default_mode"],"properties":{"group":{"type":"string","description":"The group the rule is listed under in inbox settings.","example":"security"},"label":{"type":"string","description":"The rule's name in inbox settings.","example":"Security findings"},"description":{"type":"string","maxLength":70,"description":"One line on what the rule announces.","example":"Vulnerabilities your CI scanners report."},"configurable":{"type":"boolean","description":"Whether people can change their mode for this rule.","example":true},"allows_all":{"type":"boolean","description":"Whether people can choose **Everything** for this rule under a resource. Requires `configurable`.","example":true},"default_mode":{"type":"string","enum":["off","involved","all"],"description":"What people get until they change it.","example":"involved"}}},"severity":{"type":"string","enum":["informational","maintenance","degraded","outage"],"description":"Sets the icon and the color of the notification.","example":"degraded"},"audience":{"type":"array","description":"One entry per reading. Each entry is a separate notification with its own wording.","items":{"type":"object","required":["as","action","content"],"additionalProperties":true,"properties":{"as":{"type":"string","description":"The name of this reading.","example":"owner"},"who":{"type":"object","additionalProperties":true,"description":"Who receives this reading: `{ \"user\": \"${{ context.owner_id }}\" }` for a user from the context, or `{ \"can\": \"approval_request:approve\" }` for everyone who holds that action on the resource. Without `who`, only the people who chose **Everything** receive it.","example":{"user":"${{ context.owner_id }}"}},"action":{"type":"string","description":"The rule's ID, or an ID that starts with it.","example":"acme.security_finding"},"content":{"type":"object","required":["title"],"properties":{"title":{"type":"string","description":"The notification's title. Accepts templates like `${{ context.service }}` and inline Markdown.","example":"Security finding on ${{ context.service }}"},"body":{"type":"string","description":"The notification's body. Accepts templates, inline Markdown, links, and lists.","example":"${{ context.summary }}"}}},"fallback":{"type":"object","additionalProperties":true,"description":"Another reading, with the same fields, for when `who` resolves nobody."}}}},"visible_to":{"type":"array","maxItems":1,"items":{"type":"string"},"description":"The one NRN the rule applies under. Defaults to your organization. Sends for resources outside it are refused.","example":["organization=1:account=2"]}}},"created_at":{"type":"string","format":"date-time","description":"When the rule was created.","example":"2026-10-20T14:03:11.000Z"},"updated_at":{"type":"string","format":"date-time","description":"When the rule was last updated.","example":"2026-10-20T14:03:11.000Z"},"organization_state":{"title":"OrganizationState","type":"object","description":"Only on built-in rules: how the rule stands in your organization.","properties":{"enabled":{"type":"boolean","description":"Whether the rule is on for your organization.","example":true},"customized":{"type":"boolean","description":"Whether your organization edited the rule.","example":false},"linked":{"type":"array","items":{"type":"string"},"description":"Built-in rules that turn on and off together with this one.","example":[]},"replaced_by":{"type":"string","nullable":true,"description":"The ID of your rule that takes this one's place, if any.","example":null}}}}}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## Read the inbox rule schema URL: https://docs.nullplatform.com/docs/api/inbox-rule-schema Returns the JSON Schema of an inbox rule definition, with every field documented. Use it to validate definitions or to power completion in your editor. --- ## List inbox rule sources URL: https://docs.nullplatform.com/docs/api/inbox-rule-source-list Lists the source names an inbox rule can use, with the statuses and the fields each one exposes to templates and conditions. --- ## Turn a built-in inbox rule on or off URL: https://docs.nullplatform.com/docs/api/inbox-rule-state-update Turns a built-in inbox rule on or off for your organization, together with the built-in rules linked to it. A built-in rule that one of your active rules replaces can't be turned on: deactivate your rule first. To turn off one of your own rules, deactivate it instead. --- ## Update an inbox rule URL: https://docs.nullplatform.com/docs/api/inbox-rule-update Replaces the definition of one of your organization's inbox rules. Send the whole definition: fields you leave out are removed. The `id` in the body must match the one in the path. Editing a rule never changes notifications that were already sent. ℹ️ **Note:** It can't start with `platform.`. Using a built-in rule's ID edits that rule for your organization.\n","example":"acme.security_finding"},"status":{"type":"string","enum":["active","deprecated"],"description":"Whether the rule is active. Defaults to `active`. `deprecated` means deactivated.","example":"active"},"source":{"description":"What makes the rule fire:\n\n- `{ \"kind\": \"invoked\" }`: your code calls the emission endpoint.\n- A source name, like `\"deployment\"`: the entity changes status in nullplatform. See `GET /communications/inbox_rule/source`.\n- `{ \"kind\": \"stale\", ... }`: an entity stays in a status for too long.\n","oneOf":[{"type":"string"},{"type":"object","additionalProperties":true}],"example":{"kind":"invoked"}},"context_schema":{"type":"object","additionalProperties":true,"description":"A JSON Schema for the data each send carries. An invoked rule must require `id`, because sends are idempotent by it.","example":{"type":"object","required":["id","service","owner_id"],"properties":{"id":{"type":"string"},"service":{"type":"string"},"summary":{"type":"string"},"owner_id":{"type":"integer"}}}},"category":{"type":"object","description":"How the rule shows up in inbox settings. All six fields are required.","required":["group","label","description","configurable","allows_all","default_mode"],"properties":{"group":{"type":"string","description":"The group the rule is listed under in inbox settings.","example":"security"},"label":{"type":"string","description":"The rule's name in inbox settings.","example":"Security findings"},"description":{"type":"string","maxLength":70,"description":"One line on what the rule announces.","example":"Vulnerabilities your CI scanners report."},"configurable":{"type":"boolean","description":"Whether people can change their mode for this rule.","example":true},"allows_all":{"type":"boolean","description":"Whether people can choose **Everything** for this rule under a resource. Requires `configurable`.","example":true},"default_mode":{"type":"string","enum":["off","involved","all"],"description":"What people get until they change it.","example":"involved"}}},"severity":{"type":"string","enum":["informational","maintenance","degraded","outage"],"description":"Sets the icon and the color of the notification.","example":"degraded"},"audience":{"type":"array","description":"One entry per reading. Each entry is a separate notification with its own wording.","items":{"type":"object","required":["as","action","content"],"additionalProperties":true,"properties":{"as":{"type":"string","description":"The name of this reading.","example":"owner"},"who":{"type":"object","additionalProperties":true,"description":"Who receives this reading: `{ \"user\": \"${{ context.owner_id }}\" }` for a user from the context, or `{ \"can\": \"approval_request:approve\" }` for everyone who holds that action on the resource. Without `who`, only the people who chose **Everything** receive it.","example":{"user":"${{ context.owner_id }}"}},"action":{"type":"string","description":"The rule's ID, or an ID that starts with it.","example":"acme.security_finding"},"content":{"type":"object","required":["title"],"properties":{"title":{"type":"string","description":"The notification's title. Accepts templates like `${{ context.service }}` and inline Markdown.","example":"Security finding on ${{ context.service }}"},"body":{"type":"string","description":"The notification's body. Accepts templates, inline Markdown, links, and lists.","example":"${{ context.summary }}"}}},"fallback":{"type":"object","additionalProperties":true,"description":"Another reading, with the same fields, for when `who` resolves nobody."}}}},"visible_to":{"type":"array","maxItems":1,"items":{"type":"string"},"description":"The one NRN the rule applies under. Defaults to your organization. Sends for resources outside it are refused.","example":["organization=1:account=2"]}}}}}}} > ℹ️ **Note:** It can't start with `platform.`. Using a built-in rule's ID edits that rule for your organization.\n","example":"acme.security_finding"},"status":{"type":"string","enum":["active","deprecated"],"description":"Whether the rule is active. Defaults to `active`. `deprecated` means deactivated.","example":"active"},"source":{"description":"What makes the rule fire:\n\n- `{ \"kind\": \"invoked\" }`: your code calls the emission endpoint.\n- A source name, like `\"deployment\"`: the entity changes status in nullplatform. See `GET /communications/inbox_rule/source`.\n- `{ \"kind\": \"stale\", ... }`: an entity stays in a status for too long.\n","oneOf":[{"type":"string"},{"type":"object","additionalProperties":true}],"example":{"kind":"invoked"}},"context_schema":{"type":"object","additionalProperties":true,"description":"A JSON Schema for the data each send carries. An invoked rule must require `id`, because sends are idempotent by it.","example":{"type":"object","required":["id","service","owner_id"],"properties":{"id":{"type":"string"},"service":{"type":"string"},"summary":{"type":"string"},"owner_id":{"type":"integer"}}}},"category":{"type":"object","description":"How the rule shows up in inbox settings. All six fields are required.","required":["group","label","description","configurable","allows_all","default_mode"],"properties":{"group":{"type":"string","description":"The group the rule is listed under in inbox settings.","example":"security"},"label":{"type":"string","description":"The rule's name in inbox settings.","example":"Security findings"},"description":{"type":"string","maxLength":70,"description":"One line on what the rule announces.","example":"Vulnerabilities your CI scanners report."},"configurable":{"type":"boolean","description":"Whether people can change their mode for this rule.","example":true},"allows_all":{"type":"boolean","description":"Whether people can choose **Everything** for this rule under a resource. Requires `configurable`.","example":true},"default_mode":{"type":"string","enum":["off","involved","all"],"description":"What people get until they change it.","example":"involved"}}},"severity":{"type":"string","enum":["informational","maintenance","degraded","outage"],"description":"Sets the icon and the color of the notification.","example":"degraded"},"audience":{"type":"array","description":"One entry per reading. Each entry is a separate notification with its own wording.","items":{"type":"object","required":["as","action","content"],"additionalProperties":true,"properties":{"as":{"type":"string","description":"The name of this reading.","example":"owner"},"who":{"type":"object","additionalProperties":true,"description":"Who receives this reading: `{ \"user\": \"${{ context.owner_id }}\" }` for a user from the context, or `{ \"can\": \"approval_request:approve\" }` for everyone who holds that action on the resource. Without `who`, only the people who chose **Everything** receive it.","example":{"user":"${{ context.owner_id }}"}},"action":{"type":"string","description":"The rule's ID, or an ID that starts with it.","example":"acme.security_finding"},"content":{"type":"object","required":["title"],"properties":{"title":{"type":"string","description":"The notification's title. Accepts templates like `${{ context.service }}` and inline Markdown.","example":"Security finding on ${{ context.service }}"},"body":{"type":"string","description":"The notification's body. Accepts templates, inline Markdown, links, and lists.","example":"${{ context.summary }}"}}},"fallback":{"type":"object","additionalProperties":true,"description":"Another reading, with the same fields, for when `who` resolves nobody."}}}},"visible_to":{"type":"array","maxItems":1,"items":{"type":"string"},"description":"The one NRN the rule applies under. Defaults to your organization. Sends for resources outside it are refused.","example":["organization=1:account=2"]}}},"created_at":{"type":"string","format":"date-time","description":"When the rule was created.","example":"2026-10-20T14:03:11.000Z"},"updated_at":{"type":"string","format":"date-time","description":"When the rule was last updated.","example":"2026-10-20T14:03:11.000Z"},"organization_state":{"title":"OrganizationState","type":"object","description":"Only on built-in rules: how the rule stands in your organization.","properties":{"enabled":{"type":"boolean","description":"Whether the rule is on for your organization.","example":true},"customized":{"type":"boolean","description":"Whether your organization edited the rule.","example":false},"linked":{"type":"array","items":{"type":"string"},"description":"Built-in rules that turn on and off together with this one.","example":[]},"replaced_by":{"type":"string","nullable":true,"description":"The ID of your rule that takes this one's place, if any.","example":null}}}}},{"title":"DryRunResult","type":"object","properties":{"valid":{"type":"boolean","description":"Always `true`: an invalid definition answers `400`.","example":true}}}]}}}},"400":{"description":"The definition, the context, or the NRN is invalid. Each entry in `errors` points to one problem.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","description":"A summary of every problem found.","example":"invalid notification type: /category/description must be at most 70 characters"},"errors":{"type":"array","description":"One entry per problem.","items":{"type":"object","properties":{"path":{"type":"string","description":"A JSON pointer to the invalid field in the request body.","example":"/category/description"},"message":{"type":"string","description":"What's wrong with that field.","example":"must be at most 70 characters"}}}}}}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## Validate an inbox rule URL: https://docs.nullplatform.com/docs/api/inbox-rule-validate Runs every check that creating (`mode: new`) or updating (`mode: edit`) an inbox rule runs, limits included, and saves nothing. Unlike `dry_run=true` on create and update, an invalid definition isn't an error: it answers `200` with `valid: false` and every problem found. ℹ️ **Note:** It can't start with `platform.`. Using a built-in rule's ID edits that rule for your organization.\n","example":"acme.security_finding"},"status":{"type":"string","enum":["active","deprecated"],"description":"Whether the rule is active. Defaults to `active`. `deprecated` means deactivated.","example":"active"},"source":{"description":"What makes the rule fire:\n\n- `{ \"kind\": \"invoked\" }`: your code calls the emission endpoint.\n- A source name, like `\"deployment\"`: the entity changes status in nullplatform. See `GET /communications/inbox_rule/source`.\n- `{ \"kind\": \"stale\", ... }`: an entity stays in a status for too long.\n","oneOf":[{"type":"string"},{"type":"object","additionalProperties":true}],"example":{"kind":"invoked"}},"context_schema":{"type":"object","additionalProperties":true,"description":"A JSON Schema for the data each send carries. An invoked rule must require `id`, because sends are idempotent by it.","example":{"type":"object","required":["id","service","owner_id"],"properties":{"id":{"type":"string"},"service":{"type":"string"},"summary":{"type":"string"},"owner_id":{"type":"integer"}}}},"category":{"type":"object","description":"How the rule shows up in inbox settings. All six fields are required.","required":["group","label","description","configurable","allows_all","default_mode"],"properties":{"group":{"type":"string","description":"The group the rule is listed under in inbox settings.","example":"security"},"label":{"type":"string","description":"The rule's name in inbox settings.","example":"Security findings"},"description":{"type":"string","maxLength":70,"description":"One line on what the rule announces.","example":"Vulnerabilities your CI scanners report."},"configurable":{"type":"boolean","description":"Whether people can change their mode for this rule.","example":true},"allows_all":{"type":"boolean","description":"Whether people can choose **Everything** for this rule under a resource. Requires `configurable`.","example":true},"default_mode":{"type":"string","enum":["off","involved","all"],"description":"What people get until they change it.","example":"involved"}}},"severity":{"type":"string","enum":["informational","maintenance","degraded","outage"],"description":"Sets the icon and the color of the notification.","example":"degraded"},"audience":{"type":"array","description":"One entry per reading. Each entry is a separate notification with its own wording.","items":{"type":"object","required":["as","action","content"],"additionalProperties":true,"properties":{"as":{"type":"string","description":"The name of this reading.","example":"owner"},"who":{"type":"object","additionalProperties":true,"description":"Who receives this reading: `{ \"user\": \"${{ context.owner_id }}\" }` for a user from the context, or `{ \"can\": \"approval_request:approve\" }` for everyone who holds that action on the resource. Without `who`, only the people who chose **Everything** receive it.","example":{"user":"${{ context.owner_id }}"}},"action":{"type":"string","description":"The rule's ID, or an ID that starts with it.","example":"acme.security_finding"},"content":{"type":"object","required":["title"],"properties":{"title":{"type":"string","description":"The notification's title. Accepts templates like `${{ context.service }}` and inline Markdown.","example":"Security finding on ${{ context.service }}"},"body":{"type":"string","description":"The notification's body. Accepts templates, inline Markdown, links, and lists.","example":"${{ context.summary }}"}}},"fallback":{"type":"object","additionalProperties":true,"description":"Another reading, with the same fields, for when `who` resolves nobody."}}}},"visible_to":{"type":"array","maxItems":1,"items":{"type":"string"},"description":"The one NRN the rule applies under. Defaults to your organization. Sends for resources outside it are refused.","example":["organization=1:account=2"]}}},"mode":{"type":"string","enum":["new","edit"],"default":"new","description":"`new` checks the definition as a create, and `edit` as an update of the rule in `type_id`.","example":"new"},"type_id":{"type":"string","maxLength":120,"description":"With `mode: edit`, the ID of the inbox rule you're updating. It must match `definition.id`.","example":"acme.security_finding"}}}}}}} > --- ## Create a link action URL: https://docs.nullplatform.com/docs/api/link-action-create Create a link action --- ## Delete a link action URL: https://docs.nullplatform.com/docs/api/link-action-delete Delete a link action --- ## List link actions URL: https://docs.nullplatform.com/docs/api/link-action-list List link actions --- ## Read a link action URL: https://docs.nullplatform.com/docs/api/link-action-read Read a link action --- ## Update a link action URL: https://docs.nullplatform.com/docs/api/link-action-update Updates a link action --- ## Create a link URL: https://docs.nullplatform.com/docs/api/link-create Create a link --- ## Delete a link URL: https://docs.nullplatform.com/docs/api/link-delete Delete a link --- ## List links URL: https://docs.nullplatform.com/docs/api/link-list List links **Note:** Requires the **SecOps** role. See [Roles](/docs/authorization/roles) for details.\n"}]} > --- ## Read a link URL: https://docs.nullplatform.com/docs/api/link-read Read a link --- ## Create a link specification action URL: https://docs.nullplatform.com/docs/api/link-specification-action-create Create a link specification action --- ## Delete a link specification action URL: https://docs.nullplatform.com/docs/api/link-specification-action-delete Delete a link specification action --- ## List link specification actions URL: https://docs.nullplatform.com/docs/api/link-specification-action-list List link specification actions --- ## Read a link specification action URL: https://docs.nullplatform.com/docs/api/link-specification-action-read Read a link specification action --- ## Update a link specification action URL: https://docs.nullplatform.com/docs/api/link-specification-action-update Updates a link specification action --- ## List associated link specifications URL: https://docs.nullplatform.com/docs/api/link-specification-associated-list List associated link specifications --- ## Create a link specification URL: https://docs.nullplatform.com/docs/api/link-specification-create Create a link specification --- ## Delete a link specification URL: https://docs.nullplatform.com/docs/api/link-specification-delete Delete a link specification --- ## List link specifications URL: https://docs.nullplatform.com/docs/api/link-specification-list List link specifications --- ## Read a link specification URL: https://docs.nullplatform.com/docs/api/link-specification-read Read a link specification --- ## List snapshots of a link specification URL: https://docs.nullplatform.com/docs/api/link-specification-snapshots-list List the append-only snapshot history of a link specification, newest first. Snapshot ids are what package components pin as resource_revision_id. --- ## Read a snapshot of a link specification URL: https://docs.nullplatform.com/docs/api/link-specification-snapshots-read Read a single snapshot of a link specification, including its full content. --- ## Update a link specification URL: https://docs.nullplatform.com/docs/api/link-specification-update Updates a link specification --- ## Update a link URL: https://docs.nullplatform.com/docs/api/link-update Updates a link --- ## Create a log configuration URL: https://docs.nullplatform.com/docs/api/log-config-create Creates a new log configuration or overwrites an existing one, replacing any previous values. \n> **Note**: If true, it requires the `agent:log_config:create-default` permission; otherwise, it requires `agent:log_config:create`.\n>","enum":[true,false]},"expires_at":{"type":"string","description":"If the log configuration is not default, an expiration date must be specified.","example":"2024-07-12T15:00:00Z"},"account_id":{"type":"integer","description":"Applicable if the log specification is at the account level.","example":2345},"selector":{"type":"object","description":"The tags to be matched to apply the log configuration","example":{"environment":"production"}},"configuration":{"type":"object","description":"The log configuration as defined in the log specification.","example":{"log_level":"debug","no_error_sampling":50}}},"title":"logConfigCreate"}}}}} > \n> **Note**: If true, it requires the `agent:log_config:create-default` permission; otherwise, it requires `agent:log_config:create`.\n>","enum":[true,false]},"expires_at":{"type":"string","description":"If the log configuration is not default, an expiration date must be specified.","example":"2024-07-12T15:00:00Z"},"account_id":{"type":"integer","description":"Applicable if the log specification is at the account level.","example":2345},"selector":{"type":"object","description":"The tags to be matched to apply the log configuration.","example":{"environment":"production"}},"configuration":{"type":"object","description":"The log configuration as defined in the log specification.","example":{"log_level":"debug","no_error_sampling":50}},"created_configuration_defaults":{"type":"array","items":{"type":"object","properties":{"template_id":{"type":"string"},"template_name":{"type":"string"},"capability_category":{"type":"string"},"capability_provider":{"type":"string"},"default_configuration_id":{"type":"string"}},"title":"configurationDefault"}}},"title":"logConfig"}}}}}} > --- ## Delete a log configuration URL: https://docs.nullplatform.com/docs/api/log-config-delete Delete a specific log configuration by its ID. --- ## List log configurations URL: https://docs.nullplatform.com/docs/api/log-config-list List log configurations for a specific organization and account. \n> **Note**: If true, it requires the `agent:log_config:create-default` permission; otherwise, it requires `agent:log_config:create`.\n>","enum":[true,false]},"expires_at":{"type":"string","description":"If the log configuration is not default, an expiration date must be specified.","example":"2024-07-12T15:00:00Z"},"account_id":{"type":"integer","description":"Applicable if the log specification is at the account level.","example":2345},"selector":{"type":"object","description":"The tags to be matched to apply the log configuration.","example":{"environment":"production"}},"configuration":{"type":"object","description":"The log configuration as defined in the log specification.","example":{"log_level":"debug","no_error_sampling":50}},"created_configuration_defaults":{"type":"array","items":{"type":"object","properties":{"template_id":{"type":"string"},"template_name":{"type":"string"},"capability_category":{"type":"string"},"capability_provider":{"type":"string"},"default_configuration_id":{"type":"string"}},"title":"configurationDefault"}}},"title":"logConfig"}}},"title":"logConfigListResponse"}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## Read a log configuration URL: https://docs.nullplatform.com/docs/api/log-config-read Read a specific log configuration. \n> **Note**: If true, it requires the `agent:log_config:create-default` permission; otherwise, it requires `agent:log_config:create`.\n>","enum":[true,false]},"expires_at":{"type":"string","description":"If the log configuration is not default, an expiration date must be specified.","example":"2024-07-12T15:00:00Z"},"account_id":{"type":"integer","description":"Applicable if the log specification is at the account level.","example":2345},"selector":{"type":"object","description":"The tags to be matched to apply the log configuration.","example":{"environment":"production"}},"configuration":{"type":"object","description":"The log configuration as defined in the log specification.","example":{"log_level":"debug","no_error_sampling":50}},"created_configuration_defaults":{"type":"array","items":{"type":"object","properties":{"template_id":{"type":"string"},"template_name":{"type":"string"},"capability_category":{"type":"string"},"capability_provider":{"type":"string"},"default_configuration_id":{"type":"string"}},"title":"configurationDefault"}}},"title":"logConfig"}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## Create log specification URL: https://docs.nullplatform.com/docs/api/log-specification-create Create a new log specification. --- ## Delete log specification URL: https://docs.nullplatform.com/docs/api/log-specification-delete Delete a specific log specification by its ID. --- ## List log specifications URL: https://docs.nullplatform.com/docs/api/log-specification-list List of all log specifications. --- ## Read log specification URL: https://docs.nullplatform.com/docs/api/log-specification-read Retrieve a specific log specification by its ID. --- ## Update log specification URL: https://docs.nullplatform.com/docs/api/log-specification-update Updates an existing log specification by its ID. --- ## Create template URL: https://docs.nullplatform.com/docs/api/log-template-create Create a new template. --- ## Delete template URL: https://docs.nullplatform.com/docs/api/log-template-delete Delete a specific template by its ID. --- ## List templates URL: https://docs.nullplatform.com/docs/api/log-template-list List all templates without their content. --- ## Read a template URL: https://docs.nullplatform.com/docs/api/log-template-read Retrieve a specific template by its ID. --- ## Create a namespace URL: https://docs.nullplatform.com/docs/api/namespace-create Create a new namespace in an existing account. --- ## Delete a namespace URL: https://docs.nullplatform.com/docs/api/namespace-delete Deletes a specific namespace by its ID. This action **cannot be undone**. :::info Before deleting a namespace, ensure that all associated applications and services have been deleted. ::: --- ## List namespaces URL: https://docs.nullplatform.com/docs/api/namespace-list List namespaces within an account. --- ## Read a namespace URL: https://docs.nullplatform.com/docs/api/namespace-read Read a specific namespace by ID. --- ## Update a namespace URL: https://docs.nullplatform.com/docs/api/namespace-update Updates information for a namespace. ℹ️ **Note:** Changing the namespace's name **does not change the slug**, \n as doing so could break connections with existing applications and services.\n"},"status":{"type":"string","enum":["active","inactive"],"example":"active"}},"title":"namespace_write"}}}}} > --- ## Create a channel URL: https://docs.nullplatform.com/docs/api/notification-channel-create Create a new notification channel. The `configuration` fields depend on the channel `type`. Slack, GitLab, Azure, and Bitbucket channels also require the matching provider integration to be configured for the NRN. Otherwise the request fails with a 400 error. **Note**: The path to the command line should point to the folder where the agent is installed.\n","example":"path-to-entrypoint-file/entrypoint --service-path=\"$SERVICE_PATH\""},"environment":{"type":"object","description":"The environment variables for the command.\n> **Note**: The environment variables should be in the format `KEY: VALUE`.\n","example":{"NP_ACTION_CONTEXT":"${NOTIFICATION_CONTEXT}"}},"arguments":{"type":"array","items":{"type":"string"},"description":"The arguments to be passed to the command line.\n","example":["arg1","arg2"]}}},"type":{"type":"string","enum":["exec"],"description":"The command type of the agent.\n> **Note**: Currently, the only supported type is `exec`.\n","example":"exec"}}},"selector":{"type":"object","description":"The selector that picks the agent. Keys and values must be strings.\n","additionalProperties":{"type":"string"},"example":{"environment":"local"}},"execution_config":{"type":"object","description":"Execution settings for the command.","properties":{"async":{"type":"boolean","description":"Whether the agent runs the command asynchronously. Defaults to `true`, unless the event was sent with `options.execution_strategy` set to `sync`.","example":true}}}},"title":"agent"}],"example":{"channels":["nullplatform-approvals"]}},"filters":{"type":"object","description":"A flexible object that uses MongoDB query syntax to define criteria for filtering notifications.\n\n> **Note**: See [Notification filters](/docs/notifications/filters) for more info.","example":{"action":"deployment:create"}}},"title":"notificationChannelCreate"},"examples":{"slack":{"value":{"nrn":"organization=1:account=2:namespace=3:application=4","source":["approval"],"description":"My Slack channel for approval notifications","type":"slack","configuration":{"channels":["my-organization-private-channel"]},"filters":{"action":"deployment:create"}}},"http":{"value":{"nrn":"organization=1:account=2:namespace=3:application=4","description":"My HTTP channel for approval notifications","source":["approval"],"type":"http","configuration":{"url":"https://yourdomain.com/url-you-configured","headers":{"Authorization":"Bearer the-token"}},"filters":{"action":"deployment:create"}}},"github":{"value":{"nrn":"organization=1:account=2:namespace=3:application=4","source":["service"],"description":"My GitHub channel","type":"github","configuration":{"installation_id":"my-installation-id","account":"my-github-org","repository":"services-opentofu","reference":"main","workflow_id":"provisioning.yml"},"filters":{"action":"deployment:create"}}},"gitlab":{"value":{"nrn":"organization=1:account=2:namespace=3:application=4","source":["service"],"description":"My GitLab channel","type":"gitlab","configuration":{"reference":"main","project_id":"1234"},"filters":{"action":"deployment:create"}}},"azure":{"value":{"nrn":"organization=1:account=2:namespace=3:application=4","source":["service"],"description":"My Azure channel","type":"azure","configuration":{"organization":"my-azure-org","project":"my-azure-project","pipeline_id":1234,"reference":"main"},"filters":{"action":"deployment:create"}}},"agent":{"value":{"nrn":"organization=1:account=2:namespace=3:application=4","source":["service"],"description":"My Agent channel","type":"agent","configuration":{"api_key":"AAAA.1234567890abcdef1234567890abcdefPTs=","command":{"data":{"cmdline":"path-to-entrypoint-file/entrypoint --service-path=\"$SERVICE_PATH\"","environment":{"NP_ACTION_CONTEXT":"${NOTIFICATION_CONTEXT}"}},"type":"exec"},"selector":{"environment":"local"}},"filters":{"service.specification.slug":"$service-spec-slug"}}},"bitbucket":{"value":{"nrn":"organization=1:account=2:namespace=3:application=4","source":["service"],"description":"My Bitbucket channel","type":"bitbucket","configuration":{"workspace":"my-bitbucket-workspace","repository":"services-opentofu","reference":"main","pipeline":"provisioning"},"filters":{"action":"deployment:create"}}}}}}}} > **Note**: The path to the command line should point to the folder where the agent is installed.\n","example":"path-to-entrypoint-file/entrypoint --service-path=\"$SERVICE_PATH\""},"environment":{"type":"object","description":"The environment variables for the command.\n> **Note**: The environment variables should be in the format `KEY: VALUE`.\n","example":{"NP_ACTION_CONTEXT":"${NOTIFICATION_CONTEXT}"}},"arguments":{"type":"array","items":{"type":"string"},"description":"The arguments to be passed to the command line.\n","example":["arg1","arg2"]}}},"type":{"type":"string","enum":["exec"],"description":"The command type of the agent.\n> **Note**: Currently, the only supported type is `exec`.\n","example":"exec"}}},"selector":{"type":"object","description":"The selector that picks the agent. Keys and values must be strings.\n","additionalProperties":{"type":"string"},"example":{"environment":"local"}},"execution_config":{"type":"object","description":"Execution settings for the command.","properties":{"async":{"type":"boolean","description":"Whether the agent runs the command asynchronously. Defaults to `true`, unless the event was sent with `options.execution_strategy` set to `sync`.","example":true}}}},"title":"agent"}]},"filters":{"type":"object","description":"A flexible object that uses MongoDB query syntax to define criteria for filtering notifications.\n\n> **Note**: See [Notification filters](/docs/notifications/filters) for more info.","example":{"action":"deployment:create"}}},"title":"notificationChannel"}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## Delete a channel URL: https://docs.nullplatform.com/docs/api/notification-channel-delete Delete a notification channel. This action cannot be undone. --- ## List channels URL: https://docs.nullplatform.com/docs/api/notification-channel-list List notification channels by NRN. **Note**: The path to the command line should point to the folder where the agent is installed.\n","example":"path-to-entrypoint-file/entrypoint --service-path=\"$SERVICE_PATH\""},"environment":{"type":"object","description":"The environment variables for the command.\n> **Note**: The environment variables should be in the format `KEY: VALUE`.\n","example":{"NP_ACTION_CONTEXT":"${NOTIFICATION_CONTEXT}"}},"arguments":{"type":"array","items":{"type":"string"},"description":"The arguments to be passed to the command line.\n","example":["arg1","arg2"]}}},"type":{"type":"string","enum":["exec"],"description":"The command type of the agent.\n> **Note**: Currently, the only supported type is `exec`.\n","example":"exec"}}},"selector":{"type":"object","description":"The selector that picks the agent. Keys and values must be strings.\n","additionalProperties":{"type":"string"},"example":{"environment":"local"}},"execution_config":{"type":"object","description":"Execution settings for the command.","properties":{"async":{"type":"boolean","description":"Whether the agent runs the command asynchronously. Defaults to `true`, unless the event was sent with `options.execution_strategy` set to `sync`.","example":true}}}},"title":"agent"}]},"filters":{"type":"object","description":"A flexible object that uses MongoDB query syntax to define criteria for filtering notifications.\n\n> **Note**: See [Notification filters](/docs/notifications/filters) for more info.","example":{"action":"deployment:create"}}},"title":"notificationChannel"}}},"title":"ChannelList"}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## Read a channel URL: https://docs.nullplatform.com/docs/api/notification-channel-read Read a notification channel by its ID. **Note**: The path to the command line should point to the folder where the agent is installed.\n","example":"path-to-entrypoint-file/entrypoint --service-path=\"$SERVICE_PATH\""},"environment":{"type":"object","description":"The environment variables for the command.\n> **Note**: The environment variables should be in the format `KEY: VALUE`.\n","example":{"NP_ACTION_CONTEXT":"${NOTIFICATION_CONTEXT}"}},"arguments":{"type":"array","items":{"type":"string"},"description":"The arguments to be passed to the command line.\n","example":["arg1","arg2"]}}},"type":{"type":"string","enum":["exec"],"description":"The command type of the agent.\n> **Note**: Currently, the only supported type is `exec`.\n","example":"exec"}}},"selector":{"type":"object","description":"The selector that picks the agent. Keys and values must be strings.\n","additionalProperties":{"type":"string"},"example":{"environment":"local"}},"execution_config":{"type":"object","description":"Execution settings for the command.","properties":{"async":{"type":"boolean","description":"Whether the agent runs the command asynchronously. Defaults to `true`, unless the event was sent with `options.execution_strategy` set to `sync`.","example":true}}}},"title":"agent"}]},"filters":{"type":"object","description":"A flexible object that uses MongoDB query syntax to define criteria for filtering notifications.\n\n> **Note**: See [Notification filters](/docs/notifications/filters) for more info.","example":{"action":"deployment:create"}}},"title":"notificationChannel"}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## Update a channel URL: https://docs.nullplatform.com/docs/api/notification-channel-update Updates a notification channel. **Note**: The path to the command line should point to the folder where the agent is installed.\n","example":"path-to-entrypoint-file/entrypoint --service-path=\"$SERVICE_PATH\""},"environment":{"type":"object","description":"The environment variables for the command.\n> **Note**: The environment variables should be in the format `KEY: VALUE`.\n","example":{"NP_ACTION_CONTEXT":"${NOTIFICATION_CONTEXT}"}},"arguments":{"type":"array","items":{"type":"string"},"description":"The arguments to be passed to the command line.\n","example":["arg1","arg2"]}}},"type":{"type":"string","enum":["exec"],"description":"The command type of the agent.\n> **Note**: Currently, the only supported type is `exec`.\n","example":"exec"}}},"selector":{"type":"object","description":"The selector that picks the agent. Keys and values must be strings.\n","additionalProperties":{"type":"string"},"example":{"environment":"local"}},"execution_config":{"type":"object","description":"Execution settings for the command.","properties":{"async":{"type":"boolean","description":"Whether the agent runs the command asynchronously. Defaults to `true`, unless the event was sent with `options.execution_strategy` set to `sync`.","example":true}}}},"title":"agent"}]},"filters":{"type":"object","description":"A flexible object that uses MongoDB query syntax to define criteria for filtering notifications.\n\n> **Note**: See [Notification filters](/docs/notifications/filters) for more info.","example":{"action":"deployment:create"}}},"title":"notificationChannelWrite"}}}}} > --- ## List events URL: https://docs.nullplatform.com/docs/api/notification-event-list List all notification events. **Note:** In the **Request** panel, click and drag or use Ctrl / Cmd + click to select multiple sources.\n","required":false,"style":"form","explode":true,"schema":{"type":"array","minItems":1,"maxItems":10,"items":{"type":"string","enum":["alert","approval","audit","entity","parameters","service","telemetry","checklist"]}},"example":"source=approval&source=service"},{"in":"query","name":"status","schema":{"type":"string","enum":["pending","success","fail"]},"required":false,"description":"Filters notification events by their status.","example":"success"},{"in":"query","name":"id","schema":{"type":"string","format":"uuid"},"required":false,"description":"Filters notification events by ID. Must be a valid UUID, otherwise the request fails with a 400 error.","example":"1180cd02-1c36-4274-8e7a-4483b87e8f2e"},{"in":"query","name":"created_at:gte","schema":{"type":"string","format":"date-time"},"required":false,"description":"Returns events created at or after this ISO 8601 timestamp.","example":"2026-01-01T00:00:00Z"},{"in":"query","name":"created_at:gt","schema":{"type":"string","format":"date-time"},"required":false,"description":"Returns events created after this ISO 8601 timestamp.","example":"2026-01-01T00:00:00Z"},{"in":"query","name":"created_at:lte","schema":{"type":"string","format":"date-time"},"required":false,"description":"Returns events created at or before this ISO 8601 timestamp.","example":"2026-01-31T23:59:59Z"},{"in":"query","schema":{"type":"boolean","default":false},"name":"include_context","required":false,"description":"Whether to include `context` in each event of the response.\n> ℹ️ **Note:** You need a **secops** role to use this parameter.\n","example":false}]} > --- ## Read an event URL: https://docs.nullplatform.com/docs/api/notification-event-read Retrieves a single notification event by its ID. ℹ️ **Note:** You need a **secops** role to use this parameter.\n","example":false}]} > --- ## Resend an event URL: https://docs.nullplatform.com/docs/api/notification-event-resend Resend a notification event. :::info You can choose to resend the event to specific channels or to all configured channels by omitting the `channel_ids` field in the request body. ::: ℹ️ **Note:** If omitted, the event is resent to **all configured channels**. An empty array is rejected.\n","example":[123,234,345]}},"title":"resendNotification"}}}}} > --- ## Create an NRN URL: https://docs.nullplatform.com/docs/api/nrn-create Creates a new NRN or replaces an existing one. **This operation overwrites any previous value.** > > ⚠️ **Deprecation notice** > > This operation might be removed in the future. We recommend using [Providers](/docs/providers/overview) instead. --- ## Delete namespaces or profiles URL: https://docs.nullplatform.com/docs/api/nrn-delete Removes all keys within a specified set of namespaces and/or profiles. By default, this endpoint acts on a single NRN entry and does not affect the entire hierarchy. Use the `include_parents` query parameter to modify this behavior. > > ⚠️ **Deprecation notice** > > This operation might be removed in the future. We recommend using [Providers](/docs/providers/overview) instead. --- ## Read an NRN URL: https://docs.nullplatform.com/docs/api/nrn-read Read an NRN by ID. > > ⚠️ **Deprecation notice** > > This operation might be removed in the future. We recommend using [Providers](/docs/providers/overview) instead. --- ## Update a value within an NRN URL: https://docs.nullplatform.com/docs/api/nrn-update Updates a value within an NRN. If the NRN does not exist, it will be created. > > ⚠️ **Deprecation notice** > > This operation might be removed in the future. We recommend using [Providers](/docs/providers/overview) instead. --- ## Replace an NRN URL: https://docs.nullplatform.com/docs/api/nrn-write Replaces an NRN. This operation overwrites any previous value. > > ⚠️ **Deprecation notice** > > This operation might be removed in the future. We recommend using [Providers](/docs/providers/overview) instead. --- ## Read an organization URL: https://docs.nullplatform.com/docs/api/organization-read Reads a specific organization by ID. --- ## Update an organization URL: https://docs.nullplatform.com/docs/api/organization-update Updates information for an organization. ℹ️ **Note:** Changing the organization's name **does not change the slug**, \n as doing so could break connections with existing applications and services.\n","type":"string","example":"Crypto Inc."},"status":{"description":"The status of the organization.","type":"string","enum":["active","inactive"],"example":"active"}},"title":"organization_write"}}}}} > --- ## Create a package URL: https://docs.nullplatform.com/docs/api/package-create Create a package. To publish its first revision in the same call, send `version` together with `components`. The first revision also becomes the package's `default_revision_id`. You need read access to every resource the bill of materials references. > ℹ️ **Note:** A service specification belongs to at most one package. Each `service_specification` component claims its specification for this package, and the request fails with `409` when another package already claimed it. It also fails with `409` when the NRN already has a package with the same slug. --- ## Delete a package URL: https://docs.nullplatform.com/docs/api/package-delete Soft-delete a package. Its revisions are retained, so a service that points at one of them keeps resolving its bill of materials. --- ## Diff two revisions URL: https://docs.nullplatform.com/docs/api/package-diff Compare two revisions of one package. `changes` lists every component name whose pin differs, with the component as pinned by each side (`null` on the side where the name doesn't exist). Artifact components also carry the artifact `type` and the pinned revision's `meta`, so you can tell what changed, for example a new digest or git reference. --- ## List packages URL: https://docs.nullplatform.com/docs/api/package-list List packages. The listing must be scoped: send `nrn`, `visible_to`, or both. > 💡 **Tip:** Prefer `visible_to` for consumption. It matches packages owned at the NRN, shared by ancestors or trailing-wildcard scopes, and global packages published with `organization=*`. The `nrn` filter only matches packages owned at exactly that NRN. The component filters (`component_name`, `resource_type`, `resource_id` and `resource_revision_id`) match against each package's resolved revision: `default_revision_id` when it's set, otherwise `latest_revision_id`. --- ## Read a package URL: https://docs.nullplatform.com/docs/api/package-read Read a package: its envelope plus the components of its resolved revision (`default_revision_id` when it's set, otherwise `latest_revision_id`). --- ## Read a package revision directly by id URL: https://docs.nullplatform.com/docs/api/package-revision-read-standalone Read a package revision when you only have its id, for example a service's `package_revision_id`. Authorized against the parent package. --- ## Read a revision URL: https://docs.nullplatform.com/docs/api/package-revision-read Read one revision of a package: its version and bill of materials. --- ## List revisions of a package URL: https://docs.nullplatform.com/docs/api/package-revisions-list List a package's revisions, newest first. Items don't include components unless you send `name`: then each item carries only the components with that name, which gives you the history of one component across revisions. --- ## Update a package URL: https://docs.nullplatform.com/docs/api/package-update Update `name`, `visible_to` or `default_revision_id`, publish a new revision, or both. To publish, pair `components` with exactly one of `version` or `bump`. `default: true` promotes the revision published in this call and can't be combined with `default_revision_id`. `id`, `nrn` and `slug` are immutable: sending any of them fails with `400`. :::tip PATCH or PUT? `PATCH` works on an existing package by id and never creates one. When you don't have the id, or you're publishing from a pipeline, use `PUT /packages`: it creates or publishes by `nrn` and `slug`. ::: > ℹ️ **Note:** Publishing a bill of materials identical to the latest revision's doesn't create a new revision. Publishing an existing version with a different bill of materials fails with `409`. --- ## Create or publish a package by slug URL: https://docs.nullplatform.com/docs/api/package-upsert Create a package, or publish a new revision of an existing one, in a single idempotent call keyed on `nrn` and `slug`. - When `nrn` has no package with that slug, creates the package and returns `201`. `name` is required. To publish the first revision in the same call, send `components` with an explicit `version`; `bump` needs a prior revision, so it can't be used here. - Otherwise, applies the call to the existing package and returns `200`: it publishes a new revision when you send `components`, and updates `visible_to` or `default_revision_id` when you send them. `name` is ignored on this path; rename a package with `PATCH`. `version` (explicit semver) and `bump` (`patch`, `minor` or `major`, relative to the latest revision) are mutually exclusive, and either one must be paired with `components`. With `merge_components` (default `true`) the components overlay the current bill of materials by name; with `false` the body must list the complete new bill of materials. `default: true` promotes the published revision in the same call. :::tip PUT or PATCH? `PUT` is the pipeline-friendly call. It needs no package id, creates the package on the first run, and every later run publishes against the same `nrn` and `slug`, so re-running it is safe. Use `PATCH /packages/{id}` when you already have the id, or to rename the package. ::: > ℹ️ **Note:** Publishing a bill of materials identical to the latest revision's doesn't create a new revision. Publishing an existing version with a different bill of materials fails with `409`. --- ## Create a parameter URL: https://docs.nullplatform.com/docs/api/parameter-create Create an application parameter. **Note**: Values are validated and authorized before the parameter is created, with the same permissions as `POST /parameter/{id}/values`. If a value is invalid or not authorized, the request fails and the parameter is not created.\n","items":{"type":"object","description":"A parameter value.","required":["nrn","value"],"properties":{"nrn":{"type":"string","description":"The NRN of the application or scope to which the value applies.\n\n> **Note**: For dimension settings, the NRN must be at the application-level.\n>","example":"organization=1:account=2:namespace=3:application=4:scope=5"},"value":{"type":"string","description":"The content of the value. **Cannot** exceed 32 KB for environment variables and 1 MB for files.","example":"200"},"dimensions":{"type":"object","description":"The dimensions of the value.","example":{"environment":"development","country":"US"}}}}}},"example":{"name":"Pool Size","nrn":"organization=1:account=2:namespace=3:application=4","type":"environment","encoding":"plaintext","variable":"POOL_SIZE","secret":false,"read_only":false,"values":[{"nrn":"organization=1:account=2:namespace=3:application=4","value":"10"},{"nrn":"organization=1:account=2:namespace=3:application=4","value":"50","dimensions":{"environment":"production"}}]},"title":"parameter_create_request"}}}}} > **Note:** If `show_secret_values` is set to `true`, the value is **only visible to users with the required permission**. See [Accessing secret values](/docs/parameters/secret-visibility) for more details.\n","example":"200"},"location":{"type":"string","description":"The file path. Applicable only for files; null otherwise. Also null for secrets.","example":null},"external":{"type":"object","nullable":true,"description":"Object containing the external ID and engine of the value. \n> **Note:** Applicable only for parameters stored in your own (external) infrastructure.\n","required":["id","engine"],"properties":{"id":{"description":"The external ID of the value.","type":"string","example":"external_id_123"},"engine":{"description":"The engine of the value. \n\n> **Note:** At the moment, only `hcp_vault` is supported.\n","type":"string","example":"hcp_vault"}}},"dimensions":{"type":"object","description":"The dimensions of the value.","example":{"environment":"development","country":"US"}}},"title":"parameter_value"}}},"title":"parameterResponse"}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## Delete a parameter URL: https://docs.nullplatform.com/docs/api/parameter-delete Delete a parameter along with all its values and versions. > > ⚠️ This action cannot be undone. > --- ## List parameters URL: https://docs.nullplatform.com/docs/api/parameter-list List parameters by NRN. **Note:** If `show_secret_values` is set to `true`, the value is **only visible to users with the required permission**. See [Accessing secret values](/docs/parameters/secret-visibility) for more details.\n","example":"200"},"location":{"type":"string","description":"The file path. Applicable only for files; null otherwise. Also null for secrets.","example":null},"external":{"type":"object","nullable":true,"description":"Object containing the external ID and engine of the value. \n> **Note:** Applicable only for parameters stored in your own (external) infrastructure.\n","required":["id","engine"],"properties":{"id":{"description":"The external ID of the value.","type":"string","example":"external_id_123"},"engine":{"description":"The engine of the value. \n\n> **Note:** At the moment, only `hcp_vault` is supported.\n","type":"string","example":"hcp_vault"}}},"dimensions":{"type":"object","description":"The dimensions of the value.","example":{"environment":"development","country":"US"}}},"title":"parameter_value"}}},"title":"parameterResponse"}}},"title":"ParameterList"}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## Read a parameter URL: https://docs.nullplatform.com/docs/api/parameter-read Retrieve a parameter by ID. ℹ️ **Note:** If set to `true`, secret values will be **visible only to users with the required permission**; otherwise, they will be hidden.\n> See [Accessing secret values](/docs/parameters/secret-visibility) for more details.\n","schema":{"type":"boolean","default":"false"},"example":"true"},{"name":"dimensions","in":"query","description":"Limits which secret values are revealed when `show_secret_values` is `true`. Specify [dimensions](/docs/dimensions) as comma-separated `key:value` pairs. Only the values matching these dimensions are returned with their content; every other value stays hidden.\n> ℹ️ **Note:** Users without the permission to read secret values need an approval that covers the requested dimensions. See [Accessing secret values](/docs/parameters/secret-visibility) for more details.\n","schema":{"type":"string"},"example":"environment:production"}]} > **Note:** If `show_secret_values` is set to `true`, the value is **only visible to users with the required permission**. See [Accessing secret values](/docs/parameters/secret-visibility) for more details.\n","example":"200"},"location":{"type":"string","description":"The file path. Applicable only for files; null otherwise. Also null for secrets.","example":null},"external":{"type":"object","nullable":true,"description":"Object containing the external ID and engine of the value. \n> **Note:** Applicable only for parameters stored in your own (external) infrastructure.\n","required":["id","engine"],"properties":{"id":{"description":"The external ID of the value.","type":"string","example":"external_id_123"},"engine":{"description":"The engine of the value. \n\n> **Note:** At the moment, only `hcp_vault` is supported.\n","type":"string","example":"hcp_vault"}}},"dimensions":{"type":"object","description":"The dimensions of the value.","example":{"environment":"development","country":"US"}}},"title":"parameter_value"}}},"title":"parameterResponse"}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## Create a single parameter value URL: https://docs.nullplatform.com/docs/api/parameter-value-create Create a value for a parameter, either for an application or a scope. **Note**: You can retrieve the version ID by calling the [List parameter versions](/docs/api/parameter-version-list) endpoint.\n","example":1},"nrn":{"type":"string","description":"The NRN of the application or scope to which the value applies.\n\n> **Note**: For dimension settings, the NRN must be at the application-level.\n>","example":"organization=1:account=2:namespace=3:application=4:scope=5"},"value":{"type":"string","description":"The content of the value. **Cannot** exceed 32 KB for environment variables and 1 MB for files.","example":"200"},"dimensions":{"type":"object","description":"The dimensions of the value.","example":{"environment":"development","country":"US"}}},"title":"parameter_value_create_request"}}}}} > **Note:** If `show_secret_values` is set to `true`, the value is **only visible to users with the required permission**. See [Accessing secret values](/docs/parameters/secret-visibility) for more details.\n","example":"200"},"location":{"type":"string","description":"The file path. Applicable only for files; null otherwise. Also null for secrets.","example":null},"external":{"type":"object","nullable":true,"description":"Object containing the external ID and engine of the value. \n> **Note:** Applicable only for parameters stored in your own (external) infrastructure.\n","required":["id","engine"],"properties":{"id":{"description":"The external ID of the value.","type":"string","example":"external_id_123"},"engine":{"description":"The engine of the value. \n\n> **Note:** At the moment, only `hcp_vault` is supported.\n","type":"string","example":"hcp_vault"}}},"dimensions":{"type":"object","description":"The dimensions of the value.","example":{"environment":"development","country":"US"}}},"title":"parameter_value"}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## Delete a parameter value URL: https://docs.nullplatform.com/docs/api/parameter-value-delete Deletes a specific parameter value. --- ## Compare two parameter values URL: https://docs.nullplatform.com/docs/api/parameter-values-compare Compares two parameter values of a single parameter. --- ## Create multiple parameter values URL: https://docs.nullplatform.com/docs/api/parameter-values-create Create multiple application or scope parameter values. **Note**: You can retrieve the version ID by calling the [List parameter versions](/docs/api/parameter-version-list) endpoint.\n","example":1},"values":{"type":"array","items":{"type":"object","description":"A parameter value.","required":["nrn","value"],"properties":{"nrn":{"type":"string","description":"The NRN of the application or scope to which the value applies.\n\n> **Note**: For dimension settings, the NRN must be at the application-level.\n>","example":"organization=1:account=2:namespace=3:application=4:scope=5"},"value":{"type":"string","description":"The content of the value. **Cannot** exceed 32 KB for environment variables and 1 MB for files.","example":"200"},"dimensions":{"type":"object","description":"The dimensions of the value.","example":{"environment":"development","country":"US"}}}}}},"title":"parameter_values_multi_create_request"}}}}} > **Note:** If `show_secret_values` is set to `true`, the value is **only visible to users with the required permission**. See [Accessing secret values](/docs/parameters/secret-visibility) for more details.\n","example":"200"},"location":{"type":"string","description":"The file path. Applicable only for files; null otherwise. Also null for secrets.","example":null},"external":{"type":"object","nullable":true,"description":"Object containing the external ID and engine of the value. \n> **Note:** Applicable only for parameters stored in your own (external) infrastructure.\n","required":["id","engine"],"properties":{"id":{"description":"The external ID of the value.","type":"string","example":"external_id_123"},"engine":{"description":"The engine of the value. \n\n> **Note:** At the moment, only `hcp_vault` is supported.\n","type":"string","example":"hcp_vault"}}},"dimensions":{"type":"object","description":"The dimensions of the value.","example":{"environment":"development","country":"US"}}},"title":"parameter_value"}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## Delete multiple parameter values URL: https://docs.nullplatform.com/docs/api/parameter-values-delete Deletes multiple parameter values. --- ## List parameter versions URL: https://docs.nullplatform.com/docs/api/parameter-version-list List all the available versions for a given parameter. --- ## Update a parameter URL: https://docs.nullplatform.com/docs/api/parameter-write Updates a field of a parameter. **Replacing a field **doesn't** affect the underlying parameter values.** **Note:** If `show_secret_values` is set to `true`, the value is **only visible to users with the required permission**. See [Accessing secret values](/docs/parameters/secret-visibility) for more details.\n","example":"200"},"location":{"type":"string","description":"The file path. Applicable only for files; null otherwise. Also null for secrets.","example":null},"external":{"type":"object","nullable":true,"description":"Object containing the external ID and engine of the value. \n> **Note:** Applicable only for parameters stored in your own (external) infrastructure.\n","required":["id","engine"],"properties":{"id":{"description":"The external ID of the value.","type":"string","example":"external_id_123"},"engine":{"description":"The engine of the value. \n\n> **Note:** At the moment, only `hcp_vault` is supported.\n","type":"string","example":"hcp_vault"}}},"dimensions":{"type":"object","description":"The dimensions of the value.","example":{"environment":"development","country":"US"}}},"title":"parameter_value"}}},"title":"parameterResponse"}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## Create a personal access token URL: https://docs.nullplatform.com/docs/api/pat-create Creates a personal access token that acts as you. It either follows your access (`access: same_as_user`) or holds the grants you send (`access: grants`), and every check combines it with your access at that moment, so it can never do more than you. > ℹ️ **Note:** Personal access tokens are managed with your own login session. A request authenticated with an API key, a personal access token included, is rejected with a `403`. ℹ️ **Note:** You can only grant roles you hold on that resource or one above it, and actions you hold there.\n","items":{"type":"object","oneOf":[{"type":"object","required":["nrn","role_slug"],"properties":{"nrn":{"type":"string","description":"The NRN of the resource where the API key's role is assigned.","example":"organization=1:account=1:namespace=1:application=4"},"role_slug":{"type":"string","description":"The slug of the role assigned to the API key for this NRN.\n\n> Note: See [Roles](/docs/authorization/roles) for more info.\n>\n","example":"machine:ci"}},"title":"grantsRoleSlug"},{"type":"object","required":["nrn","role_id"],"properties":{"nrn":{"type":"string","description":"The NRN of the resource where the API key's role is assigned.","example":"organization=1:account=1:namespace=1:application=4"},"role_id":{"type":"integer","description":"The ID of the role assigned to the API key for this NRN.\n\n> Note: See [Roles](/docs/authorization/roles) for more info.\n>\n","example":700317756}},"title":"grantsRoleId"},{"type":"object","required":["nrn","actions"],"properties":{"nrn":{"type":"string","description":"The NRN of the resource the grant applies to.","example":"organization=1:account=1:namespace=1"},"actions":{"type":"array","description":"The actions the API key may call at this NRN, without naming an existing role.\nnullplatform creates a role private to the key carrying exactly these actions.\n\n> Note: You can only grant actions you could hand out at that NRN yourself: the ones\n> you hold there, plus the ones carried by roles you may assign to an API key there.\n>\n","items":{"type":"string"},"example":["application:read","deployment:create"]}},"title":"grantsActions"},{"type":"object","required":["nrn","inherits"],"properties":{"nrn":{"type":"string","description":"The NRN of the resource the grant applies to.","example":"organization=1:account=1"},"inherits":{"type":"array","description":"Existing roles, by slug or ID, merged into a single role private to this key.\nInheriting two roles in one grant is not the same as granting them separately:\nit produces one role, which `actions.add` and `actions.remove` can then adjust.\nYou must be allowed to assign each inherited role to an API key at this NRN, the\nsame check a grant by `role_id` passes. The key follows the roles it inherits: an\naction later added to one of them reaches the key too.\n\n> Note: A role private to another API key cannot be inherited.\n>\n","items":{"oneOf":[{"type":"string"},{"type":"integer"}]},"example":["ops","developer"]},"actions":{"type":"object","description":"How the union of the inherited roles is adjusted.","properties":{"add":{"type":"array","description":"Actions granted on top of what the inherited roles provide. Measured against what you can hand out at this NRN.","items":{"type":"string"},"example":["application:delete"]},"remove":{"type":"array","description":"Inherited actions excluded from this grant. Each must be carried by one of the inherited roles, and none may also be in `add`.","items":{"type":"string"},"example":["deployment:create"]}}}},"title":"grantsInherits"}]}},"tags":{"type":"array","description":"Tags to find the token by. Each key can appear only once.","items":{"type":"object","properties":{"key":{"type":"string","description":"Tag key.","example":"team"},"value":{"type":"string","description":"Tag value.","example":"platform"}}}}},"title":"patNew"},"examples":{"Same access as you":{"value":{"name":"local-scripts","expires_in":2592000,"access":"same_as_user","tags":[{"key":"team","value":"platform"}]}},"Read-only grant on one account":{"value":{"name":"read-only-reports","expires_at":"2027-01-06T00:00:00Z","access":"grants","grants":[{"nrn":"organization=1:account=2","role_slug":"member"}]}}}}}}} > \n> ⚠️ **Save your token securely.**\n>\n> The token is displayed only once. Make sure to store it in a secure location as it cannot be retrieved later.","example":"AAAA.1234567890abcdef1234567890abcdefPTs="}}}],"title":"patPostResponse"}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## Delete a personal access token URL: https://docs.nullplatform.com/docs/api/pat-delete Deletes one of your personal access tokens. It stops working right away, including any access token already issued with it. --- ## List personal access tokens URL: https://docs.nullplatform.com/docs/api/pat-list Lists your own personal access tokens. You only ever see the tokens you created. > ℹ️ **Note:** Personal access tokens are managed with your own login session. A request authenticated with an API key, a personal access token included, is rejected with a `403`. --- ## Read a personal access token URL: https://docs.nullplatform.com/docs/api/pat-read Reads one of your personal access tokens by ID. --- ## Update a personal access token URL: https://docs.nullplatform.com/docs/api/pat-update Updates one of your personal access tokens. Send only what you want to change. - The expiration can only move later, up to a year from now. An expired token can't be extended. Send it as `expires_in` or `expires_at`, not both. - `grants` replaces all of the token's grants. - Switching to `same_as_user` drops the token's grants. Switching back to `grants` requires the grants it should have. Note: See [Roles](/docs/authorization/roles) for more info.\n>\n","example":"machine:ci"}},"title":"grantsRoleSlug"},{"type":"object","required":["nrn","role_id"],"properties":{"nrn":{"type":"string","description":"The NRN of the resource where the API key's role is assigned.","example":"organization=1:account=1:namespace=1:application=4"},"role_id":{"type":"integer","description":"The ID of the role assigned to the API key for this NRN.\n\n> Note: See [Roles](/docs/authorization/roles) for more info.\n>\n","example":700317756}},"title":"grantsRoleId"},{"type":"object","required":["nrn","actions"],"properties":{"nrn":{"type":"string","description":"The NRN of the resource the grant applies to.","example":"organization=1:account=1:namespace=1"},"actions":{"type":"array","description":"The actions the API key may call at this NRN, without naming an existing role.\nnullplatform creates a role private to the key carrying exactly these actions.\n\n> Note: You can only grant actions you could hand out at that NRN yourself: the ones\n> you hold there, plus the ones carried by roles you may assign to an API key there.\n>\n","items":{"type":"string"},"example":["application:read","deployment:create"]}},"title":"grantsActions"},{"type":"object","required":["nrn","inherits"],"properties":{"nrn":{"type":"string","description":"The NRN of the resource the grant applies to.","example":"organization=1:account=1"},"inherits":{"type":"array","description":"Existing roles, by slug or ID, merged into a single role private to this key.\nInheriting two roles in one grant is not the same as granting them separately:\nit produces one role, which `actions.add` and `actions.remove` can then adjust.\nYou must be allowed to assign each inherited role to an API key at this NRN, the\nsame check a grant by `role_id` passes. The key follows the roles it inherits: an\naction later added to one of them reaches the key too.\n\n> Note: A role private to another API key cannot be inherited.\n>\n","items":{"oneOf":[{"type":"string"},{"type":"integer"}]},"example":["ops","developer"]},"actions":{"type":"object","description":"How the union of the inherited roles is adjusted.","properties":{"add":{"type":"array","description":"Actions granted on top of what the inherited roles provide. Measured against what you can hand out at this NRN.","items":{"type":"string"},"example":["application:delete"]},"remove":{"type":"array","description":"Inherited actions excluded from this grant. Each must be carried by one of the inherited roles, and none may also be in `add`.","items":{"type":"string"},"example":["deployment:create"]}}}},"title":"grantsInherits"}]}},"tags":{"type":"array","description":"The token's new tags. They replace all of its current ones.","items":{"type":"object","properties":{"key":{"type":"string","description":"Tag key.","example":"team"},"value":{"type":"string","description":"Tag value.","example":"platform"}}}}},"title":"patWrite"},"examples":{"Extend the expiration":{"value":{"expires_in":7776000}},"Extend the expiration to a date":{"value":{"expires_at":"2027-04-06T00:00:00Z"}},"Follow your access":{"value":{"access":"same_as_user"}}}}}}} > --- ## Create a policy URL: https://docs.nullplatform.com/docs/api/policy-create Creates a new policy. **Note:** We recommend using selectors for services, as they allow you to target specific service specifications.\n","example":{"service.spec.id":{"$eq":2}}}},"title":"policyCreate"}}}}} > --- ## Delete a policy URL: https://docs.nullplatform.com/docs/api/policy-delete Deletes a policy. This action cannot be undone. --- ## List policies URL: https://docs.nullplatform.com/docs/api/policy-list List policies filtered by NRN. --- ## Read a policy URL: https://docs.nullplatform.com/docs/api/policy-read Reads a policy. --- ## Update a policy URL: https://docs.nullplatform.com/docs/api/policy-update Updates the conditions or selector of an existing policy. **Note:** We recommend using selectors for services, as they allow you to target specific service specifications.\n","example":{"service.spec.id":{"$eq":2}}}},"title":"policyWrite"}}}}} > --- ## List categories(Api) URL: https://docs.nullplatform.com/docs/api/provider-category-list Get a paginated list of providers categories filter by **type** and other queries --- ## Read a category(Api) URL: https://docs.nullplatform.com/docs/api/provider-category-read Read a provider category by ID. Use the **include** query parameter to retrieve related resources. --- ## Create a provider URL: https://docs.nullplatform.com/docs/api/provider-create Creates a new provider. Note that [dependencies](/docs/providers/overview#provider-dependencies) may need to be met. --- ## Delete a provider URL: https://docs.nullplatform.com/docs/api/provider-delete Delete a provider by ID --- ## List providers URL: https://docs.nullplatform.com/docs/api/provider-list Get a paginated list of providers filtered by [NRN](/docs/NRN) and other queries --- ## Read a provider URL: https://docs.nullplatform.com/docs/api/provider-read Read a provider by ID. The attributes are resolved with [inheritance](/docs/providers/overview#provider-inheritance) by default --- ## Replace a provider URL: https://docs.nullplatform.com/docs/api/provider-replace Replace a provider by ID --- ## Create a provider specification URL: https://docs.nullplatform.com/docs/api/provider-specification-create Create a new provider specification for your organization --- ## Delete a provider specification URL: https://docs.nullplatform.com/docs/api/provider-specification-delete Delete an existing provider specification. Cannot delete if providers are using it. --- ## List provider specifications URL: https://docs.nullplatform.com/docs/api/provider-specification-list Get a paginated list of providers specifications filtered by [NRN](/docs/NRN) and other queries --- ## Read a provider specification URL: https://docs.nullplatform.com/docs/api/provider-specification-read Read a provider specification by ID. The JSON schema attribute defines settings for any [nullplatform integrations](/docs/providers/supported-integrations). --- ## Update a provider specification URL: https://docs.nullplatform.com/docs/api/provider-specification-update Update an existing provider specification --- ## Update a provider URL: https://docs.nullplatform.com/docs/api/provider-update Updates a provider by ID --- ## Execute a data lake query URL: https://docs.nullplatform.com/docs/api/query-execute Execute a read-only SQL query against the audit data lake. The SQL statement can be sent in three ways: - As a JSON body: `{ "query": "SELECT ..." }` - As a plain text body with `Content-Type: text/plain` - As a `query` query string parameter > ℹ️ **Note:** If you provide the SQL in both the query string and the body, they must be identical or the request will fail. Results stream directly from ClickHouse. By default, the response uses tab-separated format. Add a `FORMAT JSON` clause to your SQL to get JSON output. --- ## Create a release URL: https://docs.nullplatform.com/docs/api/release-create Create a new release within an existing application. --- ## List releases URL: https://docs.nullplatform.com/docs/api/release-list List all releases within an application --- ## Read a release URL: https://docs.nullplatform.com/docs/api/release-read Read a specific release by ID. --- ## Update a release URL: https://docs.nullplatform.com/docs/api/release-update Update information for a release. --- ## Change published visibility URL: https://docs.nullplatform.com/docs/api/report-change-visibility Publishes a new version using the current published content with the new visibility, and updates the draft to match. Returns `204` if the visibility is unchanged, and `400` if the report has no published version. --- ## Copy a report URL: https://docs.nullplatform.com/docs/api/report-copy Creates a personal copy of an existing report. The copy is created with `user` visibility. --- ## Create a report URL: https://docs.nullplatform.com/docs/api/report-create Creates a new dynamic report. Only `name` is required. If `visibility` is omitted, the report is created as `user`. --- ## Delete a report URL: https://docs.nullplatform.com/docs/api/report-delete Soft-deletes a report. --- ## Get the published version URL: https://docs.nullplatform.com/docs/api/report-get-published-version Returns the report's currently published version, with all fields. --- ## List published reports URL: https://docs.nullplatform.com/docs/api/report-list-published Returns every report currently published and visible to you. Omits the heavy `schema`, `ui_schema`, and `queries` fields. --- ## List report versions URL: https://docs.nullplatform.com/docs/api/report-list-versions Returns the published version history for a report, without the heavy `schema`, `ui_schema`, and `queries` fields. --- ## List reports URL: https://docs.nullplatform.com/docs/api/report-list Lists the reports visible to you. Omits the heavy `schema`, `ui_schema`, and `queries` fields. --- ## Publish a report URL: https://docs.nullplatform.com/docs/api/report-publish Takes a snapshot of the current draft, stores it as a new version, and marks it as the published version. --- ## Read a report URL: https://docs.nullplatform.com/docs/api/report-read Returns the full report, including `schema`, `ui_schema`, and `queries`. --- ## Restore a version into the draft URL: https://docs.nullplatform.com/docs/api/report-restore Copies a version's content into the draft. Does not change the published version. --- ## Roll back to a version URL: https://docs.nullplatform.com/docs/api/report-rollback Makes an earlier version the published one. Does not modify the draft. --- ## Unpublish a report URL: https://docs.nullplatform.com/docs/api/report-unpublish Removes the published version reference so the report no longer appears in the published list. Version history is kept. --- ## Update a report URL: https://docs.nullplatform.com/docs/api/report-update Partially updates a report's draft. Send only the fields you want to change. --- ## Create a runtime configuration URL: https://docs.nullplatform.com/docs/api/runtime-configuration-create Creates an runtime configuration. > > ⚠️ **Deprecation notice** > > This operation might be removed in the future. We recommend using [Providers](/docs/providers/overview) instead. You can send an empty object `{}` to create a runtime configuration that applies to all dimensions.\n"},"values":{"type":"object","example":{"global-key":"global-value","my-namespace":{"namespace-key":"namespace-value"}},"description":"The set of values that this runtime configuration holds."}},"example":{"nrn":"organization=1:account=2:namespace=3","dimensions":{"environment":"production","country":"us"},"values":{"aws":{"log_reader":"...","workflow_role":"..."}}},"title":"RuntimeConfigurationCreate"}}}}} > --- ## Delete a runtime configuration URL: https://docs.nullplatform.com/docs/api/runtime-configuration-delete Remove a runtime configuration by ID. > > ⚠️ **Deprecation notice** > > This operation might be removed in the future. We recommend using [Providers](/docs/providers/overview) instead. --- ## List runtime configurations URL: https://docs.nullplatform.com/docs/api/runtime-configuration-list List runtime configuration. > > ⚠️ **Deprecation notice** > > This operation might be removed in the future. We recommend using [Providers](/docs/providers/overview) instead. --- ## Read a runtime configuration URL: https://docs.nullplatform.com/docs/api/runtime-configuration-read Read a runtime configuration. > > ⚠️ **Deprecation notice** > > This operation might be removed in the future. We recommend using [Providers](/docs/providers/overview) instead. --- ## Update a runtime configuration URL: https://docs.nullplatform.com/docs/api/runtime-configuration-write Updates a runtime configuration. > > ⚠️ **Deprecation notice** > > This operation might be removed in the future. We recommend using [Providers](/docs/providers/overview) instead. --- ## Create a scope URL: https://docs.nullplatform.com/docs/api/scope-create Create a new scope in an existing application. ℹ️ **Note:**\n> \n>• **Agent-backed scopes:** Use `custom` to create a scope managed by your installed agent. This is the recommended and most flexible option for new scopes.\n>\n> • **Legacy scopes:** The other types are\n older, predefined scope types that depend on fixed infrastructure configurations.\n","x-deprecatedValues":["web_pool","web_pool_k8s","serverless"]},"provider":{"type":"string","enum":["your_provider_id","GOOGLE:WEB_POOL:GKE","AZURE:WEB_POOL:AKS","OCI:WEB_POOL:OKE","AWS:WEB_POOL:EKS","AWS:WEB_POOL:EC2INSTANCES","AWS:SERVERLESS:LAMBDA"],"default":"your_provider_id","example":"9e84a5c3-6c2e-4f15-90b0-1c67fd37f6e8","description":"Identifies the provider that powers this scope.\n\n> ℹ️ **Note:**\n>\n> • **Agent-backed scopes:** The **UUID** of the scope specification you created. This UUID is returned when you define a new scope specification and acts as your provider ID.\n>\n> • **Legacy scopes:** One of the predefined provider strings (for example,\n `\"AWS:WEB_POOL:EKS\"` or `\"GOOGLE:WEB_POOL:GKE\"`).","x-deprecatedValues":["GOOGLE:WEB_POOL:GKE","AZURE:WEB_POOL:AKS","OCI:WEB_POOL:OKE","AWS:WEB_POOL:EKS","AWS:WEB_POOL:EC2INSTANCES","AWS:SERVERLESS:LAMBDA"]},"application_id":{"type":"integer","example":2345,"description":"The ID of the application that owns this scope."},"requested_spec":{"example":{"memory_in_gb":2,"cpu_profile":"standard","local_storage_in_gb":8},"type":"object","description":"Defines the technical specifications for this scope (will be replaced by capabilities). When you send it, include all three fields.","required":["memory_in_gb","cpu_profile","local_storage_in_gb"],"properties":{"memory_in_gb":{"type":"number","minimum":0.05,"maximum":128,"example":2,"description":"The amount of memory allocated to this scope, measured in gigabytes (GB)."},"cpu_profile":{"type":"string","enum":["standard","intensive"],"example":"standard","description":"Defines the CPU profile for this scope."},"local_storage_in_gb":{"type":"number","maximum":128,"example":2,"description":"The amount of local storage available to this scope, measured in gigabytes (GB)."}},"title":"ScopeRequestedSpec"},"capabilities":{"type":"object","example":null,"description":"Optional features such as logging, auto scaling, and spot instances. See the [capabilities](/docs/scopes/capabilities) docs."},"messages":{"type":"object","example":null,"description":"Additional system messages related to the scope's status or configuration."},"external_created":{"type":"boolean","default":false,"description":"Indicates whether the scope was provisioned externally.\n`false` when provisioned by nullplatform; `true` if created externally (e.g., via Terraform/OpenTofu).\n"},"tier":{"example":"important","type":"string","description":"Defines the tier type of the scope.","enum":["testing","non_critical","important","critical"],"title":"ScopeTier"},"domain":{"type":"string","maxLength":255,"example":"billing-api-prod.nullapps.io","description":"A custom domain for the scope. Auto-generated if not provided."},"asset_name":{"type":"string","maxLength":60,"example":"main-asset","description":"The name of the asset."},"tags":{"type":"array","items":{"type":"string"},"example":["requires_deployment","high_priority"],"description":"Tags to associate with the scope."},"dimensions":{"type":"object","example":{"environment":"production"},"description":"Defines runtime configuration dimensions, such as environment or region, that apply to this scope."},"profiles":{"type":"array","items":{"type":"string"},"example":["environment_production"],"description":"NRN profiles to apply to this scope, independent of runtime configurations."},"instance_id":{"type":"string","example":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","description":"A unique identifier for the runtime instance to associate with this scope. This value is typically set by the scope agent or cloud provider during provisioning."}},"title":"ScopeCreate"}}}}} > ℹ️ **Note:**\n>\n> • **Agent-backed scopes:** Use `custom` to create a scope managed by your installed agent.\n>\n> • **Legacy scopes:** The other types are\n older, predefined scope types that depend on fixed infrastructure configurations."},"provider":{"example":"9e84a5c3-6c2e-4f15-90b0-1c67fd37f6e8","default":"your_provider_id","type":"string","enum":["your_provider_id","GOOGLE:WEB_POOL:GKE","AZURE:WEB_POOL:AKS","OCI:WEB_POOL:OKE","AWS:WEB_POOL:EKS","AWS:WEB_POOL:EC2INSTANCES","AWS:SERVERLESS:LAMBDA"],"description":"Identifies the provider that powers this scope.\n\n> ℹ️ **Note:**\n> \n> • **Agent-backed scopes:** The **UUID** of the scope specification you created. This UUID is returned when you define a new scope specification and acts as your provider ID.\n>\n> • **Legacy scopes:** One of the predefined provider strings."},"status":{"example":"active","type":"string","description":"The status of the scope.","enum":["pending_approval","pending_hook","pending_approval_denied","pending","creating","recreating","active","updating","updating_approval","deleting","deleted","failed","stopping","stopped","cancelled"],"title":"ScopeStatus"},"slug":{"example":"billing-api","maxLength":60,"type":"string","readOnly":true,"description":"An application-wide unique slug for the scope. nullplatform derives it from the name when the scope is created: you can't send it, and it never changes afterwards."},"domain":{"example":"billing-api-prod.nullapps.io","maxLength":60,"type":"string","description":"An application-wide unique domain for the scope."},"application_id":{"example":2345,"type":"integer","description":"The ID of the application that owns this scope."},"requested_spec":{"example":{"memory_in_gb":2,"cpu_profile":"standard","local_storage_in_gb":8},"type":"object","description":"Defines the technical specifications for this scope (will be replaced by capabilities). When you send it, include all three fields.","required":["memory_in_gb","cpu_profile","local_storage_in_gb"],"properties":{"memory_in_gb":{"type":"number","minimum":0.05,"maximum":128,"example":2,"description":"The amount of memory allocated to this scope, measured in gigabytes (GB)."},"cpu_profile":{"type":"string","enum":["standard","intensive"],"example":"standard","description":"Defines the CPU profile for this scope."},"local_storage_in_gb":{"type":"number","maximum":128,"example":2,"description":"The amount of local storage available to this scope, measured in gigabytes (GB)."}},"title":"ScopeRequestedSpec"},"capabilities":{"type":"object","description":"Defines optional features such as logging, auto scaling, and spot instances. See the [capabilities](/docs/scopes/capabilities) documentation for details."},"messages":{"type":"object","description":"Additional system messages related to the scope's status or configuration.","example":null},"tier":{"example":"important","type":"string","description":"Defines the tier type of the scope.","enum":["testing","non_critical","important","critical"],"title":"ScopeTier"},"external_created":{"type":"boolean","description":"Indicates whether the scope was provisioned externally. `false` when provisioned by nullplatform, `true` if created externally (e.g., via Terraform)."},"tags":{"example":["requires_deployment","high_priority"],"type":"array","items":{"type":"string"},"description":"Tags associated with the scope."},"profiles":{"type":"array","items":{"type":"string"},"example":["environment_production"],"description":"NRN profiles that must be applied to this scope, independent of runtime configurations."},"dimensions":{"type":"object","example":{"environment":"production"},"description":"Defines runtime configuration dimensions, such as environment or region, that apply to this scope."},"runtime_configurations":{"type":"array","items":{"type":"integer"},"example":[1234,2345],"readOnly":true,"description":"Runtime configurations that apply to this scope, resolved by nullplatform from its dimension values. Assign them by setting the scope's dimensions, not by sending this field."},"domains":{"type":"array","description":"Array of custom domains associated with this scope.\n> It returns an empty array if no custom domains are assigned to the scope.\n","items":{"type":"object","required":["id","name","type"],"properties":{"id":{"type":"string","description":"The application-wide unique ID for the custom domain.","example":"e78a0d21-3c48-7421-81cd-203fe84b8e47"},"name":{"type":"string","description":"The name of the custom domain.","example":"api.crypto-inc.com"},"type":{"type":"string","description":"The type of the domain.","example":"scope"},"nrn":{"type":"string","description":"The NRN of the scope the custom domain is associated with. Not returned in list responses.","example":"organization=1:account=2:namespace=3:application=4:scope=1234"},"selector":{"type":"object","description":"The selector for the custom domain.","properties":{"organization_id":{"type":"integer","description":"The organization ID.","example":1},"nrn":{"type":"string","description":"The NRN of the scope the custom domain is associated with.","example":"organization=1:account=2:namespace=3:application=4:scope=1234"},"scope_id":{"type":"integer","description":"The ID of the scope the custom domain is associated with.","example":1234}}},"status":{"type":"string","description":"The status of the domain.","enum":["active","pending"],"example":"active"},"created_at":{"type":"string","description":"The ISO-8601 UTC timestamp of when the custom domain was created.","format":"date-time","example":"2025-01-02T00:00:00Z"},"updated_at":{"type":"string","description":"The ISO-8601 UTC timestamp of when the custom domain was last updated.","format":"date-time","example":"2025-02-02T00:00:00Z"}},"title":"scopeDomainResponse"}},"instance_id":{"type":"string","description":"A unique identifier for the runtime instance associated with this scope.","example":"a1b2c3d4-e5f6-7890-abcd-ef1234567890"},"application_name":{"type":"string","readOnly":true,"description":"The name of the application that owns this scope.","example":"Billing Service"},"active_deployment":{"type":"object","nullable":true,"description":"The last finalized deployment for this scope, or `null` if no deployment has been completed."},"current_active_deployment":{"type":"object","nullable":true,"description":"The currently running deployment for this scope."},"in_progress_deployment":{"type":"object","nullable":true,"description":"A new deployment currently in progress, or `null` if none."},"stops_at":{"type":"string","nullable":true,"format":"date-time","description":"The ISO-8601 UTC timestamp when this scope is scheduled to stop, or `null` if not scheduled.","example":"2026-04-01T00:00:00Z"},"token_for_links":{"type":"string","readOnly":true,"description":"A JWT token for generating authenticated scope links. Only present for organizations with `generate_domain_token` enabled."},"created_at":{"type":"string","format":"date-time","readOnly":true,"description":"The ISO-8601 UTC timestamp of when the scope was created.","example":"2025-01-02T00:00:00Z"},"updated_at":{"type":"string","format":"date-time","readOnly":true,"description":"The ISO-8601 UTC timestamp of when the scope was last updated.","example":"2025-06-15T12:30:00Z"},"metadata":{"type":"object","description":"The metadata loaded via the metadata catalog API.","example":{"key":"value"},"properties":{"key":{"type":"string","description":"The key of the metadata.","example":"key"},"value":{"type":"string","description":"The value of the metadata.","example":null},"additional_properties":{"type":"object","description":"Additional metadata properties, including schemas and tags.","properties":{"schemas":{"type":"array","description":"The JSON schemas of the metadata.","items":{"type":"object"}},"tags":{"type":"array","description":"Tags associated with the metadata.","items":{"type":"object","properties":{"key":{"type":"string","example":"key"},"value":{"type":"string","example":"value"}}}}}}},"title":"MetadataComponent"}},"title":"scope"}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## Remove a scope URL: https://docs.nullplatform.com/docs/api/scope-delete Remove a scope by ID. ℹ️ **Preconditions**\n>\n> The \"force delete\" option becomes available only if a previous delete attempt was unsuccessful.","schema":{"type":"string","enum":["true"]}}]} > --- ## Assign a dimension URL: https://docs.nullplatform.com/docs/api/scope-dimension-create Assign a dimension to this scope. --- ## Remove a dimension(Api) URL: https://docs.nullplatform.com/docs/api/scope-dimension-delete Remove a dimension from this scope. --- ## Create a custom domain URL: https://docs.nullplatform.com/docs/api/scope-domain-create Create a custom domain for a scope. --- ## Delete a custom domain URL: https://docs.nullplatform.com/docs/api/scope-domain-delete Delete a custom domain from a scope. --- ## List custom domains URL: https://docs.nullplatform.com/docs/api/scope-domain-list List custom domains assigned to a scope. --- ## Read a custom domain URL: https://docs.nullplatform.com/docs/api/scope-domain-read Read a custom domain assigned to a scope. --- ## Update a custom domain URL: https://docs.nullplatform.com/docs/api/scope-domain-update Updates a custom domain assigned to a scope. --- ## Get the scope token public keys URL: https://docs.nullplatform.com/docs/api/scope-jwks Returns the JSON Web Key Set (JWKS) with the public keys you can use to verify the signature of the `token_for_links` JWT returned in the [scope](/docs/api/scope-read) object. This endpoint is public: it doesn't require authentication. --- ## List scopes URL: https://docs.nullplatform.com/docs/api/scope-list List scopes within an application. ℹ️ **Note:**\n>\n> • **Agent-backed scopes:** Use `custom` to create a scope managed by your installed agent.\n>\n> • **Legacy scopes:** The other types are\n older, predefined scope types that depend on fixed infrastructure configurations."},"provider":{"example":"9e84a5c3-6c2e-4f15-90b0-1c67fd37f6e8","default":"your_provider_id","type":"string","enum":["your_provider_id","GOOGLE:WEB_POOL:GKE","AZURE:WEB_POOL:AKS","OCI:WEB_POOL:OKE","AWS:WEB_POOL:EKS","AWS:WEB_POOL:EC2INSTANCES","AWS:SERVERLESS:LAMBDA"],"description":"Identifies the provider that powers this scope.\n\n> ℹ️ **Note:**\n> \n> • **Agent-backed scopes:** The **UUID** of the scope specification you created. This UUID is returned when you define a new scope specification and acts as your provider ID.\n>\n> • **Legacy scopes:** One of the predefined provider strings."},"status":{"example":"active","type":"string","description":"The status of the scope.","enum":["pending_approval","pending_hook","pending_approval_denied","pending","creating","recreating","active","updating","updating_approval","deleting","deleted","failed","stopping","stopped","cancelled"],"title":"ScopeStatus"},"slug":{"example":"billing-api","maxLength":60,"type":"string","readOnly":true,"description":"An application-wide unique slug for the scope. nullplatform derives it from the name when the scope is created: you can't send it, and it never changes afterwards."},"domain":{"example":"billing-api-prod.nullapps.io","maxLength":60,"type":"string","description":"An application-wide unique domain for the scope."},"application_id":{"example":2345,"type":"integer","description":"The ID of the application that owns this scope."},"requested_spec":{"example":{"memory_in_gb":2,"cpu_profile":"standard","local_storage_in_gb":8},"type":"object","description":"Defines the technical specifications for this scope (will be replaced by capabilities). When you send it, include all three fields.","required":["memory_in_gb","cpu_profile","local_storage_in_gb"],"properties":{"memory_in_gb":{"type":"number","minimum":0.05,"maximum":128,"example":2,"description":"The amount of memory allocated to this scope, measured in gigabytes (GB)."},"cpu_profile":{"type":"string","enum":["standard","intensive"],"example":"standard","description":"Defines the CPU profile for this scope."},"local_storage_in_gb":{"type":"number","maximum":128,"example":2,"description":"The amount of local storage available to this scope, measured in gigabytes (GB)."}},"title":"ScopeRequestedSpec"},"capabilities":{"type":"object","description":"Defines optional features such as logging, auto scaling, and spot instances. See the [capabilities](/docs/scopes/capabilities) documentation for details."},"messages":{"type":"object","description":"Additional system messages related to the scope's status or configuration.","example":null},"tier":{"example":"important","type":"string","description":"Defines the tier type of the scope.","enum":["testing","non_critical","important","critical"],"title":"ScopeTier"},"external_created":{"type":"boolean","description":"Indicates whether the scope was provisioned externally. `false` when provisioned by nullplatform, `true` if created externally (e.g., via Terraform)."},"tags":{"example":["requires_deployment","high_priority"],"type":"array","items":{"type":"string"},"description":"Tags associated with the scope."},"profiles":{"type":"array","items":{"type":"string"},"example":["environment_production"],"description":"NRN profiles that must be applied to this scope, independent of runtime configurations."},"dimensions":{"type":"object","example":{"environment":"production"},"description":"Defines runtime configuration dimensions, such as environment or region, that apply to this scope."},"runtime_configurations":{"type":"array","items":{"type":"integer"},"example":[1234,2345],"readOnly":true,"description":"Runtime configurations that apply to this scope, resolved by nullplatform from its dimension values. Assign them by setting the scope's dimensions, not by sending this field."},"domains":{"type":"array","description":"Array of custom domains associated with this scope.\n> It returns an empty array if no custom domains are assigned to the scope.\n","items":{"type":"object","required":["id","name","type"],"properties":{"id":{"type":"string","description":"The application-wide unique ID for the custom domain.","example":"e78a0d21-3c48-7421-81cd-203fe84b8e47"},"name":{"type":"string","description":"The name of the custom domain.","example":"api.crypto-inc.com"},"type":{"type":"string","description":"The type of the domain.","example":"scope"},"nrn":{"type":"string","description":"The NRN of the scope the custom domain is associated with. Not returned in list responses.","example":"organization=1:account=2:namespace=3:application=4:scope=1234"},"selector":{"type":"object","description":"The selector for the custom domain.","properties":{"organization_id":{"type":"integer","description":"The organization ID.","example":1},"nrn":{"type":"string","description":"The NRN of the scope the custom domain is associated with.","example":"organization=1:account=2:namespace=3:application=4:scope=1234"},"scope_id":{"type":"integer","description":"The ID of the scope the custom domain is associated with.","example":1234}}},"status":{"type":"string","description":"The status of the domain.","enum":["active","pending"],"example":"active"},"created_at":{"type":"string","description":"The ISO-8601 UTC timestamp of when the custom domain was created.","format":"date-time","example":"2025-01-02T00:00:00Z"},"updated_at":{"type":"string","description":"The ISO-8601 UTC timestamp of when the custom domain was last updated.","format":"date-time","example":"2025-02-02T00:00:00Z"}},"title":"scopeDomainResponse"}},"instance_id":{"type":"string","description":"A unique identifier for the runtime instance associated with this scope.","example":"a1b2c3d4-e5f6-7890-abcd-ef1234567890"},"application_name":{"type":"string","readOnly":true,"description":"The name of the application that owns this scope.","example":"Billing Service"},"active_deployment":{"type":"object","nullable":true,"description":"The last finalized deployment for this scope, or `null` if no deployment has been completed."},"current_active_deployment":{"type":"object","nullable":true,"description":"The currently running deployment for this scope."},"in_progress_deployment":{"type":"object","nullable":true,"description":"A new deployment currently in progress, or `null` if none."},"stops_at":{"type":"string","nullable":true,"format":"date-time","description":"The ISO-8601 UTC timestamp when this scope is scheduled to stop, or `null` if not scheduled.","example":"2026-04-01T00:00:00Z"},"token_for_links":{"type":"string","readOnly":true,"description":"A JWT token for generating authenticated scope links. Only present for organizations with `generate_domain_token` enabled."},"created_at":{"type":"string","format":"date-time","readOnly":true,"description":"The ISO-8601 UTC timestamp of when the scope was created.","example":"2025-01-02T00:00:00Z"},"updated_at":{"type":"string","format":"date-time","readOnly":true,"description":"The ISO-8601 UTC timestamp of when the scope was last updated.","example":"2025-06-15T12:30:00Z"},"metadata":{"type":"object","description":"The metadata loaded via the metadata catalog API.","example":{"key":"value"},"properties":{"key":{"type":"string","description":"The key of the metadata.","example":"key"},"value":{"type":"string","description":"The value of the metadata.","example":null},"additional_properties":{"type":"object","description":"Additional metadata properties, including schemas and tags.","properties":{"schemas":{"type":"array","description":"The JSON schemas of the metadata.","items":{"type":"object"}},"tags":{"type":"array","description":"Tags associated with the metadata.","items":{"type":"object","properties":{"key":{"type":"string","example":"key"},"value":{"type":"string","example":"value"}}}}}}},"title":"MetadataComponent"}},"title":"scope"}}},"title":"scopeListResponse"}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## Read a scope URL: https://docs.nullplatform.com/docs/api/scope-read Read a specific scope. ℹ️ **Note:**\n>\n> • **Agent-backed scopes:** Use `custom` to create a scope managed by your installed agent.\n>\n> • **Legacy scopes:** The other types are\n older, predefined scope types that depend on fixed infrastructure configurations."},"provider":{"example":"9e84a5c3-6c2e-4f15-90b0-1c67fd37f6e8","default":"your_provider_id","type":"string","enum":["your_provider_id","GOOGLE:WEB_POOL:GKE","AZURE:WEB_POOL:AKS","OCI:WEB_POOL:OKE","AWS:WEB_POOL:EKS","AWS:WEB_POOL:EC2INSTANCES","AWS:SERVERLESS:LAMBDA"],"description":"Identifies the provider that powers this scope.\n\n> ℹ️ **Note:**\n> \n> • **Agent-backed scopes:** The **UUID** of the scope specification you created. This UUID is returned when you define a new scope specification and acts as your provider ID.\n>\n> • **Legacy scopes:** One of the predefined provider strings."},"status":{"example":"active","type":"string","description":"The status of the scope.","enum":["pending_approval","pending_hook","pending_approval_denied","pending","creating","recreating","active","updating","updating_approval","deleting","deleted","failed","stopping","stopped","cancelled"],"title":"ScopeStatus"},"slug":{"example":"billing-api","maxLength":60,"type":"string","readOnly":true,"description":"An application-wide unique slug for the scope. nullplatform derives it from the name when the scope is created: you can't send it, and it never changes afterwards."},"domain":{"example":"billing-api-prod.nullapps.io","maxLength":60,"type":"string","description":"An application-wide unique domain for the scope."},"application_id":{"example":2345,"type":"integer","description":"The ID of the application that owns this scope."},"requested_spec":{"example":{"memory_in_gb":2,"cpu_profile":"standard","local_storage_in_gb":8},"type":"object","description":"Defines the technical specifications for this scope (will be replaced by capabilities). When you send it, include all three fields.","required":["memory_in_gb","cpu_profile","local_storage_in_gb"],"properties":{"memory_in_gb":{"type":"number","minimum":0.05,"maximum":128,"example":2,"description":"The amount of memory allocated to this scope, measured in gigabytes (GB)."},"cpu_profile":{"type":"string","enum":["standard","intensive"],"example":"standard","description":"Defines the CPU profile for this scope."},"local_storage_in_gb":{"type":"number","maximum":128,"example":2,"description":"The amount of local storage available to this scope, measured in gigabytes (GB)."}},"title":"ScopeRequestedSpec"},"capabilities":{"type":"object","description":"Defines optional features such as logging, auto scaling, and spot instances. See the [capabilities](/docs/scopes/capabilities) documentation for details."},"messages":{"type":"object","description":"Additional system messages related to the scope's status or configuration.","example":null},"tier":{"example":"important","type":"string","description":"Defines the tier type of the scope.","enum":["testing","non_critical","important","critical"],"title":"ScopeTier"},"external_created":{"type":"boolean","description":"Indicates whether the scope was provisioned externally. `false` when provisioned by nullplatform, `true` if created externally (e.g., via Terraform)."},"tags":{"example":["requires_deployment","high_priority"],"type":"array","items":{"type":"string"},"description":"Tags associated with the scope."},"profiles":{"type":"array","items":{"type":"string"},"example":["environment_production"],"description":"NRN profiles that must be applied to this scope, independent of runtime configurations."},"dimensions":{"type":"object","example":{"environment":"production"},"description":"Defines runtime configuration dimensions, such as environment or region, that apply to this scope."},"runtime_configurations":{"type":"array","items":{"type":"integer"},"example":[1234,2345],"readOnly":true,"description":"Runtime configurations that apply to this scope, resolved by nullplatform from its dimension values. Assign them by setting the scope's dimensions, not by sending this field."},"domains":{"type":"array","description":"Array of custom domains associated with this scope.\n> It returns an empty array if no custom domains are assigned to the scope.\n","items":{"type":"object","required":["id","name","type"],"properties":{"id":{"type":"string","description":"The application-wide unique ID for the custom domain.","example":"e78a0d21-3c48-7421-81cd-203fe84b8e47"},"name":{"type":"string","description":"The name of the custom domain.","example":"api.crypto-inc.com"},"type":{"type":"string","description":"The type of the domain.","example":"scope"},"nrn":{"type":"string","description":"The NRN of the scope the custom domain is associated with. Not returned in list responses.","example":"organization=1:account=2:namespace=3:application=4:scope=1234"},"selector":{"type":"object","description":"The selector for the custom domain.","properties":{"organization_id":{"type":"integer","description":"The organization ID.","example":1},"nrn":{"type":"string","description":"The NRN of the scope the custom domain is associated with.","example":"organization=1:account=2:namespace=3:application=4:scope=1234"},"scope_id":{"type":"integer","description":"The ID of the scope the custom domain is associated with.","example":1234}}},"status":{"type":"string","description":"The status of the domain.","enum":["active","pending"],"example":"active"},"created_at":{"type":"string","description":"The ISO-8601 UTC timestamp of when the custom domain was created.","format":"date-time","example":"2025-01-02T00:00:00Z"},"updated_at":{"type":"string","description":"The ISO-8601 UTC timestamp of when the custom domain was last updated.","format":"date-time","example":"2025-02-02T00:00:00Z"}},"title":"scopeDomainResponse"}},"instance_id":{"type":"string","description":"A unique identifier for the runtime instance associated with this scope.","example":"a1b2c3d4-e5f6-7890-abcd-ef1234567890"},"application_name":{"type":"string","readOnly":true,"description":"The name of the application that owns this scope.","example":"Billing Service"},"active_deployment":{"type":"object","nullable":true,"description":"The last finalized deployment for this scope, or `null` if no deployment has been completed."},"current_active_deployment":{"type":"object","nullable":true,"description":"The currently running deployment for this scope."},"in_progress_deployment":{"type":"object","nullable":true,"description":"A new deployment currently in progress, or `null` if none."},"stops_at":{"type":"string","nullable":true,"format":"date-time","description":"The ISO-8601 UTC timestamp when this scope is scheduled to stop, or `null` if not scheduled.","example":"2026-04-01T00:00:00Z"},"token_for_links":{"type":"string","readOnly":true,"description":"A JWT token for generating authenticated scope links. Only present for organizations with `generate_domain_token` enabled."},"created_at":{"type":"string","format":"date-time","readOnly":true,"description":"The ISO-8601 UTC timestamp of when the scope was created.","example":"2025-01-02T00:00:00Z"},"updated_at":{"type":"string","format":"date-time","readOnly":true,"description":"The ISO-8601 UTC timestamp of when the scope was last updated.","example":"2025-06-15T12:30:00Z"},"metadata":{"type":"object","description":"The metadata loaded via the metadata catalog API.","example":{"key":"value"},"properties":{"key":{"type":"string","description":"The key of the metadata.","example":"key"},"value":{"type":"string","description":"The value of the metadata.","example":null},"additional_properties":{"type":"object","description":"Additional metadata properties, including schemas and tags.","properties":{"schemas":{"type":"array","description":"The JSON schemas of the metadata.","items":{"type":"object"}},"tags":{"type":"array","description":"Tags associated with the metadata.","items":{"type":"object","properties":{"key":{"type":"string","example":"key"},"value":{"type":"string","example":"value"}}}}}}},"title":"MetadataComponent"}},"title":"scope"}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## Create a troubleshooting action URL: https://docs.nullplatform.com/docs/api/scope-troubleshooting-action-create Submits an action to kill a specific instance or pod. **Note**: Actions with the name `autoscaling_stop` and `autoscaling_start` do not require parameters.","example":{"instance_id":"i-0a1234567890b1234"},"properties":{"instance_id":{"oneOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"The ID of the instance to act on, or a list of IDs. Applies only to the `instance_kill` and `instance_restart` actions.","example":"i-0a1234567890b1234"},"desired_instances":{"type":"integer","description":"The target number of instances. Applies only to the `autoscaling_fix` action.","example":10},"autoscaling_enabled":{"type":"boolean","description":"Indicates whether autoscaling should be enabled. Applies only to the `autoscaling_fix` action.","example":true}},"title":"ScopeActionParameters"}},"example":{"name":"instance_kill","parameters":{"instance_id":"i-0a1234567890b1234"}},"title":"ScopeActionInstanceKillCreate"}}}}} > --- ## Read a troubleshooting action URL: https://docs.nullplatform.com/docs/api/scope-troubleshooting-action-read Read a troubleshooting action --- ## Create a scope type URL: https://docs.nullplatform.com/docs/api/scope-type-create Enable scope types for your applications. --- ## Delete a scope type URL: https://docs.nullplatform.com/docs/api/scope-type-delete Delete a scope type. The request fails with a `409` error while an active scope still uses it. --- ## List scope types URL: https://docs.nullplatform.com/docs/api/scope-type-list List your scope types for a given NRN. ℹ️ **Note:** This is the scope specification ID.","example":"9f3b4a12-6e29-4b71-92d8-45c7e2fd9f36"},{"in":"query","name":"name","schema":{"type":"string"},"required":false,"description":"The name displayed to the developers to identify the scope type.","example":"EKS CronJob"},{"in":"query","name":"include","schema":{"type":"string"},"required":false,"description":"Comma-separated list of additional data to include. Available values:\n\n- `capabilities`: adds `capabilities` and `parameters` (the schema of the values a scope of this type accepts) to each scope type.\n- `available`: adds an `available` flag that tells whether the scope type can be used for the given `nrn` (and `dimensions`).\n- `wildcard`: also returns scope types defined for all organizations.","example":"capabilities,available"},{"in":"query","name":"dimensions","schema":{"type":"string"},"required":false,"description":"Dimensions used to calculate the `available` flag when `include` contains `available`.","example":"environment:staging"}]} > --- ## Read a scope type URL: https://docs.nullplatform.com/docs/api/scope-type-read Read a scope type. --- ## Update a scope type URL: https://docs.nullplatform.com/docs/api/scope-type-write Updates the name, description or status of a scope type. Other fields can't be changed and are ignored. --- ## Update a scope URL: https://docs.nullplatform.com/docs/api/scope-update Updates information for a scope. ℹ️ **Note:** Changing the scope's name **does not change the slug**, \n as doing so could break connections with existing applications and services.\n","type":"string","maxLength":100,"example":"Payments API."},"dimensions":{"type":"object","example":{"environment":"production"},"description":"Defines runtime configuration dimensions, such as environment or region, that apply to this scope."}},"title":"scope_update"}}}}} > --- ## Create a service action URL: https://docs.nullplatform.com/docs/api/service-action-create Create a service action --- ## Delete a service action URL: https://docs.nullplatform.com/docs/api/service-action-delete Delete a service action --- ## List service actions URL: https://docs.nullplatform.com/docs/api/service-action-list List service actions --- ## Send a notification for a service action URL: https://docs.nullplatform.com/docs/api/service-action-notify Send a notification for a service action by action ID --- ## Read a service action URL: https://docs.nullplatform.com/docs/api/service-action-read Read a service action by action ID --- ## Update a service action URL: https://docs.nullplatform.com/docs/api/service-action-update Updates a service action --- ## Compare two services URL: https://docs.nullplatform.com/docs/api/service-compare Compare two services --- ## Create a service URL: https://docs.nullplatform.com/docs/api/service-create Create a service --- ## Delete a service URL: https://docs.nullplatform.com/docs/api/service-delete Delete a service --- ## List services URL: https://docs.nullplatform.com/docs/api/service-list List services **Note:** Requires the **SecOps** role. See [Roles](/docs/authorization/roles) for details.\n"},{"schema":{"type":"boolean"},"in":"query","name":"has_links","required":false,"description":"Filter services by whether they have links. If true, only services with at least one link that is neither archived nor deleted are returned. If false, only services with no such links are returned"},{"schema":{"title":"NRN","type":"string","pattern":"^organization=[0-9*]+(:account=[0-9*]+)?(:namespace=[0-9*]+)?(:application=[0-9*]+)?(:scope=[0-9*]+)?$","description":"Nullplatform Resource Name (NRN) representing a hierarchical identifier for nullplatform resources"},"in":"query","name":"link_entity_nrn","required":false,"description":"Narrows has_links to a single entity. With has_links=true, returns services that have links matching this entity NRN prefix; with has_links=false, services that have no links matching it"},{"schema":{"title":"Id","type":"string","format":"uuid","description":"Unique identifier for the entity represented as a UUID"},"in":"query","name":"package_revision_id","required":false,"description":"Filter services by the package revision they are currently running"},{"schema":{"title":"Id","type":"string","format":"uuid","description":"Unique identifier for the entity represented as a UUID"},"in":"query","name":"desired_package_revision_id","required":false,"description":"Filter services by the package revision they should move to"}]} > --- ## Read a service URL: https://docs.nullplatform.com/docs/api/service-read Read a service **Note:** Requires the **SecOps** role. See [Roles](/docs/authorization/roles) for details.\n"}]} > --- ## Create a service specification action URL: https://docs.nullplatform.com/docs/api/service-specification-actions-create Create a service specification action --- ## Delete a service specification action URL: https://docs.nullplatform.com/docs/api/service-specification-actions-delete Delete a service specification action --- ## List service specification actions URL: https://docs.nullplatform.com/docs/api/service-specification-actions-list List service specification actions. --- ## Read a service action specification URL: https://docs.nullplatform.com/docs/api/service-specification-actions-read Read an action specification of a service specification --- ## Update a service specification action URL: https://docs.nullplatform.com/docs/api/service-specification-actions-update Updates a service specification action --- ## Create a service specification URL: https://docs.nullplatform.com/docs/api/service-specification-create Create a service specification --- ## Delete a service specification URL: https://docs.nullplatform.com/docs/api/service-specification-delete Delete a service specification --- ## List service specifications URL: https://docs.nullplatform.com/docs/api/service-specification-list List service specifications --- ## Package an existing service specification URL: https://docs.nullplatform.com/docs/api/service-specification-package-create Create a package from a service specification that doesn't belong to a package yet. The server assembles the bill of materials for you, in a single database transaction: 1. Pins the specification, each of its action specifications, each of its link specifications and each link's action specifications to a snapshot. An entity whose current content matches its latest snapshot reuses that snapshot; otherwise, including an entity with no snapshot yet, a new snapshot is minted. 2. Resolves `sources`. An inline source (`type` and `meta`) registers the artifact, or a new revision of it, under `nrn`. A referenced source (`artifact_id`) pins an existing artifact at `revision_id`, or at its newest revision when you omit it. 3. Creates the package and its first revision with these components: - `spec`: the service specification. - One component per source, named after the source (`source` when there's a single unnamed source). - One component per action specification, named after its slug. - One component per link specification, named after its slug. - One component per link action specification, named `:`. The package's `name` and `slug` default to the specification's, and `visible_to` defaults to `[nrn]`. The response is the same package `POST /packages` returns. > ℹ️ **Note:** `nrn` must be concrete (no wildcards). The request fails with `409` when the specification already belongs to a package; to publish a new revision of that package, use `PATCH /packages/{id}`. --- ## Read a service specification URL: https://docs.nullplatform.com/docs/api/service-specification-read Read a service specification --- ## List snapshots of a service specification URL: https://docs.nullplatform.com/docs/api/service-specification-snapshots-list List the append-only snapshot history of a service specification, newest first. Snapshot ids are what package components pin as resource_revision_id. --- ## Read a snapshot of a service specification URL: https://docs.nullplatform.com/docs/api/service-specification-snapshots-read Read a single snapshot of a service specification, including its full content. --- ## Update a service specification URL: https://docs.nullplatform.com/docs/api/service-specification-update Updates a service specification --- ## Update a service URL: https://docs.nullplatform.com/docs/api/service-update Updates a service --- ## Mark suggestion as applied URL: https://docs.nullplatform.com/docs/api/suggestion-applied Report that a suggestion was successfully applied. --- ## Approve a suggestion URL: https://docs.nullplatform.com/docs/api/suggestion-approve Approve a pending suggestion. The owning agent will then execute the fix. --- ## Create a suggestion URL: https://docs.nullplatform.com/docs/api/suggestion-create Create a new suggestion for an action item. Suggestions are created in `pending` status. The suggestion creator is always set from the authenticated user's bearer token. --- ## Mark suggestion as failed URL: https://docs.nullplatform.com/docs/api/suggestion-failed Report that a suggestion execution failed. The suggestion can be retried by approving it again. --- ## List suggestions URL: https://docs.nullplatform.com/docs/api/suggestion-list List all suggestions for an action item. --- ## Read a suggestion URL: https://docs.nullplatform.com/docs/api/suggestion-read Get a single suggestion by ID. --- ## Reject a suggestion URL: https://docs.nullplatform.com/docs/api/suggestion-reject Reject a pending suggestion. --- ## Update a suggestion URL: https://docs.nullplatform.com/docs/api/suggestion-update Update a suggestion. The `user_metadata` field is only editable when the suggestion is in `pending` or `failed` status. To retry a failed suggestion, set `status` to `approved`. --- ## Create a technology template URL: https://docs.nullplatform.com/docs/api/technology-template-create Create a technology template that serves as a starter or skeleton for new applications. For example, if the URL is `https://github.com/nullplatform/technology-templates-nextjs`, \nthe repository name should be `technology-templates-nextjs`.\n","example":"technology-templates-nextjs"}}},"organization":{"type":"integer","description":"The ID of the organization.","example":234},"account":{"type":"integer","description":"The ID of account.","example":345},"status":{"type":"string","description":"The status of the template.","enum":["active","inactive"],"example":"active"},"components":{"type":"array","description":"The components of the template.","items":{"type":"object","properties":{"type":{"type":"string","description":"The type of component (e.g., language, framework).","example":"language"},"id":{"type":"string","description":"The ID of the component (e.g., javascript, golang, java).","example":"javascript-language"},"version":{"type":"string","description":"The version of the component (e.g., 20.2, 1.18, 17).","example":"1.0"}},"title":"TemplateComponent"}},"tags":{"type":"array","items":{"type":"string"},"description":"Tags associated with the template (e.g., node, nextjs, frontend)","example":"frontend"},"rules":{"type":"object","description":"Defines rules for enforcing naming conventions and structuring repository paths.\n","items":{"type":"object","properties":{"application_name":{"type":"string","description":"A regular expression pattern used to validate the format of the application name.\n> **Note**: This field allows you to define naming conventions for applications, ensuring consistency across repository naming. For example, to require the application name to end with `-api`, use a pattern like `^(.*)-api$`.\n","format":"regex","example":"^(.*)-api$"},"repository_path":{"type":"string","description":"A substitution expression defining the custom structure of the repository path based on matched groups from the application name. \n> **Note**: This field uses a syntax similar to `sed` substitution (`s/regex/replacement/flags`), enabling you to dynamically construct repository paths. > For instance, to rearrange parts of the application name with a new directory structure, use a pattern like `s/^(.*)-(.*)$/${namespace.slug}-golang-${1}-${2}/g`.\n","format":"regex","example":"s/^(.*)-(.*)$/${namespace.slug}-golang-${1}-${2}/g"}},"title":"TemplateRules"}}},"example":{"name":"Next.JS 13","organization":234,"account":345,"status":"active","url":"https://github.com/nullplatform/technology-templates-nextjs","provider":{"repository":"technology-templates-nextjs"},"components":[{"type":"language","id":"javascript","version":"es6"},{"type":"framework","id":"next.js","version":"13"}],"rules":{"application_name":"^(.*)-api$","repository_path":"s/^(.*)-(.*)$/${namespace.slug}-golang-${1}-${2}/g"},"tags":["javascript","frontend","nextjs"]},"title":"TechnologyTemplateCreate"}}}}} > For example, if the URL is `https://github.com/nullplatform/technology-templates-nextjs`, \nthe repository name should be `technology-templates-nextjs`.\n","example":"technology-templates-nextjs"}}},"components":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","description":"The type of component (e.g., language, framework).","example":"language"},"id":{"type":"string","description":"The ID of the component (e.g., javascript, golang, java).","example":"javascript-language"},"version":{"type":"string","description":"The version of the component (e.g., 20.2, 1.18, 17).","example":"1.0"}},"title":"TemplateComponent"},"description":"The components of the template."},"tags":{"type":"array","example":"frontend","items":{"type":"string"},"description":"Tags associated with the template (e.g., node, nextjs, frontend)."},"updated_at":{"type":"string","nullable":true,"description":"The ISO-8601 UTC timestamp of when the technology template was last updated.","format":"date-time","example":"2021-01-02T00:00:00Z"},"created_at":{"type":"string","description":"The ISO-8601 UTC timestamp of when the technology template was created.","format":"date-time","example":"2021-01-02T00:00:00Z"}},"title":"TechnologyTemplateResponse"}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## Delete a technology template URL: https://docs.nullplatform.com/docs/api/technology-template-delete Deletes a specific technology template. --- ## List technology templates URL: https://docs.nullplatform.com/docs/api/technology-template-list List all technology templates available in your organization. For example, if the URL is `https://github.com/nullplatform/technology-templates-nextjs`, \nthe repository name should be `technology-templates-nextjs`.\n","example":"technology-templates-nextjs"}}},"components":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","description":"The type of component (e.g., language, framework).","example":"language"},"id":{"type":"string","description":"The ID of the component (e.g., javascript, golang, java).","example":"javascript-language"},"version":{"type":"string","description":"The version of the component (e.g., 20.2, 1.18, 17).","example":"1.0"}},"title":"TemplateComponent"},"description":"The components of the template."},"tags":{"type":"array","example":"frontend","items":{"type":"string"},"description":"Tags associated with the template (e.g., node, nextjs, frontend)."},"updated_at":{"type":"string","nullable":true,"description":"The ISO-8601 UTC timestamp of when the technology template was last updated.","format":"date-time","example":"2021-01-02T00:00:00Z"},"created_at":{"type":"string","description":"The ISO-8601 UTC timestamp of when the technology template was created.","format":"date-time","example":"2021-01-02T00:00:00Z"}},"title":"TechnologyTemplateResponse"}}},"title":"TechnologyTemplateListResponse"}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## Read a technology template URL: https://docs.nullplatform.com/docs/api/technology-template-read Read a specific technology template. For example, if the URL is `https://github.com/nullplatform/technology-templates-nextjs`, \nthe repository name should be `technology-templates-nextjs`.\n","example":"technology-templates-nextjs"}}},"components":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","description":"The type of component (e.g., language, framework).","example":"language"},"id":{"type":"string","description":"The ID of the component (e.g., javascript, golang, java).","example":"javascript-language"},"version":{"type":"string","description":"The version of the component (e.g., 20.2, 1.18, 17).","example":"1.0"}},"title":"TemplateComponent"},"description":"The components of the template."},"tags":{"type":"array","example":"frontend","items":{"type":"string"},"description":"Tags associated with the template (e.g., node, nextjs, frontend)."},"updated_at":{"type":"string","nullable":true,"description":"The ISO-8601 UTC timestamp of when the technology template was last updated.","format":"date-time","example":"2021-01-02T00:00:00Z"},"created_at":{"type":"string","description":"The ISO-8601 UTC timestamp of when the technology template was created.","format":"date-time","example":"2021-01-02T00:00:00Z"}},"title":"TechnologyTemplateResponse"}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## Update a technology template URL: https://docs.nullplatform.com/docs/api/technology-template-write Updates a technology template. For example, if the URL is `https://github.com/nullplatform/technology-templates-nextjs`, \nthe repository name should be `technology-templates-nextjs`.\n","example":"technology-templates-nextjs"}}},"organization":{"type":"integer","description":"The ID of the organization.","example":234},"account":{"type":"integer","description":"The ID of account.","example":345},"status":{"type":"string","description":"The status of the template.","enum":["active","inactive"],"example":"active"},"components":{"type":"array","description":"The components of the template.","items":{"type":"object","properties":{"type":{"type":"string","description":"The type of component (e.g., language, framework).","example":"language"},"id":{"type":"string","description":"The ID of the component (e.g., javascript, golang, java).","example":"javascript-language"},"version":{"type":"string","description":"The version of the component (e.g., 20.2, 1.18, 17).","example":"1.0"}},"title":"TemplateComponent"}},"tags":{"type":"array","example":"frontend","items":{"type":"string"},"description":"Tags associated with the template (e.g., node, nextjs, frontend)"},"rules":{"type":"object","description":"Defines rules for enforcing naming conventions and structuring repository paths.\n","items":{"type":"object","properties":{"application_name":{"type":"string","description":"A regular expression pattern used to validate the format of the application name.\n> **Note**: This field allows you to define naming conventions for applications, ensuring consistency across repository naming. For example, to require the application name to end with `-api`, use a pattern like `^(.*)-api$`.\n","format":"regex","example":"^(.*)-api$"},"repository_path":{"type":"string","description":"A substitution expression defining the custom structure of the repository path based on matched groups from the application name. \n> **Note**: This field uses a syntax similar to `sed` substitution (`s/regex/replacement/flags`), enabling you to dynamically construct repository paths. > For instance, to rearrange parts of the application name with a new directory structure, use a pattern like `s/^(.*)-(.*)$/${namespace.slug}-golang-${1}-${2}/g`.\n","format":"regex","example":"s/^(.*)-(.*)$/${namespace.slug}-golang-${1}-${2}/g"}},"title":"TemplateRules"}},"metadata":{"type":"object","description":"The metadata loaded via the metadata catalog API.","example":{"key":"value"},"properties":{"key":{"type":"string","description":"The key of the metadata.","example":"key"},"value":{"type":"string","description":"The value of the metadata.","example":null},"additional_properties":{"type":"object","description":"Additional metadata properties, including schemas and tags.","properties":{"schemas":{"type":"array","description":"The JSON schemas of the metadata.","items":{"type":"object"}},"tags":{"type":"array","description":"Tags associated with the metadata.","items":{"type":"object","properties":{"key":{"type":"string","example":"key"},"value":{"type":"string","example":"value"}}}}}}},"title":"MetadataComponent"}},"example":{"name":"Next.JS 13","organization":234,"account":345,"status":"active","url":"https://github.com/nullplatform/technology-templates-nextjs","provider":{"repository":"technology-templates-nextjs"},"components":[{"type":"language","id":"javascript","version":"es6"},{"type":"framework","id":"next.js","version":"13"}],"rules":{"application_name":"^(.*)-api$","repository_path":"s/^(.*)-(.*)$/${namespace.slug}-golang-${1}-${2}/g"},"tags":["javascript","frontend","nextjs"],"metadata":{"key":"value","schemas":[{"schema":"schema1"},{"schema":"schema2"}],"tags":[{"key":"tag1","value":"value1"},{"key":"tag2","value":"value2"}]}},"title":"TechnologyTemplate_write"}}}}} > For example, if the URL is `https://github.com/nullplatform/technology-templates-nextjs`, \nthe repository name should be `technology-templates-nextjs`.\n","example":"technology-templates-nextjs"}}},"components":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","description":"The type of component (e.g., language, framework).","example":"language"},"id":{"type":"string","description":"The ID of the component (e.g., javascript, golang, java).","example":"javascript-language"},"version":{"type":"string","description":"The version of the component (e.g., 20.2, 1.18, 17).","example":"1.0"}},"title":"TemplateComponent"},"description":"The components of the template."},"tags":{"type":"array","example":"frontend","items":{"type":"string"},"description":"Tags associated with the template (e.g., node, nextjs, frontend)."},"updated_at":{"type":"string","nullable":true,"description":"The ISO-8601 UTC timestamp of when the technology template was last updated.","format":"date-time","example":"2021-01-02T00:00:00Z"},"created_at":{"type":"string","description":"The ISO-8601 UTC timestamp of when the technology template was created.","format":"date-time","example":"2021-01-02T00:00:00Z"}},"title":"TechnologyTemplateResponse"}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## Create a user URL: https://docs.nullplatform.com/docs/api/user-create Create a new user in your organization. --- ## Bulk create grants for a user URL: https://docs.nullplatform.com/docs/api/user-grants-bulk-create Create multiple grants for a user in a single request. This operation is **additive**: it adds new grants without affecting existing ones. The operation is **transactional**: if any grant fails validation, no grants are created. You can **use either `role_id` or `role_slug`** for each grant in the request. Note: See [Roles](/docs/authorization/roles) for more info.\n","example":"developer"}}},{"title":"Using role ID","type":"object","required":["nrn","role_id"],"properties":{"nrn":{"type":"string","description":"The NRN of the resource where the role is assigned.","example":"organization=1:account=2"},"role_id":{"type":"integer","description":"The ID of the role to assign.\n\n> Note: See [Roles](/docs/authorization/roles) for more info.\n","example":700317756}}}]},"title":"bulkGrantRequest"},"examples":{"Using role_slug":{"value":[{"nrn":"organization=1:account=2","role_slug":"developer"},{"nrn":"organization=1:account=3","role_slug":"ops"}]},"Using role_id":{"value":[{"nrn":"organization=1:account=2","role_id":700317756},{"nrn":"organization=1:account=3","role_id":708509758}]},"Mixed role_id and role_slug":{"value":[{"nrn":"organization=1:account=2","role_slug":"developer"},{"nrn":"organization=1:account=3","role_id":708509758}]}}}}}} > --- ## List grants for a user URL: https://docs.nullplatform.com/docs/api/user-grants-list List all grants assigned to a user. Returns a paginated list of grants with role information. --- ## Replace all grants for a user URL: https://docs.nullplatform.com/docs/api/user-grants-replace-all Replace all existing grants for a user with a new set of grants. This operation **deletes all existing grants** and creates the new ones. - Send an **empty array** `[]` to remove all grants from the user. - Send a **non-empty array** to replace all grants with the specified ones. The operation is **transactional**: if any new grant fails validation, no changes are made. You can **use either `role_id` or `role_slug`** for each grant in the request. Note: See [Roles](/docs/authorization/roles) for more info.\n","example":"developer"}}},{"title":"Using role ID","type":"object","required":["nrn","role_id"],"properties":{"nrn":{"type":"string","description":"The NRN of the resource where the role is assigned.","example":"organization=1:account=2"},"role_id":{"type":"integer","description":"The ID of the role to assign.\n\n> Note: See [Roles](/docs/authorization/roles) for more info.\n","example":700317756}}}]},"title":"bulkGrantReplaceRequest"},"examples":{"Replace with new grants":{"value":[{"nrn":"organization=1:account=2","role_slug":"admin"}]},"Remove all grants":{"value":[]}}}}}} > --- ## List users URL: https://docs.nullplatform.com/docs/api/user-list List all users in your organization. --- ## Read a user URL: https://docs.nullplatform.com/docs/api/user-read Read a user by ID. --- ## List user roles URL: https://docs.nullplatform.com/docs/api/user-role-list Lists all roles assigned to users in your organization. • `pending`: Include roles that are pending acceptance.\n>\n> • `actions`: Add an `actions` array with every action the user can grant to an API key\n> at this NRN: the actions of the roles they hold on it or on a parent, plus the actions of\n> the roles they may assign to a key there. Requires exactly one `user_id` and one `nrn`.\n> This is the ceiling the fine-grained API key picker offers.\n","required":false,"example":"actions"}]} > • `active`: The user has been granted the role and currently has access.\n>\n> • `pending`: The role assignment is awaiting approval; access is not yet granted.\n","example":"active"},"role":{"type":"object","required":["id","slug","name"],"properties":{"id":{"type":"integer","description":"The ID of the role assigned to the user","example":696188987},"name":{"type":"string","description":"The human-readable name of the role.","example":"Admin"},"slug":{"type":"string","description":"The slug of the role.","example":"admin"},"description":{"type":"string","description":"A brief description of the role's purpose and permissions.","example":"Manage all the resources"},"level":{"type":"string","description":"The level of the role within the organization hierarchy.","nullable":true,"deprecated":true,"example":null},"can_assign_roles":{"type":"array","description":"List of role that this role can assign to other users.\n\n> ℹ️ If the role cannot assign any roles, we return an empty array.\n","items":{"type":"object","required":["id","slug","name"],"properties":{"id":{"type":"integer","description":"The ID of the role that can be assigned.","example":700317756},"name":{"type":"string","description":"The human-readable name of the role that can be assigned.","example":"Developer"},"slug":{"type":"string","description":"The slug of the role that can be assigned.","example":"developer"},"description":{"type":"string","description":"A brief description of the role's purpose and permissions.","example":"Can create builds and releases, scopes, and start deployments"},"level":{"type":"string","description":"The level of the role within the organization hierarchy.","nullable":true,"deprecated":true,"example":null},"assignment_restriction":{"type":"string","nullable":true,"description":"Indicates any restrictions on assigning this role, depending on the user type.\n","enum":["PERSON","MACHINE"],"example":null}}}}}}},"title":"userRoleResponse"}},"actions":{"type":"array","description":"Present when the request includes `include=actions`: the actions the user can grant to an\nAPI key at the requested NRN. An action that has an alias is listed under the alias, not\nunder the name it is stored as.\n","items":{"type":"string"},"example":["application:read","deployment:create"]}},"title":"userRolePaginatedResponse"}}}}},"4XX":{"description":"The request couldn't be processed. For example, a parameter is invalid, the resource doesn't exist, or you don't have permission. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"urn:nullplatform:error:routes-helper:invalid-params"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"There are invalid parameters on the request"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":400},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations.","example":"ROUTES_HELPER.INVALID_PARAMS"},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"errors":{"type":"array","description":"One entry per invalid field or parameter. Only present on validation errors.","items":{"type":"object","properties":{"field":{"type":"string","description":"The name of the invalid field or parameter.","example":"application_id"},"in":{"type":"string","description":"Where the field was sent: `body`, `query`, or `params` (path parameters).","example":"query"},"issue":{"type":"string","description":"What's wrong with the field, such as `required` or `invalid`.","example":"invalid"},"value":{"description":"The value that was sent, when relevant.","example":"not-a-number"},"message":{"type":"string","description":"A human-readable explanation of the issue."}}}},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"There are invalid parameters on the request"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}},"5XX":{"description":"Something went wrong on nullplatform's side. The body is a problem details object (RFC 9457).","content":{"application/problem+json":{"schema":{"type":"object","description":"Describes what went wrong, following the RFC 9457 problem details format.","required":["type","title","status"],"properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It's an identifier, not a link, and it's the same every time this problem occurs.","example":"about:blank"},"title":{"type":"string","description":"A short, human-readable summary of the problem.","example":"Oops.. there was an internal error"},"status":{"type":"integer","description":"The HTTP status code of the response.","example":500},"code":{"type":"string","description":"A stable, machine-readable error code. Use it to handle specific errors in your integrations."},"detail":{"type":"string","nullable":true,"description":"More information about what went wrong in this request, when available.","example":null},"message":{"type":"string","deprecated":true,"description":"Same value as `title`, kept for backward compatibility. Use `title` or `code` instead.","example":"Oops.. there was an internal error"},"traceId":{"type":"string","description":"An identifier for this request, when available. Include it when you contact nullplatform support."}}}}}}}} > --- ## Update a user URL: https://docs.nullplatform.com/docs/api/user-write Updates a user in your organization. :::info Only users of type `person` can be updated. ::: ℹ️ **Note:** Only users of type `person` can be updated.\n"}]} > ℹ️ **Info:** Set the status to `active` to allow the user to log in to the platform, or to `inactive` to prevent login access.\n","enum":["active","inactive"],"type":"string","example":"active"},"email":{"description":"The email of the user you want to update.","type":"string","example":"alex.doe@email.com"},"first_name":{"description":"The first name of the user you want to update.","type":"string","example":"Alex"},"last_name":{"description":"The last name of the user you want to update.","type":"string","example":"Doe"},"organization_id":{"description":"The organization ID of the user you want to update.","type":"integer","example":1234},"avatar":{"description":"The avatar of the user you want to update.","type":"string","example":"https://my-avatar-url.com"}},"title":"userWrite"}}}}} > --- ## Activate an alias URL: https://docs.nullplatform.com/docs/api/workflow-alias-activate Turns the alias on: every trigger on its revision registers, so cron schedules start firing, webhooks get a public URL, and event subscriptions are created. Until this call, a published workflow has zero side effects. - If any trigger fails to register, the response is still 200 but the alias rolls back to inactive: check `failedTriggers` for the reason. - `latest` cannot be activated. Create a named alias (for example `live`) first. --- ## Create an alias URL: https://docs.nullplatform.com/docs/api/workflow-alias-create Creates an alias pointing at an existing revision. `latest` is reserved. Creating an alias does not activate its triggers; call the activate endpoint for that. --- ## Deactivate an alias URL: https://docs.nullplatform.com/docs/api/workflow-alias-deactivate Marks the alias inactive and tears down its running triggers (best effort). --- ## Delete an alias URL: https://docs.nullplatform.com/docs/api/workflow-alias-delete Deletes the alias. `latest` cannot be deleted. --- ## List aliases URL: https://docs.nullplatform.com/docs/api/workflow-alias-list Lists the workflow's aliases with per-trigger activation state. Webhook URLs in `triggerStates` are recomputed on each read. --- ## Read an alias URL: https://docs.nullplatform.com/docs/api/workflow-alias-read Returns one alias with its trigger states. --- ## Repoint an alias URL: https://docs.nullplatform.com/docs/api/workflow-alias-update Points the alias at another existing revision. Idempotent. --- ## Delete a secret or variable URL: https://docs.nullplatform.com/docs/api/workflow-config-delete Deletes the entry and its stored value. --- ## List secrets and variables URL: https://docs.nullplatform.com/docs/api/workflow-config-list Lists the entries defined at a place: a folder (`path`) or a workflow (`workflow`), exactly one of the two. With `ancestors=true`, returns the whole precedence chain up to `/`, flagging each row's `effective` winner. Secret values are never included. --- ## Read a secret or variable URL: https://docs.nullplatform.com/docs/api/workflow-config-read Returns one entry by id. `value` is only present for non-secret entries. --- ## Rotate a value URL: https://docs.nullplatform.com/docs/api/workflow-config-rotate Replaces the entry's value in place. Name, place, and the secret flag are unchanged. --- ## Set a secret or variable URL: https://docs.nullplatform.com/docs/api/workflow-config-set Creates a secret or variable, or rotates its value if it already exists at that place. Safe to call repeatedly. - The place is exactly one of `path` (a folder, `/` for the whole organization) or `workflow` (one workflow). - Writing a name that already exists at that place replaces its value; workflows referencing it pick the new value up on their next run. - The `name` and the `secret` flag are immutable: to change them, delete the entry and recreate it. --- ## Create a workflow URL: https://docs.nullplatform.com/docs/api/workflow-create Creates a workflow. The request body is the definition document itself, not wrapped in any envelope. - The workflow starts at revision 1, with the auto-maintained `latest` alias pointing at it. - Nothing runs yet: triggers only register when you activate an alias. - Referencing secrets or variables that have no value yet is fine: creation succeeds and the response lists them under `warnings` so you can set them afterwards. --- ## Delete a workflow URL: https://docs.nullplatform.com/docs/api/workflow-delete Deletes the workflow: it disappears from lists and reads, along with its revisions and aliases. The record is retained internally (soft delete). --- ## Read engine metadata URL: https://docs.nullplatform.com/docs/api/workflow-engine-metadata Returns what the engine supports: API version, how many plugins are installed, and feature flags. This is the only endpoint that requires no authentication, which makes it a simple connectivity check. --- ## Execute a workflow URL: https://docs.nullplatform.com/docs/api/workflow-execute Starts an execution of the revision resolved by `alias` (default `latest`) or `revision`. With an `idempotencyKey`, retries return the original execution with 200 and `deduplicated: true`. --- ## Cancel an execution URL: https://docs.nullplatform.com/docs/api/workflow-execution-cancel Cancels a running execution. --- ## Export an execution URL: https://docs.nullplatform.com/docs/api/workflow-execution-export Streams the full execution as NDJSON: one `execution` line, one `step` line per attempt (full payloads), and one `log` line per entry. --- ## List execution steps URL: https://docs.nullplatform.com/docs/api/workflow-execution-list-steps Lists step attempts, paginated. Payloads are omitted by default (their sizes are reported instead); pass `include=payloads` for full rows. --- ## List executions URL: https://docs.nullplatform.com/docs/api/workflow-execution-list Lists executions in your organization, paginated and filterable. --- ## Read execution logs URL: https://docs.nullplatform.com/docs/api/workflow-execution-logs Returns log entries, newest window first. Page backwards by passing the response's `nextCursor` as `cursor` until it is absent. --- ## List pending signals URL: https://docs.nullplatform.com/docs/api/workflow-execution-pending-signals Lists the signal waits the execution is currently parked on. --- ## Read a step's invocations URL: https://docs.nullplatform.com/docs/api/workflow-execution-read-step Returns every attempt and iteration of one step, always with full inputs, outputs, and logs. --- ## Read an execution URL: https://docs.nullplatform.com/docs/api/workflow-execution-read Returns the execution record. Large final snapshots are omitted by default and their size reported as `finalSnapshotBytes`; pass `include=finalSnapshot` for the full payload. --- ## Re-run an execution URL: https://docs.nullplatform.com/docs/api/workflow-execution-rerun Starts a new execution of the same workflow and revision. With `fromStep`, resumes from that step replaying the prior steps' outputs. Optional body `inputs` override the original run inputs. --- ## Read execution state URL: https://docs.nullplatform.com/docs/api/workflow-execution-state Returns a live state snapshot: per-step outputs, current steps, pending signal waits, variables, and run metadata. Queried live while the execution runs; rebuilt from storage afterwards. --- ## List workflows URL: https://docs.nullplatform.com/docs/api/workflow-list Lists the workflows in your organization, paginated. --- ## List plugins URL: https://docs.nullplatform.com/docs/api/workflow-plugin-list Lists the plugin descriptors available on the engine, paginated and filterable. --- ## Read a plugin URL: https://docs.nullplatform.com/docs/api/workflow-plugin-read Returns one plugin descriptor, including its full config schema and examples. --- ## Read a workflow URL: https://docs.nullplatform.com/docs/api/workflow-read Returns the workflow and one resolved revision: the latest by default, or the one selected with `revision` or `alias` (mutually exclusive). --- ## List revisions URL: https://docs.nullplatform.com/docs/api/workflow-revision-list Lists the workflow's revisions, paginated. Definitions are omitted (`definition: null`) unless `includeDefinition=true`. --- ## Read a revision(Api) URL: https://docs.nullplatform.com/docs/api/workflow-revision-read Returns one revision with its full definition. --- ## Send a signal by correlation key URL: https://docs.nullplatform.com/docs/api/workflow-signal-send-by-correlation Delivers a signal to the non-terminal execution of `workflowId` matching `correlationKey`. With `startIfMissing: true`, starts a new execution (using `startInputs`) when none is waiting. --- ## Send a signal to an execution URL: https://docs.nullplatform.com/docs/api/workflow-signal-send Delivers a signal to an execution waiting on it. The request body is passed through verbatim as the signal payload. Returns 409 when the execution is not waiting on that signal. --- ## List trigger bindings URL: https://docs.nullplatform.com/docs/api/workflow-trigger-list Flat list of trigger bindings across workflows and aliases, one row per trigger, with live status and webhook URLs. --- ## Update workflow metadata URL: https://docs.nullplatform.com/docs/api/workflow-update-metadata Updates record-level fields (name, description, path, metadata) without creating a revision. At least one field is required. --- ## Update a workflow URL: https://docs.nullplatform.com/docs/api/workflow-update Pushes a new version of the definition. The body can be the bare definition or `{ definition, mode }`, where `mode` controls versioning: - `auto` (default): overwrites the latest revision, unless an active alias points at it, in which case a new revision is created. Safe by default: you never rewrite something that is live. - `new-revision`: always create a new revision. - `overwrite`: always rewrite the latest revision. If the `{id}` in the URL is a key that doesn't exist yet, the workflow is created instead and the response is 201: `PUT` doubles as an upsert, so a git repository of definitions can push them without knowing which ones already exist. --- ## Validate a definition URL: https://docs.nullplatform.com/docs/api/workflow-validate Dry-run validation of a definition (graph shape plus step configs) with no writes. Always returns 200; inspect `valid` and `errors`. --- ## Welcome to the nullplatform API! URL: https://docs.nullplatform.com/docs/api-getting-started Everything in nullplatform is an API. This guide gets you making calls in a couple of minutes and covers the conventions every endpoint follows. 🚀 ## Make your first call All API requests need an access token in the `Authorization` header. To grab yours, log in to nullplatform, click your **user menu** (the avatar in the top-right corner), and select **Copy session token**. Then you're one request away: ```bash curl -L "https://api.nullplatform.com/account" \ -H "Authorization: Bearer $NP_ACCESS_TOKEN" ``` :::info Machine users Scripts, automations, and integrations authenticate with an **API key** and obtain access tokens from it. See the [API keys](/docs/authorization/api-keys) docs for details. ::: ## OpenAPI specification The nullplatform API is built on the OpenAPI 3.0 specification. Download the full spec to import into your API client or code generator: - Nullplatform OpenAPI (YAML) - Nullplatform OpenAPI (JSON) ## Key entities When working with the API, you'll interact with the following entities. Click each node to see what it is and how to query it: One organization per company. Accounts separate business units, namespaces group applications by team or domain (like "billing" or "fraud"), and each application deploys to one or more scopes (like "staging" or "EU"). ## Explore the API Or browse the full surface in the sidebar. ## Listing entities, filtering, and sorting data You can retrieve entity lists by making requests without specifying an ID (e.g., `GET /account`). Here are some key points: - **Filtering:** Use query parameters, e.g., `GET /account?status=active`. We support filtering by multiple values using a comma as a separator like `GET /account?name=crypto-kong&status=active`. - **Sorting:** Use the `sort` parameter with the field and order, e.g. `GET /account?sort=name:desc`. Available sorts are `asc` and `desc`. - **Paging:** Use `limit` and `offset` to navigate through paginated results. `offset` indicates where to start in the result set (default is 0), `limit` specifies the number of results per page (default is 30), and the response's `total` is the number of results that match your query. The maximum page size depends on the endpoint. For example, `GET /application` without a parent entity returns at most 100 results per page. - **Specify the parent entity:** You are authorized only to list things that you have access to, so you have to filter by the parent entity. For example, if you are searching for applications, specify under which `namespace_id` you'll be running the query (e.g., `GET /application?namespace_id=123`). You can specify multiple parent entities for the search (up to 10), as long as you have listing permissions for each of them. - **Responses:** All entities share a uniform response format for list requests: ```json GET /application?namespace_id=123 { "paging": { "total": 30, "offset": 0, "limit": 30 }, "results": [ { "id": 1234, "name": "Billing API", "slug": "billing-api", "namespace_id": 123, "nrn": "organization=1:account=12:namespace=123:application=1234", "status": "active", "repository_url": "https://example.com/crypto-inc/accounting-billing-api", "repository_app_path": null, "is_mono_repo": false, "auto_deploy_on_creation": false, "template_id": 3456, "tags": {}, "settings": {}, "messages": [], "created_at": "2026-08-14T13:05:21.118Z", "updated_at": "2026-08-14T13:05:29.402Z" } ] } ``` Reading a single entity returns the same fields plus `metadata`, the values attached through the [metadata catalog](/docs/catalog/getting-started). List responses don't include it. ## Technical conventions ### Use the right domain Note that our API is hosted at `https://api.nullplatform.com`. We do not use the `.io` domain for the API. ### PUT vs. PATCH **Use `PATCH` to update entities.** It's the method our APIs expect, it only touches the fields you send, and on several endpoints it's the only one accepted. Applications, for example, answer any `PUT` with a `405 Method Not Allowed`: ```json PUT /application/1234 { "message": "Applications allow only partial update through PATCH." } ``` Reach for `PUT` only when the endpoint supports it and you deliberately want to replace a whole resource. :::warning `PUT` replaces, it doesn't merge Using `PUT` on the NRN API fully replaces the NRN content, so every field you leave out of the body is lost. Unless you're really sure of what you're doing, send a `PATCH`. ::: ## Permissions and access control Nullplatform has a permission system that allows for granular access control: - **API endpoints represent actions.** Each endpoint corresponds to an action (e.g., `deploy:create`), and to call it you must have permission for that action. Some endpoints require permissions for additional actions. - **Roles bundle actions.** Every user has one or more roles (e.g., `developer`, `admin`) that determine which actions they can perform. - **Roles are granted on specific resources.** Being a `developer` in a test application doesn't automatically grant the same permissions in another application. The same role might be granted in one area of the organization but not in another, so it needs to be explicitly granted for each resource where access is required. - **Permissions extend to child resources.** If you have the `developer` role at the account level, you inherit permissions for all namespaces, applications, and scopes within that account. :::tip Putting it all together - **Action**: Defines what you can do in nullplatform (e.g., `deploy:create`). - **Role**: Links a group of users (e.g., `developer`) to the actions required for that role (e.g., `organization:read`). - **Resources**: The specific part of the organization's assets that a user is granted access to (e.g., "application 8" or "application 4"). ::: To grant a role on a resource, see the [role and grant APIs](/docs/authz-api-index). --- ## Service and link API URL: https://docs.nullplatform.com/docs/api-service-index Services are the instances developers create from a service specification: a database, a bucket, a queue, or anything a platform team crafted. This section covers what happens to a service once it exists, from creating it to linking a scope to it and running actions on it. The definitions behind those services live in the [Service and link specifications API](/docs/service-specification-api-index). ## How the endpoints nest | Entity | Endpoints | Lives under | Defined by | | --- | --- | --- | --- | | Service | `/service`, `/service/{id}`, `/service/{id}/compare` | The NRN you create it at (`entity_nrn`) | A [service specification](/docs/service-specification-api-index) | | Service action | `/service/{serviceId}/action`, `/service/{serviceId}/action/{id}` | A service | The specification's [service action specifications](/docs/service-action-specification-api-index) | | Link | `/link`, `/link/{id}` | A scope and a service | One of the specification's [link specifications](/docs/link-specification-api-index) | | Link action | `/link/{linkId}/action`, `/link/{linkId}/action/{id}` | A link | The link specification's [link action specifications](/docs/link-action-specification-api-index) | ## A typical flow 1. **Create the service** with `POST /service`, sending its `specification_id` and the `entity_nrn` it belongs to. When the specification is packaged, the service binds to the package's default revision unless you send a `package_revision_id`. 2. **Follow its actions.** Creating, updating and deleting a service run the specification's actions. `GET /service/{serviceId}/action` lists them with their status, and `POST` runs a custom action. 3. **Link a scope** with `POST /link`, choosing one of the specification's link specifications. Links run their own actions, listed under `/link/{linkId}/action`. 4. **Move it to another version** by patching `desired_package_revision_id`. The next successful action makes that revision the one the service runs. ## Related guides - [Services](/docs/services/getting-started): what services are and how developers use them - [Manage and provision services](/docs/services/manage-services): create, link and operate services from the UI and the CLI ## Endpoints --- ## Service and link specifications API URL: https://docs.nullplatform.com/docs/api-service-specification-index A service specification defines a service: the attributes a developer fills in, the actions that run on it, and how scopes can link to it. Everything in this section is what a platform team crafts. The instances created from it live in the [Service and link API](/docs/services-api-index). ## How the endpoints nest Specifications form a tree. A service specification owns action specifications and link specifications, and each link specification owns its own action specifications. Every specification, link and action also keeps an append-only history of snapshots, which packages pin. ```text service_specification ├── action_specification service actions: create, update, delete, custom ├── link_specification │ └── action_specification link actions: link, unlink, custom └── snapshots one history per specification, link and action ``` | Entity | Endpoints | Nested under | Where to find it | | --- | --- | --- | --- | | Service specification | `/service_specification`, `/service_specification/{id}` | Nothing, it's the root | This page | | Link specifications of a specification | `/service_specification/{id}/link_specification` | Service specification | This page | | Snapshots | `/{service,link,action}_specification/{id}/snapshots`, plus `/{snapshotId}` | Each specification, link or action | This page, [Link specification](/docs/link-specification-api-index) and [Action specification](/docs/services-action-api-index) | | Package a specification | `/service_specification/{id}/package` | Service specification | This page, then the [Package API](/docs/package-api-index) | | Link specification | `/link_specification`, `/link_specification/{id}` | Service specification, through `specification_id` | [Link specification](/docs/link-specification-api-index) | | Service action specification | `/service_specification/{serviceSpecificationId}/action_specification`, plus `/{id}` | Service specification | [Service action specification](/docs/service-action-specification-api-index) | | Link action specification | `/link_specification/{linkSpecificationId}/action_specification`, plus `/{id}` | Link specification | [Link action specification](/docs/link-action-specification-api-index) | | Any action specification | `/action_specification`, `/action_specification/{id}`, `/action_context` | Reads across both kinds | [Action specification](/docs/services-action-api-index) | ## A typical flow 1. **Create the specification** with `POST /service_specification`: its name, who can see it (`visible_to`), and the JSON schema of its attributes. With `use_default_actions`, nullplatform creates the create, update and delete actions for you. 2. **Add its actions** under `/service_specification/{serviceSpecificationId}/action_specification`, one per operation the service supports beyond the defaults. 3. **Describe how scopes link to it** with `POST /link_specification`, then add the link's own actions under `/link_specification/{linkSpecificationId}/action_specification`. 4. **Version it.** Every change mints a snapshot. To freeze a set of snapshots together with the image or repository that implements them, package the specification with `POST /service_specification/{id}/package` and continue in the [Package API](/docs/package-api-index). Specification reads accept `package_revision_id` to return exactly what a package revision pins. ## Related guides - [Craft a service](/docs/services/craft-a-service/overview): the end-to-end guide to writing a specification - [Service specifications](/docs/services/craft-a-service/service-specs): the specification schema and its fields - [Link specifications](/docs/services/craft-a-service/link-specs): how a service exposes itself to scopes - [Action specifications](/docs/services/craft-a-service/service-actions): the actions a service runs - [Action specifications for links](/docs/services/craft-a-service/link-action-specs): the actions a link runs ## Endpoints --- ## Application settings URL: https://docs.nullplatform.com/docs/applications/application-settings Every application carries a `settings` object: **configuration values nullplatform keeps for the application**, such as its default branch and environment, where its build assets are stored, and which code repository it's linked to. Keeping them accurate lets nullplatform resolve the right defaults and find your build assets. You can read and update these values through the Application API. **All settings are optional**, so any field may be empty if it hasn't been set yet. ## What's in the settings object The `settings` object groups related configuration into three blocks. A fully populated object looks like this: ```json { "settings": { "defaults": { "branch": "main", "environment": "staging" }, "asset": { "docker_server": { "uri": "registry.example.com/acme-corp/billing-api" }, "ecr": { "uri": "123456789012.dkr.ecr.us-east-1.amazonaws.com/billing-api", "arn": "arn:aws:ecr:us-east-1:123456789012:repository/billing-api" } }, "code_repository": { "specification_id": "7f3c910e-8138-4d65-9d09-b97d89f2c509", "specification_slug": "github-configuration", "specification_name": "GitHub", "specification_icon": "github", "provider_id": "82eee42c-46c9-4bb7-a53d-c08b027817f3" } } } ``` ### Defaults **`defaults`** holds the values nullplatform falls back to when one isn't specified explicitly. | Field | Description | |---|---| | `branch` | Default branch name for the application. | | `environment` | Default environment name for the application. | ### Asset location **`asset`** records where the application's build assets are stored, so nullplatform knows where to find the images it deploys. | Field | Description | |---|---| | `docker_server.uri` | Location of the application's Docker server assets. | | `asset.ecr.uri` | Amazon ECR repository URI for the application's assets. | | `asset.ecr.arn` | Amazon ECR repository ARN for the application's assets. | ### Code repository **`code_repository`** describes the repository provider the application is linked to. It references the [provider specification](/docs/providers/overview) (`specification_id`, `specification_slug`, `specification_name`, `specification_icon`) and the provider instance (`provider_id`). ## Read the settings Send a [GET request](/docs/api/application-read) to the Application API. The `settings` object is included in the application's response body. ```bash np application get --id 123 ``` ```bash curl -L 'https://api.nullplatform.com/application/123' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ' ``` ## Update the settings Send a [PATCH request](/docs/api/application-update) to the Application API with the keys you want to change. For example, to set the default branch and environment: ```bash np application patch --id 123 --body '{ "settings": { "defaults": { "branch": "main", "environment": "staging" } } }' ``` ```bash curl -L -X PATCH 'https://api.nullplatform.com/application/123' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ' \ -d '{ "settings": { "defaults": { "branch": "main", "environment": "staging" } } }' ``` :::tip Updates use JSON Merge Patch Updates to `settings` follow [JSON Merge Patch (RFC 7396)](https://datatracker.ietf.org/doc/html/rfc7396). This means you only send the keys you want to change: - A key with a non-null value is **created or updated**. - A key set to `null` is **deleted**. - A key you omit is **left untouched**. ::: :::note Deleting a setting To clear a value, send the key with `null` rather than an empty string. For example, `{ "settings": { "defaults": { "branch": null } } }` removes the default branch while preserving every other setting. ::: ## Related resources - [Create a new application](creating-new-application.md): set up an application and its repository - [Update the name of your repository](update-repository-name.md): keep `repository_url` in sync after renaming --- ## Asset push authentication URL: https://docs.nullplatform.com/docs/applications/ci-cd/asset-push-authentication When your pipeline runs `np asset push`, the CLI needs credentials to push the image to your registry (or the file to S3, for Lambda functions). It takes them from the **asset repository provider** configured in nullplatform, so your pipeline never has to hold registry secrets or run `docker login`. How it authenticates depends on the provider: - **Docker registry** (Docker Hub, GitHub Container Registry, Azure Container Registry, Google Artifact Registry, Harbor, self-hosted): the CLI logs in with the username and password stored in the provider. - **AWS ECR**: the CLI gets an ECR login token with either an **IAM role** it assumes at push time using your CI tool's OpenID Connect (OIDC) token (recommended, no long-lived secrets anywhere), or a **static access key pair** stored in the provider. If you'd rather **log in from the pipeline yourself**, pass `--no-login` and the CLI pushes with the session you already opened. See [Logging in from the pipeline](#logging-in-from-the-pipeline). ## Docker registry If your provider is a Docker registry, there's nothing AWS-related to configure. Nullplatform builds the image URL from the provider's **Server URL** and **Registry Path**, and the CLI runs `docker login` (or `crane auth login` when Docker isn't installed) with the provider's username and password before pushing. Any AWS setting, in the provider or on the command line, is ignored. | Registry | Username | Password | | --- | --- | --- | | Azure Container Registry | The service principal's application id | The service principal's secret | | Google Artifact Registry / GCR | `_json_key_base64` | The service account key, base64-encoded | | Harbor, GitHub Container Registry, Docker Hub, self-hosted | A robot account or user | Its password or token | > ℹ️ **Note:** Rotating the registry credential is a change in the provider, not in your pipelines. Update the username or password there and every pipeline picks it up on its next push. ## ECR For an ECR provider, `np asset push` resolves AWS credentials in this order and stops at the first match: 1. `--aws-role-arn` on the command line. If you also pass `--aws-access-key-id` and `--aws-secret-access-key`, those keys are used to assume the role. 2. `--aws-access-key-id` and `--aws-secret-access-key` on the command line, used as they are. 3. The **Role ARN** configured in the provider. 4. The **Access Key** and **Secret Key** configured in the provider. If none applies, the command fails with a message listing what to configure. The region comes from `--aws-region`, then the provider's region, then `AWS_REGION`. For Docker images the CLI can also read it from the ECR registry hostname, so you rarely need to set it. > ℹ️ **Note:** Once a role is configured, the provider's keys are ignored. This lets you add the role while the keys are still there, confirm your pipelines work, and then remove the keys. If something goes wrong, remove the role and the pipelines fall back to the keys on their next run. ### How the role is assumed The CLI assumes the role with whatever identity the runner can present, trying these in order: 1. **An OIDC token file.** If `AWS_WEB_IDENTITY_TOKEN_FILE` points to a file with an OIDC token, the CLI exchanges it for the role's credentials. This is the same variable the AWS CLI and SDKs use, so it works with any CI tool that issues OIDC tokens. 2. **GitHub Actions.** If the job has the `id-token: write` permission, the CLI requests the job's OIDC token from GitHub. Nothing to configure in the workflow. 3. **Ambient AWS credentials.** Otherwise the CLI uses whatever credentials the runner already has (an EC2 instance profile, an EKS pod identity, or AWS keys in the environment). In every case, the role's trust policy has to allow the identity the runner presents. The steps below show what that means for each tool. ## 1. Configure the role in the provider 1. Create an IAM role in the AWS account that owns the registry. Give it `ecr:GetAuthorizationToken` plus push permissions on your repositories (`ecr:BatchCheckLayerAvailability`, `ecr:InitiateLayerUpload`, `ecr:UploadLayerPart`, `ecr:CompleteLayerUpload`, `ecr:PutImage`, `ecr:BatchGetImage`). If you push Lambda functions, add `s3:PutObject` on the assets bucket. 2. Set its trust policy according to your CI tool (see below). 3. In nullplatform, go to **Platform settings > Asset repository**, open your ECR configuration, and set **Role ARN** under **CI/CD Image Push Configuration**. You can leave the existing **Access Key** and **Secret Key** in place for now. > 💡 **Tip:** If you manage the provider with OpenTofu, the `nullplatform/asset/ecr` module exposes the same setting as the `build_workflow_role_arn` variable. ## 2. Set up your CI tool ### GitHub Actions Give the job the `id-token: write` permission. The CLI takes care of the rest: ```yaml name: CI/CD pipeline nullplatform env: NULLPLATFORM_API_KEY: ${{ secrets.NULLPLATFORM_API_KEY }} on: push: branches: - main permissions: id-token: write contents: read jobs: build: runs-on: ubuntu-latest steps: - name: Install nullplatform CLI run: curl https://cli.nullplatform.com/install.sh | sh - name: Check out code uses: actions/checkout@v4 - name: Create build on nullplatform run: np build start - name: Build Docker image run: docker build -t my-app . - name: Push Docker image run: np asset push --type docker-image --source my-app - name: Set the build as successful or failed if: ${{ always() }} run: np build update --status ${{ contains(fromJSON('["failure", "cancelled"]'), job.status) && 'failed' || 'successful' }} ``` Trust policy: allow the GitHub OIDC provider (`token.actions.githubusercontent.com`) with the audience `sts.amazonaws.com`, and restrict the `sub` claim to your organization or repositories. It's the same trust policy `aws-actions/configure-aws-credentials` uses, so you can reuse an existing role. ### GitLab CI Request an ID token with the `sts.amazonaws.com` audience, write it to a file, and point `AWS_WEB_IDENTITY_TOKEN_FILE` at it before running the CLI: ```yaml build-finalize: stage: build id_tokens: AWS_ID_TOKEN: aud: sts.amazonaws.com before_script: - apk add curl crane - curl https://cli.nullplatform.com/install.sh | sh - echo "$AWS_ID_TOKEN" > /tmp/aws-web-identity-token - export AWS_WEB_IDENTITY_TOKEN_FILE=/tmp/aws-web-identity-token script: - np asset push --type docker-image --source image.tar - np build update --status successful ``` Trust policy: allow the GitLab OIDC provider (`https://gitlab.com`, or your self-managed instance's URL) with the audience `sts.amazonaws.com`, and restrict the `sub` claim to your project or branch. ### Other CI tools with OIDC Any tool that issues an OIDC token works like GitLab: write the token to a file and export `AWS_WEB_IDENTITY_TOKEN_FILE`. For example, Bitbucket Pipelines exposes the token as `BITBUCKET_STEP_OIDC_TOKEN` when the step has `oidc: true`, and CircleCI as `CIRCLE_OIDC_TOKEN_V2`: ```bash echo "$BITBUCKET_STEP_OIDC_TOKEN" > /tmp/aws-web-identity-token export AWS_WEB_IDENTITY_TOKEN_FILE=/tmp/aws-web-identity-token np asset push --type docker-image --source image.tar ``` Trust policy: allow that tool's OIDC provider with the audience it uses for the token. > 💡 **Tip:** Don't set `AWS_ROLE_ARN` next to `AWS_WEB_IDENTITY_TOKEN_FILE`. The CLI already knows which role to assume from the provider. If both are set, it treats them as the runner's own identity (for example, an EKS pod identity) and uses those credentials to assume the provider role. ### Runners that already have AWS credentials If the runner already has AWS credentials (an EC2 instance profile, an EKS pod identity, or a Jenkins agent with AWS keys in its environment), there's nothing to add to the pipeline. The role's trust policy has to allow that principal. ### Runners without OIDC or AWS credentials Keep using the provider's access keys and don't configure a role. Other pipelines in the organization can still move to the role. If you'd rather not store keys in the provider, pass `--aws-role-arn` together with `--aws-access-key-id` and `--aws-secret-access-key` to assume the role with a dedicated user's keys. ## 3. Remove the keys Once your pipelines push successfully with the role, remove **Access Key** and **Secret Key** from the provider and retire the IAM user. Nothing changes in the pipelines: the CLI was already ignoring the keys. If your workflow used to obtain AWS credentials on its own, you can also drop those steps (such as `aws-actions/configure-aws-credentials` and `aws-actions/amazon-ecr-login`) and the `--no-login` flag from `np asset push`. Make sure the CLI is up to date on your runners: the install script always installs the latest version. ## Logging in from the pipeline If you'd rather keep registry access under your pipeline's control, log in with your own tooling and pass `--no-login`. The CLI then skips credential resolution entirely: it doesn't read the provider's credentials, call AWS, or run `docker login`. It just tags and pushes the image with the session your pipeline opened, and registers the asset in the build. `--no-login` applies to Docker images only. Lambda uploads to S3 always use the AWS credentials described above. This works with any registry and any CI tool. For example, with a plain `docker login`: ```yaml - name: Log in to the registry run: echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login myregistry.azurecr.io -u "${{ secrets.REGISTRY_USERNAME }}" --password-stdin - name: Push Docker image run: np asset push --type docker-image --source my-app --no-login ``` Or with the AWS actions for ECR: ```yaml - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::123456789012:role/ci-image-pusher aws-region: us-east-1 - name: Log in to Amazon ECR uses: aws-actions/amazon-ecr-login@v2 - name: Push Docker image run: np asset push --type docker-image --source my-app --no-login ``` The trade-off is that every pipeline owns its registry credentials and the steps to obtain them, instead of a single place in nullplatform. --- ## Build and push assets for arm64 URL: https://docs.nullplatform.com/docs/applications/ci-cd/build-arm This page describes how nullplatform supports building Docker assets for the ARM64 platform. By default, Docker builds for the x86_64 platform; however, cloud providers are increasingly offering instances that use ARM platforms. Our CI/CD tools, such as GitHub Actions, GitLab Pipelines, and Azure Pipelines, provide flexibility, allowing for default behavior and the use of custom build commands, arguments, and options. :::warning Partial Support Currently, nullplatform supports the ARM64 platform only for AWS. ::: ## Set up Docker Buildx Docker Buildx is the utility used to build images for different target platforms. It can be set up manually using the create command: ```shell docker buildx create --use ``` If using GitHub Actions, you can import the following actions to set it up: ```yaml - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 ``` ## Build and push Docker assets for arm64 Once `docker buildx` is set up, you need to change your build and push command to use it: ```shell docker buildx build --platform linux/arm64 -t main --load . # create a docker image for arm64 and load it as main:latest np asset push --type docker-image --source main # use nullplatform cli to push the asset. ``` The nullplatform CLI infers the target platform from the Docker image's manifest. ## Build and push Docker assets for multiple platforms You can build a single application for multiple platforms by having multiple build and push steps, one for each target platform: ```shell docker buildx build --platform linux/amd64 -t x86_64 --load . docker buildx build --platform linux/arm64 -t arm64 --load . np asset push --type docker-image --source x86_64 --name x86_64 np asset push --type docker-image --source arm64 --name arm64 ``` :::tip Push Multiple Assets When pushing multiple assets, you need to specify the `--name` argument to the nullplatform CLI. ::: ### Examples #### Docker image asset for arm64 ```yaml name: ci-nullplatform env: NULLPLATFORM_API_KEY: ${{ secrets.NULLPLATFORM_API_KEY }} on: push: branches: - main - master permissions: id-token: write contents: read packages: read jobs: build: runs-on: ubuntu-latest steps: - name: Install nullplatform cli run: curl https://cli.nullplatform.com/install.sh | sh - name: Checkout code uses: actions/checkout@v4 - name: Start nullplatform CI run: np build start - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build arm64 asset run: docker buildx build --platform linux/arm64 -t arm64 --load . - name: Push arm64 asset run: np asset push --type docker-image --source arm64 - name: End nullplatform CI if: ${{ always() }} run: np build update --status ${{ contains(fromJSON('["failure", "cancelled"]'), job.status) && 'failed' || 'successful' }} ``` #### Docker image asset for multiple platforms ```yaml name: ci-nullplatform env: NULLPLATFORM_API_KEY: ${{ secrets.NULLPLATFORM_API_KEY }} on: push: branches: - main - master permissions: id-token: write contents: read packages: read jobs: build: runs-on: ubuntu-latest steps: - name: Install nullplatform cli run: curl https://cli.nullplatform.com/install.sh | sh - name: Checkout code uses: actions/checkout@v4 - name: Start nullplatform CI run: np build start - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build x86_64 asset run: docker buildx build --platform linux/amd64 -t x86_64 --load . - name: Build arm64 asset run: docker buildx build --platform linux/arm64 -t arm64 --load . - name: Push x86_64 asset run: np asset push --type docker-image --source x86_64 --name x86_64 - name: Push arm64 asset run: np asset push --type docker-image --source arm64 --name arm64 - name: End nullplatform CI if: ${{ always() }} run: np build update --status ${{ contains(fromJSON('["failure", "cancelled"]'), job.status) && 'failed' || 'successful' }} ``` --- ## Build and push Next.JS assets URL: https://docs.nullplatform.com/docs/applications/ci-cd/build-next-js In this section, we explain how nullplatform supports the process of building and pushing Next.JS applications. Most Next.JS applications are packaged into `.js` and `.css` files, which are then distributed as static assets. As such, any runtime environment variables must be included in the packaged application during the CI pipeline. :::warning Partial Support The nullplatform parameters services cannot be used for Next.JS applications, as they are inserted at runtime after the application has been packaged and stored as static files. ::: ## Building for multiple environments If your application requires different configurations for various environments (e.g., development, staging, production), you will need to create a separate build for each environment and configure your CI pipeline to inject the correct environment variables into each build. There are two primary strategies to achieve this: branch per environment or multi-asset builds. ### Branch per environment In this approach, you should create a separate branch for each environment you intend to support. When a commit is pushed to any of these branches, your CI pipeline must package the application with the corresponding environment configurations. Common practices include using the master or main branch for production builds, and stage or dev branches for pre-production environments. By following this strategy, you will have a distinct build for each environment. We recommend implementing a versioning strategy that clearly identifies the target environment of each release. :::tip Making Your Builds Distinguishable in Nullplatform You can specify the `--name` argument when using the `np asset push` command in the nullplatform CLI. By assigning a unique asset name for each environment, nullplatform will help prevent deploying the wrong asset to an incorrect environment. ::: ### Multi-asset builds An alternative approach is to maintain a single branch where the CI pipeline builds an asset for each environment. In this scenario, you will have a single build, and the appropriate asset will be selected during the deployment process. :::tip Pushing Multiple Assets When pushing multiple assets, you must specify the `--name` argument in the `np asset push` command of the nullplatform CLI. ::: --- ## CI-CD pipelines URL: https://docs.nullplatform.com/docs/applications/ci-cd/ ## What is CI/CD? **Continuous Integration and Continuous Delivery (CI/CD)** automates, optimizes, and secures the development lifecycle, leading to faster, more reliable releases. CI/CD is typically structured as a pipeline with these key stages: - **Continuous Integration (CI):** Developers frequently merge changes into a shared branch, triggering automated builds and tests to catch conflicts early. - **Continuous Delivery (CD):** Code that passes tests is packaged and prepared for deployment, ensuring it is always deployment-ready. On top of this, you can have **continuous deployment** where the software is automatically deployed to production or to your testing environment, depending on your configuration. ## Configuring nullplatform in your CI/CD To leverage nullplatform in your development lifecycle, you need to configure your CI/CD pipeline to notify nullplatform about new builds, releases, and assets. With the [nullplatform's CLI](/docs/cli), you can seamlessly integrate with any CI/CD tool, including GitHub Actions, GitLab, Jenkins, and Azure Pipelines. ### Create or import the application Before setting up your CI/CD pipeline, you need to [create or import your application](/docs/applications/creating-new-application.md) in nullplatform. This is required because communicating with nullplatform requires credentials, which are injected into your CI/CD pipeline when the application is created or imported. ### Create builds and assets This is the first touchpoint with nullplatform in the development lifecycle. It starts by notifying that your application is being built and ends with the production of a set of assets, which will eventually be delivered to runtime environments. Here's an example of how we'd modify a CI/CD workflow to achieve this: ```yaml ############################################################################ ### Example of how to modify a workflow that builds a Docker image ############################################################################ # Install the np cli binary that will take care of communication with nullplatform API - name: Install nullplatform's CLI run: curl https://cli.nullplatform.com/install.sh | sh # Check out your code - name: Check out the code uses: actions/checkout@v4 # Create the build with a 'creating' status, # This will be automatically reflected on nullplatform's UI. - name: Create build on nullplatform run: np build start # Build and test - name: Build and test run: docker build -t main . # Add the asset to the build - name: Add the asset to the build run: np asset push --type docker-image --source main # Update the build to its final status - name: Set the build as successful or failed if: ${{ always() }} run: np build update --status ${{ contains(fromJSON('["failure", "cancelled"]'), job.status) && 'failed' || 'successful' }} ``` Here's how the workflow works: 1. **Install the CLI**: This utility handles communication with nullplatform's API. 2. **Start a build:** We used `np build start` to notify nullplatform that a build has begun. This is reflected in the **Builds** section of the UI, where your build will appear with a `Creating` status. 3. **Upload assets:** We used `np asset push` to (a) upload the assets (e.g., Docker images, Lambda functions) to the designated repository, and (b) add the asset metadata to the build. :::note The image goes directly into your image repository. It is not proxied by nullplatform's servers. ::: 4. **Update build status:** We used `np build update` to inform nullplatform whether the build succeeded or failed. #### How do I handle failed builds? As shown in the example above, you can use `np build update --status failed` to notify nullplatform of a failed build. Nullplatform will mark the build as failed and prevent it from being deployed. #### How does the CLI authenticate against my asset repository? Notice that the workflow above never logs into the registry. `np asset push` takes the credentials from the asset repository provider configured in nullplatform: a Docker registry's username and password, or, for ECR, an IAM role or access key pair. Your pipeline doesn't have to hold registry secrets. See [Asset push authentication](/docs/applications/ci-cd/asset-push-authentication) for how it works, how to set up each CI tool, and how to log in from the pipeline yourself with `--no-login`. ## Continuous Deployment (CD) To implement CD on nullplatform, go to your application's **Scopes** settings, enable **Continuous Deployment**, and list the branches that should trigger an automatic release into that scope. Example CD configuration: - **Branch**: `main` - **Scope**: `staging` - **Action**: Automatically deploy builds from `main` to the staging scope. #### What happens when I configure CD? When you configure CD, nullplatform automatically handles the following: 1. **Generates a new release**, automatically incrementing the semantic version based on the latest release. 2. **Executes the deployment** on the scopes marked for CD. Note that this will be an "all-in" deployment, meaning there will be no traffic-switching phase. ### Matching branches with regular expressions The **Branches** field accepts regular expressions, not just literal branch names. Each entry you add is a pattern, and a successful build triggers a deployment when its branch matches at least one of them. This way a single scope can pick up a whole family of branches without you listing them one by one. Three rules govern how patterns are evaluated: - Patterns must match the **entire** branch name. `feature` won't match `feature/login`, but `feature/.*` will. - Matching is **case-insensitive**, so `Main` and `main` are treated the same. - A scope can hold as many patterns as you need. One match is enough to trigger the deployment. Here are some common patterns: | Pattern | Matches | Doesn't match | | --- | --- | --- | | `main` | `main` | `main-fix`, `feature/main` | | `release/.*` | `release/1.2.0`, `release/hotfix` | `release`, `releases/1.2.0` | | `feature/.*` | `feature/login`, `feature/team/login` | `feat/login` | | `main\|develop` | `main`, `develop` | `development` | | `hotfix-\d+` | `hotfix-42` | `hotfix-abc` | | `.*` | Every branch | Nothing | A plain branch name is already a valid pattern: `main` is a regular expression that matches only itself. You only need the extra syntax when a scope should cover more than one branch. :::caution If a pattern isn't a valid regular expression, nullplatform can't evaluate it and the scope is skipped, so no deployment is triggered. Double-check special characters such as `[`, `(`, and `\` when writing patterns by hand. ::: ## Adding catalog metadata to the CI/CD process You can enrich your builds with **catalog metadata** to enforce policies or improve decision-making. For example, you can store test coverage data and prevent deployments if coverage is too low: ```yaml np metadata create --entity build --data '{"linter": { "code_smells": $CODE_SMELLS }, "coverage": { "percentage": $COVERAGE } }' ``` By leveraging metadata, you can enforce quality control before pushing builds to production. For more information, see our [Entity catalog documentation](/docs/catalog/getting-started.md). ## Adding multiple assets to a build Nullplatform supports builds that produce multiple assets. For example, you can build both a Docker image and a Lambda function in the same workflow. Example multi-asset workflow: ```yaml - name: Start Build run: np build start - name: Build Docker Image run: np asset push --type docker-image --source my-app - name: Build Lambda Function run: np asset push --type lambda --zip my-lambda.zip - name: Update Build Status run: np build update --status success ``` ## Best practices To streamline your CI/CD experience in nullplatform, consider the following best practices: - #### Use templates We provide [pre-built templates](/docs/api/technology-template-list) for common technologies. These templates include ready-to-use CI/CD workflows, so you don't have to start from scratch. - #### Leverage catalog metadata Use metadata to enforce quality and security policies. For example: - Require a minimum code coverage (e.g., 80%) for production deployments. - Block deployments if critical vulnerabilities are detected. See our [Catalog entity docs](/docs/catalog/getting-started) for more info. - #### Keep pipelines simple Once your application is on nullplatform, your pipelines don’t need to handle low-level infrastructure interactions for deployment purposes. Simplify your pipelines by focusing only on building and testing code. - #### Choose a branching strategy Pick a branching strategy that works for your team. For example: - Git flow: Use `main` for production and `develop` for staging. - Trunk-based development: Use a single branch (`main`) for all deployments. ## What's next? Check out our [full CI/CD workflow examples](/docs/applications/ci-cd/full-ci-cd-examples) using different CI tools integrated with nullplatform. --- ## Examples: CI/CD workflows URL: https://docs.nullplatform.com/docs/applications/ci-cd/full-ci-cd-examples This section offers CI/CD workflow examples with various CI tools integrated with nullplatform. They cover building and deploying Docker images, pushing assets, and updating build status with the **nullplatform CLI**. ## GitHub Actions ### Example: one build, one image per application ```yaml name: CI/CD pipeline nullplatform env: NULLPLATFORM_API_KEY: ${{ secrets.NULLPLATFORM_API_KEY }} on: push: branches: - main permissions: id-token: write contents: read packages: read jobs: build: runs-on: ubuntu-latest steps: - name: Install nullplatform CLI run: curl https://cli.nullplatform.com/install.sh | sh - name: Check out code uses: actions/checkout@v4 - name: Create build on nullplatform run: np build start - name: Run code analysis run: ./code_analysis.sh - name: Build Docker image run: docker build -t my-app . - name: Push Docker image run: np asset push --type docker-image --source my-app - name: Add metadata run: np metadata create --entity build --data "{\"linter\": {\"code_smells\": $CODE_SMELLS}, \"coverage\": {\"percentage\": $COVERAGE}}" - name: Set the build as successful or failed if: ${{ always() }} run: np build update --status ${{ contains(fromJSON('["failure", "cancelled"]'), job.status) && 'failed' || 'successful' }} ``` ## Azure DevOps ### Example: one build, one image per application ```yaml trigger: - main pool: Default variables: - group: nullplatform-${{ variables['Build.DefinitionName'] }} jobs: - job: build steps: - script: curl https://cli.nullplatform.com/install.sh | sh displayName: 'Install nullplatform CLI' - script: np build start displayName: 'Create build on nullplatform' env: NULLPLATFORM_API_KEY: $(NULLPLATFORM_API_KEY) - script: docker build -t main . displayName: 'Build and test asset' - script: np asset push --type docker-image --source main displayName: 'Add the asset to the build' env: NULLPLATFORM_API_KEY: $(NULLPLATFORM_API_KEY) - script: np build update --status $(if [ "$(Agent.JobStatus)" = "Failed" ] || [ "$(Agent.JobStatus)" = "Canceled" ]; then echo "failed"; else echo "successful"; fi) displayName: 'Set the build as successful or failed' condition: always() env: NULLPLATFORM_API_KEY: $(NULLPLATFORM_API_KEY) ``` ## Jenkins ### Example: one build, one image per application ```groovy pipeline { agent any environment { NULLPLATFORM_API_KEY = credentials('NULLPLATFORM_API_KEY') } parameters { string(name: 'GIT_REPOSITORY', description: 'Repository to build') string(name: 'BRANCH', description: 'Branch to build', defaultValue: 'master') } stages { stage('Prepare Workspace') { steps { cleanWs() } } stage('Clone repository') { steps { git url: "${GIT_REPOSITORY}.git", branch: "$BRANCH", credentialsId: 'gitlab-crypto-kong-user' } } stage('Prepare last commit details') { steps { script { env.COMMIT_SHA = sh(script: 'git rev-parse HEAD', returnStdout: true).trim() env.COMMIT_PERMALINK = "${GIT_REPOSITORY}/commit/$COMMIT_SHA" env.COMMIT_DESCRIPTION = sh(script: "git log -1 --pretty=%B $COMMIT_SHA", returnStdout: true).trim() } } } stage('Start nullplatform CI') { steps { sh "np build start --repository $GIT_REPOSITORY --branch $BRANCH --commit-sha $COMMIT_SHA --commit-permalink $COMMIT_PERMALINK --description '$COMMIT_DESCRIPTION'" } } stage('Build Docker Image') { steps { script { docker.build('main:latest') } } } stage('Push asset') { steps { sh "np asset push --type docker-image --source main --repository $GIT_REPOSITORY --branch $BRANCH --commit-sha $COMMIT_SHA" } } } post { always { script { def jobStatus = currentBuild.currentResult def status = jobStatus == 'FAILURE' || jobStatus == 'ABORTED' ? 'failed' : 'successful' sh "np build update --status ${status} --repository $GIT_REPOSITORY --branch $BRANCH --commit-sha $COMMIT_SHA" } } } } ``` ## Gitlab ### Example: one build, one image per application ```yaml default: image: alpine:latest .setup-np-cli: &setup-np-cli - apk add curl - curl https://cli.nullplatform.com/install.sh | sh - source /root/.bashrc build-start: stage: build before_script: - *setup-np-cli script: - np build start except: - tags build-image: stage: build image: name: gcr.io/kaniko-project/executor:debug entrypoint: [""] script: - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --no-push --destination foo --tar-path image.tar artifacts: paths: - image.tar except: - tags build-finalize: stage: build needs: - build-start - build-image before_script: - *setup-np-cli - apk add crane script: - np asset push --type docker-image --source image.tar - np build update --status successful except: - tags build-failed: stage: .post before_script: - *setup-np-cli script: - np build update --status=failed except: - tags when: on_failure ``` ### Example: one repository, one application, one build, multiple assets (debug and live modes) ```yaml build: stage: build image: name: public.ecr.aws/nullplatform/np-cli:kaniko pull_policy: always script: - /np build start - modes="debug,live" - |+ IFS=',' for mode in $modes; do /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --no-push --tarPath $mode.tar --destination foo:1 --build-arg=MODE=$mode /np asset push --type docker-image --source $mode.tar --name $mode done - /np build update --status=successful except: - tags build-failed: stage: build image: name: public.ecr.aws/nullplatform/np-cli:kaniko pull_policy: always script: - /np build update --status=failed except: - tags when: on_failure ``` ### Example: one repository, multiple apps, one build, one asset per application (monorepo) ```yaml build: stage: build image: name: public.ecr.aws/nullplatform/np-cli:kaniko pull_policy: always script: - changed_applications=$(git diff --name-only HEAD^ HEAD -- apps | cut -d / -f 1,2 | uniq | paste -sd "," -) - |+ IFS=',' for app in $changed_applications; do /np build start --path $app /kaniko/executor --context $CI_PROJECT_DIR/$app --dockerfile $CI_PROJECT_DIR/$app/Dockerfile --no-push --tarPath $app.tar --destination foo:1 /np asset push --type docker-image --source $app.tar --path $app /np build update --status=successful --path $app done except: - tags ``` --- ## Build and push assets using Make URL: https://docs.nullplatform.com/docs/applications/ci-cd/makefile-build-push This page describes how nullplatform uses [GNU Make](https://www.gnu.org/software/make/manual/html_node/index.html) and Makefiles for asset building and pushing to repositories. Our GitHub Actions, GitLab Pipelines, Azure Pipelines, and more provide flexibility, allowing for default behavior, customization of Makefiles, and the use of custom build commands, arguments, and options. ## Behavior When no customizations are provided, the `make` command is executed in your CI/CD pipeline using a Makefile. The key steps involved are: - **Makefile Discovery**: Our building process checks if a Makefile (`makefile` or `Makefile`) exists in the *build working directory* you provided. If not found, a default Makefile is used. - **Default Rules**: If a Makefile is found, it looks for a default build rule named `build` and an upload-to-repository rule named `push`. If found, it executes these rules. - **Example Command**: If no customizations are provided, the command executed is: `make -C BUILD_WORKING_DIRECTORY=. build` and `make -C BUILD_WORKING_DIRECTORY=. push`. :::tip Execute make command locally You can run these commands on your local machine to test your own Makefiles and simulate how the CI/CD pipeline builds your application. This can help you quickly identify errors. ::: ## Default Makefile Behavior When no customizations are provided, the default Makefile is used. The key steps involved are: - **Makefile Discovery**: Our building process checks if a Makefile (`makefile` or `Makefile`) exists in the *build working directory* you provided. - **Default Rule**: If a Makefile is found, it looks for a default build rule named `build` and an upload-to-repository rule named `push`. If found, it executes these rules. - **Example Command**: If no customizations are provided, the command executed is: `make -C BUILD_WORKING_DIRECTORY=. build` and `make -C BUILD_WORKING_DIRECTORY=. push`. You can execute these commands on your machine to test your Makefiles. ### Customizing Arguments and Options You can change the *arguments* and *options* to customize the asset build or push while still using the default `make` command. For example: - **Custom Arguments and Options**: By specifying different values for *arguments* and *options*, you can influence how the default Makefile's `build` rule is executed. For example, you can customize a Docker build argument by providing a `BUILD_ARGS` option. - **Example Command**: If custom arguments and options are provided, the command executed might look like this: `make -C BUILD_WORKING_DIRECTORY=. BUILD_ARGS=--build-args=TOKEN=secret build` or `make -C BUILD_WORKING_DIRECTORY=. build custom-argument custom-option`. ### Custom Makefile If you have a specific Makefile structure that depends on the *build working directory* and *asset working directory*, you can customize the Makefile based on these directories: - **Makefile Location**: The GitHub Action checks for a Makefile in the *build working directory* provided. If found, it uses this Makefile. - **Fallback to Defaults**: If no Makefile is found in the specified directory, it falls back to using the default Makefile. ## Advanced Customization For advanced customization, you can provide a custom command, custom arguments, and options to execute a completely customized build process. This scenario allows you to specify the exact command you want to run: - **Custom Command**: By specifying a custom command, you can execute a non-Makefile-based build process. - **Custom Arguments and Options**: Additional arguments and options can be provided to fine-tune the custom command execution. - **Example Command**: If a custom command is provided, the CI/CD pipeline runs it as-is, along with any specified arguments and options. For instance: `custom-command custom-argument custom-option`. Understanding how Makefiles interact with the CI/CD pipeline is essential for effective asset building. By following the outlined scenarios and including recommended customizations, you can streamline the build process for your project's assets, whether using the default Makefile, custom Makefiles, or entirely custom build commands. ### Variables in the Makefile To facilitate the `build` and `push` rules, several variables are used in the Makefile: **BUILD_WORKING_DIRECTORY**: Specifies the directory where the build process takes place. **ASSET_WORKING_DIRECTORY**: Denotes the directory containing source code and assets for the build process. **ASSET_OUTPUT_DIRECTORY**: Specifies the directory where the built assets will be output. **ASSET_NAME**: The name of the asset being built or pushed. **ASSET_TYPE**: The type of asset, which determines which rules to apply. **ASSET_TARGET_URL**: The URL or location where the asset is pushed or deployed. ### Recommended Rules While the minimum required rules for build and push actions have been outlined, it is recommended to include the following rules in your custom Makefiles: **setup**: This rule sets up the necessary environment or dependencies for the asset build. **test**: Use this rule to run tests related to the asset. **compile**: This rule is responsible for compiling or preparing the asset for packaging. **package**: The package rule bundles the asset into a deployable format. **push**: The push rule pushes the packaged asset to a target location, which can be a Docker registry or an S3 bucket, depending on the asset type. ### Examples #### Node.js Serverless Asset ```make ASSET_FILE=$(ASSET_NAME).zip .PHONY: all clean build setup test compile package push all: clean build push build: setup test compile package clean: rm -r $(ASSET_OUTPUT_DIRECTORY) setup: mkdir -p $(ASSET_OUTPUT_DIRECTORY) cp $(ASSET_WORKING_DIRECTORY)/package.json $(ASSET_OUTPUT_DIRECTORY) npm --prefix $(ASSET_OUTPUT_DIRECTORY) install --omit=dev test: npm --prefix $(ASSET_WORKING_DIRECTORY) install --only=dev npm --prefix $(ASSET_WORKING_DIRECTORY) run test compile: cp $(ASSET_WORKING_DIRECTORY)/*.js $(ASSET_OUTPUT_DIRECTORY) package: cd $(ASSET_OUTPUT_DIRECTORY) && zip -r $(ASSET_FILE) . push: aws s3 cp $(ASSET_OUTPUT_DIRECTORY)/$(ASSET_FILE) $(ASSET_TARGET_URL) ``` #### Docker image asset ```make .PHONY: all clean build setup test compile package push all: clean build push build: setup test compile package setup: docker build -t $(ASSET_NAME)-test --target test -f $(ASSET_WORKING_DIRECTORY)/Dockerfile . test: docker run -it --rm --name $(ASSET_NAME)-test-run $(ASSET_NAME)-test compile: package: docker build -t $(ASSET_NAME) --target build -f $(ASSET_WORKING_DIRECTORY)/Dockerfile . push: docker tag $(ASSET_NAME):latest $(ASSET_TARGET_URL) docker push $(ASSET_TARGET_URL) ``` --- ## Applications URL: https://docs.nullplatform.com/docs/applications/creating-new-application An application is a logical unit typically linked to a repository or, in the case of a mono-repo, to a folder within it. Applications are organized into namespaces, which often represent teams or functional areas (e.g., checkout, billing). ## Create a new application Whether you are starting a new application from scratch or migrating an existing application to nullplatform, you need to **create a new application**. To do this: 1. Go to your namespace's home page and click **+ Create application**. 2. Give your application a descriptive name. 3. Choose to create a new repository or import an existing one. You can either create a new repository from a template or import an existing one. In both cases, nullplatform integrates with your repository provider (GitHub, GitLab, Azure DevOps, etc.) to: - **For new repositories:** Create the repository using the selected template. - **For new and imported repositories:** Add an [API key](/docs/authorization/api-keys) for CI workflows to push assets, builds, releases, and metadata. ## Naming conventions When you create a new application from scratch, we automatically generate a repository for you using this naming convention: - `https://my-git-provider/my-organization/${namespace-name}-${application-slug}`. > Example: > > If your organization is **acme-corp** and you're setting up a **transactions api** application within the **wallet** namespace, the repository URL would look like this: `https://github.com/acme-corp/wallet-transactions-api`. #### What is a slug? A **slug** is a simplified version of a name. Since names can contain special characters (e.g., apostrophes) that may cause issues in certain contexts, we automatically convert them into a standardized format. > For example, an application named *John's app!* would be mapped to `johns-app`. :::info Renaming an entity won't change its slug - Once created, **an application's slug does not change**, even if you rename the application. - The same applies to other entities like **organization**, **account**, **namespaces**, or **scopes**. The slug is used in URLs, repository names, and integrations, so changing it could break connections with existing applications and services. ::: ### Customizing repository naming patterns You can define custom naming patterns for repositories to better align with your organization's conventions. To learn how, see [Setting custom naming patterns](/docs/applications/naming-patterns). ## Continuous Integration Any application created from a nullplatform template is already set up with a working CI pipeline that will build, test, and upload the assets to the right location for deployment (e.g., Docker images in case of server instances, zip packages for serverless functions). While we recommend not altering the CI pipeline and maintaining it as simple as possible, you can change it in any way you want as long as you keep the steps that communicate with nullplatform and with the cloud provider. ## Importing an existing repository If you're migrating an existing codebase, you'll want to create a new application by importing an existing repository. In this case, you will enter the repository name, and we will automatically: 1. Create the application in nullplatform 2. Inject the required secrets into your repository so you can set up the CI pipeline. :::tip Create a branch for your new CI pipeline When migrating an application, consider creating a new branch where you can test your new CI pipeline without disturbing the main branch where production code resides. ::: Once the application has been created on nullplatform, you will have to: 1. Adapt your CI pipeline to deliver the assets to the right location. You can read about nullplatform CI/CD integration [here](/docs/applications/ci-cd). 2. (Server-based Applications) Make sure the application is listening on port 8080 and that it is returning a `200 OK` status code for the health url (the default is `/health`). Once you start committing to your application, you'll be able to see the builds in the Build section of your application's dashboard. --- ## Delete applications URL: https://docs.nullplatform.com/docs/applications/delete-application You can delete applications from your organization using the nullplatform UI, CLI, or API. :::info You must have an [**Admin** or **Developer** role](/docs/authorization/roles) to delete applications. ::: You can delete an application directly from the **Namespace** view. Open the options menu **(⋮)** next to the application you want to remove and select **Delete**. [Patch the application](/docs/api/application-update)'s status to **`deleting`**. ```bash np application patch \ --id \ --body '{ "status": "deleting" }' ``` [Patch the application](/docs/api/application-update)'s status to **`deleting`**. ```bash curl -L -X PATCH 'https://api.nullplatform.com/application/:id' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ' \ -d '{ "status": "deleting" }' ``` ## Troubleshooting: I can't delete my application You can't delete an application if it still owns any services or links, or has scopes associated with it. To resolve this, you'll need to: 1. Transfer ownership of links and services. 2. Update where the service can be linked. 3. Remove scopes associated with the application. ### Transfer ownership of links and services #### 1. Identify what services or links the application owns First, check what services or links the application owns: 1. Go to **Development > Services**. 2. Select the **Owned by this application** tab to see the services you need to transfer. Use the [service list](/docs/api/service-list) or [link list](/docs/api/link-list) commands with the application's `entity_nrn` as a query parameter. > **Tip:** For services, the `nrn` (source) must match the `entity_nrn`. ```bash np service list --nrn organization=1:account=1:namespace=1:application=1 --entity_nrn organization=1:account=1:namespace=1:application=1 ``` ```bash np link list --nrn organization=1:account=1:namespace=1:application=1 ``` Use the [service list](/docs/api/service-list) or [link list](/docs/api/link-list) commands with the application's `entity_nrn` as a query parameter. > **Tip:** For services, the `nrn` (source) must match the `entity_nrn`. ```bash curl -L 'https://api.nullplatform.com/service?nrn=organization=1:account=1:namespace=1:application=1&entity_nrn=organization=1:account=1:namespace=1:application=1' \ -H 'Authorization: Bearer ' ``` ```bash curl -L 'https://api.nullplatform.com/link?nrn=organization=1:account=1:namespace=1:application=1' \ -H 'Authorization: Bearer ' ``` These commands will show what the application owns and what's blocking deletion. #### 2. Change the owner Once you've identified the service or link, update its owner by changing the `entity_nrn` to a different application. Use a PATCH request to [services](/docs/api/service-update) or [links](/docs/api/link-update). - Update a service owner: ```bash np service patch \ --id \ --body '{ "entity_nrn": "organization=1:account=1:namespace=1:application=2" }' ``` - Update a link owner: ```bash np link patch \ --id \ --body '{ "entity_nrn": "organization=1:account=1:namespace=1:application=2" }' ``` - Update a service owner: ```bash curl -L -X PATCH 'https://api.nullplatform.com/service/' \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer ' \ -d '{ "entity_nrn": "organization=1:account=1:namespace=1:application=2" }' ``` - Update a link owner: ```bash curl -L -X PATCH 'https://api.nullplatform.com/link/' \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer ' \ -d '{ "entity_nrn": "organization=1:account=1:namespace=1:application=2" }' ``` Replace the IDs and target NRNs with your own values. ### Update the `linkable_to` property After transferring ownership, you might also want to [update the service](/docs/api/service-update)'s `linkable_to` property so it reflects the new relationships between applications. 1. Decide which applications should be able to link to this service using their `nrn` values. 2. Patch the service's `linkable_to` array to include the new application(s) and remove the old owner if needed. ```bash np service patch \ --id \ --body '{ "linkable_to": [ "organization=1:account=1:namespace=1:application=2", "organization=1:account=1:namespace=1:application=3" ] }' ``` ```bash curl -L -X PATCH 'https://api.nullplatform.com/service/' \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer ' \ -d '{ "linkable_to": [ "organization=1:account=1:namespace=1:application=2", "organization=1:account=1:namespace=1:application=3" ] }' ``` ### Remove scopes from your application Next, remove all scopes associated with your application. 1. Go to **Development > Scopes**. 2. Click on **Manage** and select **Delete** for each scope. Use the [scope endpoint](/docs/scope-api-index) to list and remove scopes. 1. List scopes associated with the application. ```bash np scope list \ --application_id 1234 ``` 2. Take the IDs returned for each scope and delete them. ```bash np scope delete \ --id 2345 ``` Use the [scope endpoint](/docs/scope-api-index) to list and remove scopes. 1. List scopes associated with the application. ```bash curl -L 'https://api.nullplatform.com/scope?application_id=1234' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ' ``` 2. Take the IDs returned for each scope and delete them. ```bash curl -L -X DELETE 'https://api.nullplatform.com/scope/1' \ -H 'Authorization: Bearer ' ``` :::note If you're having problems deleting your scopes, see [how to force delete misconfigured scopes](/docs/agent-backed-scopes/troubleshooting/force-delete-scopes). ::: Now try deleting your application again. --- ## Customize repository naming patterns URL: https://docs.nullplatform.com/docs/applications/naming-patterns When you create an application, its repository name is automatically generated based on the technology template you select. However, you can **customize how those repository names are generated to follow your organization’s conventions**. By defining naming patterns in your technology template, you can enforce consistent repository structures across teams and ensure repository names reflect your organization’s internal standards. You can customize these patterns before creating applications by editing your technology templates through the API. Once configured, any new applications created from that template will automatically follow your defined naming rules. ## Understanding templates Technology templates define the base structure, language, and configuration used when creating new applications in your organization. They work as blueprints: every application starts from a template, which determines its initial repository structure, supported languages or frameworks, and any predefined components. There are two types of templates: - **Organization-owned templates**: Created, managed, and customized by your organization. These are typically aligned with internal standards, tooling, and best practices. - **Built-in nullplatform templates**: Templates provided out of the box so teams can start building quickly without needing to create their own. > 💡 **Note:** When updating naming patterns or custom rules, you are modifying the configuration of a specific template. ## Manage templates in the UI You can also manage which templates are available for developers directly from the UI. The **Templates** view in **Platform settings** lets you see all templates in your organization and control their availability. ### Control which templates developers can use You can decide which templates appear as selectable options when creating new applications: - **Disable all built-in nullplatform templates** Disable built-in templates to hide all nullplatform default templates from appearing during application creation. They remain visible in the **Templates** view for reference, but developers cannot select them. - **Activate or deactivate organization-owned templates** You can individually mark organization-owned templates as **Active** or **Inactive**. Only active templates will be available for your developers. :::tip This setting does not affect existing applications and can be turned on or off at any time. ::: ## How to customize naming patterns ### 1. Get the template `id` To make changes, you’ll need the `id` of the template you want to update. Send a [List all technology templates](/docs/api/technology-template-list) request and retrieve the `id` of the template. Here’s an example request: ```bash curl -L 'https://api.nullplatform.com/template' \ -H 'Accept: application/json' ``` You'll get a response like this: ```json { "paging": {}, "results": [ { "id": 123456789, // This is the template ID you'll need in the next step "name": "Golang template", "status": "active", "url": "https://example.com/repository-1", "provider": { "repository": "technology-templates-golang" }, "components": [ { "type": "language", "id": "example-language", "version": "1.0" }, ], "tags": ["golang", "backend"], "updated_at": "2024-01-01T00:00:00.000Z", "created_at": "2024-01-01T00:00:00.000Z" }, { "id": 987654321, "name": "Next.JS template", "status": "active", "url": "https://example.com/repository-2", "provider": { "repository": "example-repository-2" }, "components": [ { "type": "language", "id": "example-language-2", "version": "2.0" }, { "type": "framework", "id": "example-framework", "version": "3.0" } ], "tags": ["example", "frontend"], "updated_at": "2024-01-02T00:00:00.000Z", "created_at": "2024-01-02T00:00:00.000Z" }, ... ] } ``` ### 2. Update the technology template To define custom naming patterns, update the technology template. To do this, send a [PATCH request](/docs/api/technology-template-write) to modify the template. In the request, you’ll: - Provide the technology template `id`. - Update the `rules` object to define specific naming patterns for: - `application_name`: A regular expression to validate and enforce the format of application names (e.g., ensure they end with `-api`). - `repository_path`: A substitution expression (similar to `sed` syntax) that defines the custom structure for the repository path, using matched groups from the application name. **Example** Imagine you want to create an application named "transactions-api" under the namespace "wallet", and you want to use a Golang template. - **Default behavior:** Without any customization to the Golang template, the repository name would default to `wallet-transactions-api`. - **Custom behavior:** To set the repository name to `mygit.com/wallet/api/golang/transactions`, you need to define the following custom naming patterns in the template: ```bash curl -X PATCH 'https://api.nullplatform.com/template/123456789' \ -H 'Content-Type: application/json' \ -d '{ "rules": { "application_name": "^(.*)-api$", "repository_path": "s/^(.*)-(.*)$/${namespace.slug}-golang-${1}-${2}/g" } }' ``` In this example: - `application_name` ensures that the application name matches the pattern `transactions-api` (ending with `-api`). - `repository_path` dynamically generates the repository structure, incorporating the namespace (wallet), language (golang), and the application name (transactions). :::warning Check pattern compatibility Make sure the structure you define is valid for your specific source repository provider to avoid issues with path resolution or repository creation. ::: These rules allow you to enforce consistent, meaningful naming conventions across your organization. ### 3. Create your application You can [now create your application](/docs/applications/creating-new-application) using the updated template. The repository will follow the custom naming pattern you defined. ## More resources If you need to rename the repository in your source provider after creating an application, you can manually update the repository URL in the application to reflect that change and maintain consistency. See [Update the name of your repository](update-repository-name.md) for more info. --- ## Update the name of your repository URL: https://docs.nullplatform.com/docs/applications/update-repository-name When you create an application, a repository is automatically generated for it based on [default naming conventions](./creating-new-application.md). If you later rename that repository in your provider (for example, GitHub), you must update the application’s `repository_url` in nullplatform to match the new repository path. If you don’t update it, actions like creating a new build will fail with an error similar to: ``` error: 'could not get the app: repository: ' ``` Updating the repository URL ensures the platform can continue syncing and tracking your application correctly. ## Update the repository URL Follow these steps to update the repository URL after renaming it in your provider. 1. **Rename the repository** in your provider (for example, GitHub). For example: `https://github.com/acme-corp/billing-api` → `https://github.com/acme-corp/checking-api` 2. **Update the repository URL** using the [Application API](/docs/api/application-update) to match the new URL. ```bash np application patch --id 123 --body '{ "repository_url": "https://github.com/crypto-inc/accounting-checking-api" }' ``` ```bash curl -L -X PATCH 'https://api.nullplatform.com/application/123' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ' \ -d '{ "repository_url": "https://github.com/crypto-inc/accounting-checking-api" }' ``` :::tip Keep your repository in sync Ensure the `repository_url` value exactly matches your new repository location. This allows nullplatform to continue tracking commits, linking builds, and maintaining a proper connection with your source code. ::: ## Related resources - [Create a new application](creating-new-application.md) - [Customize repository naming patterns](naming-patterns.md) - [Application settings](application-settings.md) --- ## Approval actions URL: https://docs.nullplatform.com/docs/approvals/actions ## What is an approval action? An approval action specifies the entities and actions requiring approval, and their outcomes. It determines: - when an approval process is triggered, such as deploying to a production environment or creating a new scope. - what the outcome of that action is: approved, denied, or flagged for manual review. - at what level in your organization this action applies. ## Available entities and actions Approval actions apply to the following entities and actions: | Entity | Actions | | ---------------- | ----------------------------------------------------------------------------- | | `deployment` | `deployment:create` | | `scope` | `scope:create`, `scope:recreate`, `scope:write`, `scope:delete`, `scope:stop` | | `service:action` | `service:action:create` | | `parameter` | `parameter:read-secrets` | | `action_item` | `action_item:resolve`, `action_item:defer`, `action_item:reject` | Each **entity** is a resource type, while each **action** describes an operation performed on that entity. For example, `deployment` defines the entity and `deployment:create` refers to creating a deployment. The `action_item` entity gates how developers transition [action items](/docs/action-items): resolving, deferring, or rejecting them. Unlike `deployment` or `scope`, it doesn't take dimensions, so you scope these approvals through the NRN alone. See [Configure and manage action items](/docs/action-items/configure-and-manage#require-approval-to-transition-items) for an end-to-end example. ## Create an approval action The first step to configure approvals is to create an approval action. You can use our CLI or API. Send a [POST request](/docs/api/approval-action-create) to our Approval endpoint. ```bash np approval action create --body '{ "nrn": "organization=1:account=2:namespace=3:application=4", "entity": "deployment", "action": "deployment:create", "dimensions": { "environment": "production", "country": "us" }, "on_policy_success": "manual", "on_policy_fail": "manual" }' ``` ```bash curl -L -X POST 'https://api.nullplatform.com/approval/action' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ' \ -d '{ "nrn": "organization=1:account=2:namespace=3:application=4", "entity": "deployment", "action": "deployment:create", "dimensions": { "environment": "production", "country": "us" }, "on_policy_success": "manual", "on_policy_fail": "manual" } ``` :::info Default behavior for approval requests If you don’t include `on_policy_success` and `on_policy_fail` in your request, the approval will default to **manual review**. ::: #### What this approval action does This action ensures that when a user attempts to **create a deployment in a production environment** at the organization level `organization=1:account=2:namespace=3:application=4`, the request requires **manual approval**. It must be reviewed and explicitly approved or denied before proceeding. #### Example response For a successful request, you'll get a response like this: ```json { "id": 98765, // Save the action ID for the next steps "nrn": "organization=1:account=2:namespace=3:application=4", "entity": "deployment", "action": "deployment:create", "on_policy_success": "manual", "on_policy_fail": "manual" } ``` This ensures that requests to create deployments are evaluated and either approved or flagged for manual review based on policy results. :::tip The `id` field contains the unique approval action ID. Be sure to save it, as you'll need it in the next steps. ::: ## Using policies to enhance approval actions Approval **actions work on their own** to specify approvals for specific entities and actions. However, you can link them to [policies](/docs/approvals/policies) to add conditions that define **when** approval is needed. This allows you to enforce rules that align approvals with your organization’s requirements, such as time-based rules or conditions on the size of the resource being created. :::warning Upcoming deprecation Policies will be deprecated in favor of [checklists](/docs/approvals/checklists). Attach a checklist to any new action instead. [Specs and actions](/docs/approvals/checklist-specs) shows how to connect one. If you want to migrate policies to checklists, [reach out to the nullplatform team](/docs/support). ::: ## What's next Now that you've created your approval actions, the next step is to [set up a notification channel](/docs/approvals/set-up-notifications) so that reviewers are alerted when a request is pending. --- ## Approval request lifecycle URL: https://docs.nullplatform.com/docs/approvals/approval-requests An **approval request** is created automatically whenever a user triggers an action governed by an approval action. It records the pending decision and tracks the request through review, approval or denial, and execution. ## Request statuses | Status | Description | |---|---| | `pending` | Awaiting a decision from a human reviewer or policy evaluation. | | `approved` | Manually approved by a reviewer. | | `auto_approved` | Automatically approved because a policy passed and `on_policy_success` is set to `approve`, or because a checklist run resolved as `approve`. A checklist approved through an override or a manual review lands `approved` instead. | | `auto_denied` | Automatically denied because a policy failed and `on_policy_fail` is set to `deny`. | | `denied` | Manually denied by a reviewer. | | `cancelled` | Cancelled before a decision was reached. | | `expired` | No decision was made before the reply window closed. | ```mermaid flowchart TD A([Request created]) --> B[pending] B -->|Policy or checklist passes — auto| C[auto_approved] B -->|Policy fails — auto| D[auto_denied] B -->|Manual approval| E[approved] B -->|Manual denial| F[denied] B -->|Cancelled| G[cancelled] B -->|Reply window closes| H[expired] C --> I([Execution window opens]) E --> I D --> Z([Done]) F --> Z G --> Z H --> Z ``` ## Execution statuses Once a request reaches `approved` or `auto_approved`, an execution window opens. The `execution_status` tracks the state of the underlying action. | Execution status | Description | |---|---| | `pending` | Approved but not yet executed. | | `executing` | Execution is in progress. If it's still `executing` after 60 seconds, whatever was running it stopped: you can start the action again or cancel the request. | | `success` | The action executed successfully. | | `failed` | Execution encountered an error. You can start the action again. | | `expired` | The execution window closed before the action was triggered. | In checklist mode, whether the action starts on its own once the request is approved is up to the spec's `execution_trigger`, covered in [Choose what starts the approved action](/docs/approvals/checklist-specs#choose-what-starts-the-approved-action). When it doesn't, someone starts it: **Start deployment**, **Create scope**, or **Apply changes** in the UI, or a `POST` request to `/approval/{id}/execute`. In either mode, starting a request that's already `success`, or `executing` for less than 60 seconds, does nothing: the call answers `200` with the request as it is, and the action isn't sent twice. ## Expiration windows :::note Expiration windows are currently used when users request access to secret parameter values. If that's your use case, check out [Accessing secret values](/docs/parameters/secret-visibility#ask-for-approval) for a full example. ::: Two time limits control how long each phase can last. Both are configured on the approval action, not on individual requests. - **`time_to_reply`**: How long (in milliseconds) approvers have to respond. If the window closes without a decision, the request status moves to `expired`. - **`allowed_time_to_execute`**: How long (in milliseconds) after approval the action can be executed. If execution doesn't happen within this window, the `execution_status` moves to `expired`. ## Checking request status To retrieve the current status of an approval request, send a [GET request to the approval endpoint](/docs/api/approval-read): ```bash np approval read --id 1234 ``` ```bash curl -L 'https://api.nullplatform.com/approval/1234' \ -H 'Authorization: Bearer ' ``` The response includes both `status` and `execution_status`, so you can tell at a glance where the request is in its lifecycle. ## What's next [Create a checklist](/docs/approvals/checklists) to automate approval decisions and reduce the number of requests that require manual review. [Policies](/docs/approvals/policies) still work for the actions that use them, but they'll be deprecated in favor of checklists. --- ## Approvals URL: https://docs.nullplatform.com/docs/approvals/ Gain fine-grained control over your applications, scopes, deployments, services, and more by setting up approvals and creating custom policies. You can also use approvals to gate access to sensitive data, such as [secret parameter values](/docs/parameters/secret-visibility#ask-for-approval). ## Authorization vs. approvals Approvals provide a dynamic alternative to standard authorization: - **Authorization** is static, defining "who can do what" using fixed rules based on standard, coarse-grained [roles](/docs/authorization/roles). - **Approvals** enable detailed decision-making based on contextual information, leveraging the metadata catalog (e.g., "Do not deploy during a holiday freeze unless the user is a Principal"). By combining authorization and approvals, nullplatform provides a flexible way to manage access and workflows to meet your organization's needs. ## How approvals work Approvals in nullplatform allow you to enforce fine-grained control over actions such as deployments, services, or scopes by combining **approval actions**, **policies**, and **notification channels**. These components work together to ensure that specific actions are reviewed and approved based on contextual conditions. To illustrate these concepts, let’s consider an example where you want to control deployments to your production environment. ### Approval actions An **approval action** defines which entities (e.g., deployments) and actions (e.g., create or delete) require approval. For instance, to require approval for deployment attempts in production, you would define an approval action like this: ```json { "nrn": "organization=1:account=2:namespace=3:application=4", "entity": "deployment", "action": "deployment:create", "dimensions": { "environment": "production" }, "on_policy_success": "approve", "on_policy_fail": "manual" } ``` This approval action specifies that any attempt to create a deployment in the production environment must be approved. If the associated policy conditions are met, the action is automatically approved; otherwise, it requires manual review. ### Policies A **policy** defines the conditions under which an action is approved, denied, or flagged for manual review. For our production deployment example, a policy might require that auto-scaling is enabled, a minimum number of instances are configured, and the code coverage meets a certain threshold: ```json { "nrn": "organization=1:account=2:namespace=3:application=4", "name": "Prod deployments require approval", "conditions": { // Policies use MongoDB syntax for conditions, letting you create flexible and complex rules. "scope.capabilities.auto_scaling.enabled": true, "scope.capabilities.auto_scaling.instances.min_amount": { "$gte": 2 }, "scope.dimensions.environment": "production", "build.metadata.coverage.percent": { "$gte": 80 } } } ``` When an approval action is triggered, the policy is evaluated against the request. If all conditions are met, the action is approved. If not, the outcome depends on the policy's configuration. The action is either denied or flagged for manual review. :::warning Upcoming deprecation Policies will be deprecated in favor of [checklists](/docs/approvals/checklists). A checklist is a list of separate requirements, each resolving on its own and each showing its status: automatic conditions like the one above, sign-offs from named people, and checks handed to your own systems. Start any new guardrail as a checklist. An action runs on policies or on a checklist, never both. If you want to migrate policies to checklists, [reach out to the nullplatform team](/docs/support). ::: ### Notification channels Notification channels can be set up to send alerts via platforms like Slack, ensuring that approvers are promptly notified and can take action. For example, a Slack notification channel might look like this: ```json { "nrn": "organization=1", "source": ["approval"], "type": "slack", "configuration": { "channels": ["my-organization-private-channel"] }, "filters": { "action": "deployment:create" } } ``` ### Summary | Step | Description | | -------------------- | ------------------------------------------------------------------------------- | | Approval action | Specifies the entities and actions requiring approval and their outcomes. | | Policy | Defines the conditions for approving, denying, or flagging actions for review. | | Notification channel | Sends alerts to approvers about requests, allowing for quick review and action. | ## How to configure approvals The approval setup process involves four main steps: ```mermaid flowchart LR; Step1["Create an approval action"] --> Step2["Set up a notification channel"]; Step2 --> Step3["Create your policy"]; Step3 --> Step4["Associate the policy with the action"]; ``` We'll guide you through each stage in the following articles. ### Where do I start? :::tip While you can start with either approval actions or notification channels, **it’s best to create policies last** to avoid confusion. ::: We recommend starting by creating an approval action, followed by setting up a notification channel to test if the approval action triggers the approval request. Once this is working, you can proceed to create policies and associate them with the action to define the rules for when the action is triggered. ## What’s next Now that you’ve covered the basics, explore the following sections for more details on setting up approvals in your organization: - [Approval actions](/docs/approvals/actions) - [Set up approval notifications](/docs/approvals/set-up-notifications) - [Approval request lifecycle](/docs/approvals/approval-requests) - [Checklists](/docs/approvals/checklists) - [Checklist items](/docs/approvals/checklist-items) - [Policies](/docs/approvals/policies) (to be deprecated) - [Associate the policy with the action](/docs/approvals/associate-policy-with-action) (to be deprecated) --- ## Associate a policy with an action URL: https://docs.nullplatform.com/docs/approvals/associate-policy-with-action :::warning Upcoming deprecation Policies will be deprecated in favor of [checklists](/docs/approvals/checklists). Start any new guardrail as a checklist. If you want to migrate policies to checklists, [reach out to the nullplatform team](/docs/support). ::: ## Linking a policy to an approval action Next, let's link the newly created policy to the approval action so that when a policy is evaluated against an approval action, each condition is checked. To do this, you'll need to send a [POST request](/docs/api/approval-action-associate-policy) to associate the policy with the action. In the request, you'll need to send: - the approval action ID as a path parameter (e.g.,`98765`). - the policy ID in the request body (e.g., `12345`). #### Example request ```bash np approval action policy create --id 98765 --body '{ "policy_id": 12345 }' ``` ```bash curl -L -X POST 'https://api.nullplatform.com/approval/action/98765/policy' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ' \ -d '{ "policy_id": 12345 }' ``` A successful response (200 OK) confirms the association. --- ## How policy evaluation works Once you've linked your policy to an approval action, nullplatform evaluates it against relevant entity data. Each condition in the policy is checked against real-time metadata to determine whether the approval should proceed. Below is an example of how this evaluation process works in practice. ```json title="Policy evaluation example" { "id": 12345, "name": "Enforce coverage, security, and scaling for deployments", "conditions": { "build.metadata.coverage": { "$gte": 80 }, "build.metadata.security.issues": { "$lte": 4 }, "scope.capabilities.auto_scaling": true, "$or": [ { "scope.dimensions.environment": "dev" }, { "scope.dimensions.environment": "qa" } ] }, "evaluations": [ { "criteria": { "build.metadata.coverage": { "$gte": 80 } }, "result": "met" }, { "criteria": { "build.metadata.security.issues": { "$lte": 4 } }, "result": "not_met" }, { "criteria": { "scope.capabilities.auto_scaling": true }, "result": "met" }, { "criteria": { "$or": [ { "scope.dimensions.environment": "dev" }, { "scope.dimensions.environment": "qa" } ] }, "result": "met" } ], "passed": false } ``` **What's evaluated here:** - Coverage (≥ 80%) → `"met"` → The condition was satisfied. - Security issues (≤ 4) → `"not_met"` → The condition was not met (perhaps there were more than 4 security issues). - Auto-scaling enabled → `"met"` → The resource supports auto-scaling. - Environment (dev or qa) → `"met"` → The deployment was in `"dev"` or `"qa"`. Since at least one condition **was not met**, the overall policy evaluation **failed** (`"passed": false`). ## What's next Your approval setup is complete. From here you can: - [Check the status of approval requests](/docs/approvals/approval-requests#checking-request-status) to monitor pending decisions and execution outcomes. - [Set up approval notifications](/docs/approvals/set-up-notifications) if you haven't already, to keep reviewers informed in real time. --- ## Checklist items URL: https://docs.nullplatform.com/docs/approvals/checklist-items Every [checklist](/docs/approvals/checklists) is an array of items, and each item answers two separate questions. - Its **type** says how the item gets resolved, whether nullplatform evaluates it, a person signs it, or one of your systems reports back. - Its **behavior** says what the result does to the action, whether it can block it, or is only there to be read. Keeping those apart is what makes a checklist expressive. A security scan and a compliance note can both be automatic conditions, but one blocks the deploy and the other is there for the record. Whatever its type, every item requires two fields: an `id` and a `title`. The `id` is how nullplatform addresses the item, both in the [outcome expression](#how-the-outcome-is-computed) and in the item's own state. The `title` is the label a person reads in the list, and a condition needs one as much as a sign-off does: the run is a list someone reads, even when nothing in it is waiting on a human. ## Item types ### `condition` Evaluated automatically against the request context using a `query`, a mongo-like filter document. This is the same condition language and the same field paths used by [policies](/docs/approvals/policies), which is why a predicate can move between the two unchanged. ```json { "id": "coverage_gate", "type": "condition", "behavior": "gate", "title": "Build coverage above 80%", "query": { "build.metadata.coverage.lines.percent": { "$gte": 80 } } } ``` Operators include `$eq`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$and`, `$or`, and `$nor`. :::warning Field paths carry no `context.` prefix Write `build.metadata.coverage`, never `context.build.metadata.coverage`. A `context.`-rooted path is rejected when the checklist spec is saved. Before that validation existed, such a spec saved cleanly and then failed every run on every request. ::: Watch the shape of what you're comparing. Coverage, for example, is an object rather than a number, so `build.metadata.coverage` compared against `80` will never match. Address the field you actually mean, like `build.metadata.coverage.lines.percent`. ### `manual` Waits for a person. The item stays pending until someone submits a decision, which is recorded with their identity and their comment. ```json { "id": "security_signoff", "type": "manual", "behavior": "gate", "title": "Security team sign-off", "description": "Required for changes that touch auth or PII." } ``` The `title` is what the developer sees in the list, and the `description` is where you put the guidance the signer needs. Write it for someone who doesn't have the context you have right now. A plain manual item collects a decision and, if you set `require_comment: true`, a comment. When the sign-off needs more than that, give it structure: #### Collect structured inputs A manual item can declare `inputs`: a JSON Schema describing what the signer must fill in. The form is rendered from the schema, validated when the person submits, and the submitted values are persisted on the item, so "which CAB ticket covered this?" has an answer in the audit trail. ```json { "id": "cab_approval", "type": "manual", "behavior": "gate", "title": "Change Advisory Board approval", "inputs": { "schema": { "type": "object", "required": ["cab_ticket", "risk_level"], "properties": { "cab_ticket": { "type": "string", "title": "CAB ticket", "pattern": "^CAB-\\d{4}-\\d{4}$" }, "risk_level": { "type": "string", "title": "Risk level", "enum": ["low", "medium", "high"] } } } } } ``` The schema follows the same convention as [service specifications](/docs/services/craft-a-service/service-specs): JSON Schema with a top-level `type: object`, validated twice, once when the spec is saved and again when the values are submitted. You can add a `ui_schema` to control the layout; without one, the form renders as a vertical layout in schema order, which is usually fine. Submitted values appear as `input_values` when you read the run. The item keeps the values as submitted, but the event trail and the audit notifications don't. A schema property marked `writeOnly: true` or `format: "password"` reaches them as `[REDACTED]`. Nullplatform reads that mark on `properties`, nested ones included, and on the `items` of an array. It doesn't read it under `$ref`, `allOf`, `anyOf`, `oneOf`, `patternProperties` or `additionalProperties`, so declare a secret field directly. A set of values larger than 8 KB appears in the trail as a digest instead of the values. #### Guard who can sign with validations `validations` are rules evaluated on the server when someone approves the item. When someone rejects it, only the ones that read the `actor` run. They use the same query language as conditions, but run against a different document: the submission itself, so a rule can see the `inputs` being submitted, the `actor` submitting, the `approval` being decided, and the other items in the run. String values that start with `$` are references: nullplatform resolves them against that document before evaluating the rule, so one field of the submission can be compared against another. That's all a four-eyes rule takes, the rule that stops whoever requested the change from approving or rejecting it themselves: ```json { "validations": [ { "id": "four_eyes", "message": "Someone other than the requester must confirm.", "rule": { "actor.user_id": { "$ne": "$approval.requested_by" } } }, { "id": "peer_role", "message": "Requires developer or admin on this scope.", "rule": { "actor.effective_roles": { "$in": ["developer", "admin"] } } } ] } ``` Three things about validations matter: - **Rules that read the `actor` gate both approving and rejecting.** Like both rules above, they decide who can sign: whoever fails one can neither approve nor reject. They get a `422` with the rule's `message`, and the item stays as it was. If nobody passes these rules, nobody can reject the item either, and a request it blocks waits until it expires or someone cancels it. Rules that check what's submitted or the state of the run, like a form field or another item's status, gate only approving. - **Keep the actor and the form in separate validations.** A rejection's form isn't checked: its validations see empty `inputs`. A rule that requires both the right `actor` and a filled-in field fails every rejection, so nobody can reject the item. - **A broken reference fails closed.** If a `$` reference doesn't resolve, because of a typo or a field that doesn't exist, the rule fails and the error names the offending reference. It never resolves silently to nothing, which would quietly disable the check. Note the four-eyes rule above references `$approval.requested_by` with no `.id`: the approval exposes its requester as a plain user id. `$approval.requested_by.id` resolves to nothing, so that version of the rule blocks every signer, approving or rejecting, rather than just the requester. When a rule fails, the signer gets the rule's `message` next to the field, not a generic error. #### Delegate a validation to an external system Instead of a `rule`, a validation can declare `external: { kind, inputs }`. When the person submits, the item enters a `validating` status while nullplatform dispatches the check and waits for the answer. All external validations pass and the item lands on the status the person submitted; any of them fails, or the dispatch itself fails, and the item returns to `pending` with the failure recorded on the card. On a rejection, nullplatform dispatches only the external validations whose `inputs` read the actor, like `{{ actor.user_id }}`. This is how "a human signs, but the system double-checks the ticket they cited" works. The other end is typically a workflow. ### `external` Delegates the whole check to one of your systems. Nullplatform dispatches an event carrying the item's `kind` and inputs, and waits for the system to call back with the result. ```json { "id": "e2e_suite", "type": "external", "behavior": "gate", "title": "End-to-end suite", "external": { "kind": "e2e-suite", "trigger": "auto", "timeout_seconds": 900, "inputs": { "release_id": "{{ context.release.id }}" } } } ``` With `trigger: auto`, nullplatform dispatches the item as soon as the run starts. With `trigger: on_demand`, the item sits in `latent` until someone activates it from the run. Choose `on_demand` for checks that cost real time or money, like an end-to-end suite, so they run when someone asks for them rather than on every request. `timeout_seconds` is how long nullplatform waits for the callback, counted from the moment the item is dispatched, which for an `on_demand` item means from the moment it's activated. If the callback never arrives, the item lands on `timed_out`, which blocks the action exactly like a failure while staying distinguishable from one in the audit trail. Placeholders in `inputs` are the one place the `context.` prefix survives, because that value is the payload you're sending rather than a query being evaluated. Keep `{{ context.release.id }}` as-is here. #### Report a typed output When the callback reports `passed` or `failed`, it can also send an `output`: a JSON object of up to 64 KB that other items can read to [depend on this one](#depend-on-another-item). Declare its shape in `external.output_schema`, a JSON Schema: ```json { "id": "change_analysis", "type": "external", "behavior": "informational", "title": "Change analysis", "external": { "kind": "change-analysis", "trigger": "auto", "output_schema": { "type": "object", "required": ["risk"], "properties": { "risk": { "type": "string", "enum": ["low", "medium", "high"] } } } } } ``` With a schema, a `passed` callback must send an `output` that validates, while a `failed` one can leave it out. Any `output` that doesn't match gets a `422`, and the item stays where it was until your system calls back again or the item times out. The natural consumer of these events is a workflow subscribed to the same `kind`. ### `group` Holds other items in `children`, which can themselves be groups. Groups organize the list the developer reads, like "Pre-deploy checks" and "Manual sign-offs". They're for structure, not sequencing: nullplatform evaluates conditions in parallel and waits on asynchronous items independently, so ordering items never controls when they run. To make one item wait for another, [depend on it](#depend-on-another-item) instead. ```json { "id": "pre_deploy_checks", "type": "group", "behavior": "gate", "title": "Pre-deploy checks", "children": [ { "id": "tests_passed", "type": "condition", "behavior": "gate", "title": "Tests passed", "query": { "build.metadata.tests_status": { "$eq": "passed" } } }, { "id": "lint_clean", "type": "condition", "behavior": "gate", "title": "Lint clean", "query": { "build.metadata.lint_errors": { "$eq": 0 } } } ] } ``` #### Require all children, or any By default a group passes when **all** of its children pass. Set `aggregation: any` when one is enough: ```json { "id": "peer_signoff", "type": "group", "behavior": "gate", "title": "Peer sign-off", "aggregation": "any", "children": [ { "id": "developer_signs", "type": "manual", "behavior": "gate", "title": "A peer developer signs off" }, { "id": "admin_signs", "type": "manual", "behavior": "gate", "title": "An account admin signs off" } ] } ``` That group reads as "either a peer developer or an account admin signs off", which is much closer to how organizations actually work than forcing both. One restriction: a group with `aggregation: any` can't contain `override` children, and the spec is rejected at save if it does, because "any" semantics would let an override slip through as a routine pass. ## Behaviors Every item must declare one. This is the field that decides what your guardrail actually enforces. | Behavior | Effect on the action | |---|---| | `gate` | Blocks when it fails. The run can't be approved normally while a blocking item is failing | | `informational` | Never blocks, whatever the result. Use it to surface a report or a diagnostic | | `override` | Only matters once a blocking item has failed. Approving it resolves the run as approved with an override | Use `informational` more than you'd expect. Attaching a scan report to every request costs nothing and gives reviewers context, and it doesn't add a way for the action to get stuck. Keep `override` items rare and clearly labeled. An override is a deliberate, attributable decision to ship past a failed guardrail, so the `description` should name the circumstances under which that's acceptable. ## Item ids Each item needs an `id` matching `^[a-z][a-z0-9_]{0,63}$`, unique within the checklist spec. The id is how the outcome expression refers to the item and how per-item state is keyed, so it shows up in logs and in the audit trail. :::danger Never name an item `and`, `or`, `not`, `true`, or `false` The expression that computes the outcome reserves those five words as operators and literals. An item called `or` makes the expression unparseable and the whole run resolves as failed. The spec is rejected when you save it, so you'll find out immediately. Note `nor` is not reserved. ::: ## Apply an item conditionally Any item or group accepts `applies_when`, a query in the same language that decides whether the item is part of this run at all. An item excluded this way is marked as not applicable and never blocks. ```json { "id": "security_signoff", "type": "manual", "behavior": "gate", "title": "Security team sign-off", "applies_when": { "scope.dimensions.environment": "production" } } ``` This is how one checklist spec serves several environments. The list a developer sees in staging is the same guardrail with the production-only items stood down, rather than a second spec that drifts from the first. ### Depend on another item `applies_when` can also read the other items in the run, so an item joins only after another one has answered. This is how "an admin signs off only when the change analysis says the risk is high" works: the admin approval can't be given before the analysis is done, and a low-risk change skips it. Reference another item with `run.items..status`, or with `run.items..output.` to read the [output](#report-a-typed-output) an external item reported: ```json { "id": "admin_approval", "type": "manual", "behavior": "override", "title": "Admin approval", "applies_when": { "run.items.change_analysis.output.risk": "high" } } ``` Until the items it reads finish, the dependent is `undetermined`. It blocks the run like any unresolved item, and nobody can act on it: a sign-off sent too early gets a `400` that names what the item is waiting on. You can mix references with context keys under `$and` and `$or`, and nullplatform decides as soon as the answer can no longer change, so an `$or` with a matching context branch doesn't wait at all. Each dependent is decided once: it either joins the run or is skipped as not applicable. A dependent that joined stays in the run. One that was skipped opens again when the item it reads gets a new answer, through a retry or a corrected sign-off, and is decided against that answer. When you save the spec, nullplatform rejects references it couldn't evaluate safely: - **Only final statuses.** Compare against `passed`, `failed`, `timed_out`, or `skipped`. An item that ends `cancelled` never matches. - **No negations.** `$ne`, `$nin`, `$not`, `$nor`, `$exists: false`, and `null` aren't allowed on a reference, because they'd already be true before the other item answers. List the values you expect instead, like `{ "$in": ["failed", "timed_out"] }`. - **Outputs come from external items.** Only an `external` item reports an output. An item can't reference itself, and a cycle between items is rejected. - **References go on items, not on groups.** A group's own `applies_when` can't read other items. An output path the item didn't report never matches, which skips the dependent. Give the external item an `output_schema` so a typo or a missing field fails loudly instead. Saving a spec that reads an output without one returns a warning. ## Items on a deployment group When the approval is for a [deployment group](/docs/deployments/overview#deployment-groups), an item that reads the deployment (`deployment.*`, `scope.*`, `deployment_strategy.*`, or `previous_release.*`) runs once per deployment in the group, and only for the deployments it applies to. The `security_signoff` example above is checked against each deployment's own scope, so in a group with one production and one staging deployment it runs only for the production one. An item that reads only what the group shares runs once for the whole group. The item passes when it passes for every deployment it ran on. A `manual` or `override` item asks for one sign-off per deployment, so a single sign-off can't grant an override for the whole group. If the item applies to none of the deployments, it's not applicable: a production-only gate doesn't gate a group with no production deployment. Dependencies between items follow the deployment too. An item that runs per deployment can [depend on other items](#depend-on-another-item) through its `applies_when`, and whether it applies is decided separately for each deployment. When the item it depends on also runs per deployment, each deployment's check waits for the result of that same deployment. It reads that result as not applicable if the item didn't run there. An item that runs once for the group reads the group's combined result instead. An item that reads the `output` of an item that runs per deployment runs per deployment too, even if it doesn't read the deployment itself. If it's a `manual` item, it asks for one sign-off for each deployment where it applies. ## Severity Items take an optional `severity` of `info`, `minor`, `major`, or `critical`. Severity does not affect the outcome: a failing `minor` blocking item blocks exactly as hard as a failing `critical` one. What it drives is how the item is presented and how it reads in audit reports, so set it to help a human triage the list. ## Item statuses | Status | Meaning | |---|---| | `passed` | Resolved successfully | | `failed` | Resolved unsuccessfully | | `pending` | Waiting on a person, or on an external system that was dispatched | | `undetermined` | Waiting on the items its `applies_when` reads. Nobody can act on it, and it blocks the run until nullplatform decides whether it applies | | `latent` | An `on_demand` external item waiting for someone to activate it. Its timeout doesn't start until then | | `in_progress` | Activated, or acknowledged by the external system now working on it | | `validating` | A manual item was submitted (approved or rejected) and its external validations are being checked | | `skipped` | Never ran. The item's `sub_status` says why | | `timed_out` | An external item never called back within its window. It blocks like a failure | | `cancelled` | The run ended before this item resolved | `skipped` covers three different stories, told apart by `sub_status`. The run read returns it on the item itself, next to `status`, together with `applicable`, which is `false` only for an item that `applies_when` pruned out of the run: | `sub_status` | What happened | |---|---| | `not_applicable` | The item's `applies_when` didn't match, so it was never part of this run and takes no part in the outcome | | `not_required` | The run was approved before this item had to run | | `not_evaluated` | The run failed before reaching this item | The last two are the resolution sweep. When a run settles on `approve` or `fail`, every item still unresolved is closed out as `skipped` rather than left hanging, so a finished run never shows items in limbo. A run that ends `cancelled` or `expired` sweeps them to `cancelled` instead. Every manual resolution is attributed: the run records `resolved_by` with the person's identity, their effective roles at that moment, and the timestamp. Together with `input_values` and the per-item logs, that's what turns "someone approved it" into an answer an auditor can use. ## How the outcome is computed A run tracks an aggregate status while items resolve, then settles on a final outcome. | Aggregate status | What it means | |---|---| | `pending_items` | At least one item is still unresolved | | `pending_aggregation` | Every item resolved, the outcome is being computed | | `pending_override` | A blocking item failed and an override item is available but undecided | | `resolved` | The run ended and the final outcome is set | | Final outcome | When it happens | |---|---| | `approve` | Every blocking item passed, or the checklist has none | | `approve_with_override` | A blocking item failed and an override item was approved | | `fail` | A blocking item failed with no override approved, or none defined | | `cancelled` | The run was cancelled externally | | `expired` | The approval action's allowed execution window passed first | Nullplatform derives a boolean expression from your items: all `gate` items must pass, or any `override` item must be approved. For the spec on [Specs and actions](/docs/approvals/checklist-specs#what-a-checklist-spec-looks-like), that expression is: ``` ( coverage_gate.passed AND snyk_high_severity.passed AND security_signoff.passed ) OR cab_override.passed ``` Reading that expression is the quickest way to check that a checklist enforces what you meant. If the shape you need can't be expressed by "all blocking items, or an override", you can supply your own expression in the spec, though it is an escape hatch rather than a normal crafting step. ### When a run fails A failed run isn't necessarily the end of the request. By default the approval stays open and resumable: the developer reads which item failed, fixes the cause, and redeploys, or asks for a manual review when they need a human decision. Reviewers are notified only when review is requested. If you'd rather send failures straight to reviewers, that's the action-level `on_checklist_fail: manual` setting, covered in [Choose what a failed run does](/docs/approvals/checklist-specs#choose-what-a-failed-run-does). Every transition, including the aggregate status before and after, is written to the run's event trail. When a run ends in a state you didn't expect, that trail plus the per-item logs tell you which item moved and when. ## What's next - [Checklists](/docs/approvals/checklists): what checklists solve and how a run works - [Specs and actions](/docs/approvals/checklist-specs): creating specs via the API and connecting actions - [Approval request lifecycle](/docs/approvals/approval-requests): what happens around the run, from trigger to execution - [Policies](/docs/approvals/policies): the same condition language in its single-verdict form, to be deprecated in favor of checklists --- ## Specs and actions URL: https://docs.nullplatform.com/docs/approvals/checklist-specs A [checklist](/docs/approvals/checklists) is carried by two entities: - **Checklist specification**, spec for short: the items themselves, plus how their results add up to an outcome. - **Approval action**: which requests the spec governs, which version of it they run against, and what happens when it fails. This page is the contract for both: crafting, wiring, and permissions. If you haven't built a checklist yet, start with [your first checklist](/docs/approvals/your-first-checklist) instead; everything the skill did there is this API. ## What a checklist spec looks like A spec is a name, an NRN, and a `definition` holding the items: ```json { "items": [ { "id": "coverage_gate", "type": "condition", "behavior": "gate", "severity": "major", "title": "Build coverage above 80%", "query": { "build.metadata.coverage.lines.percent": { "$gte": 80 } } }, { "id": "snyk_high_severity", "type": "condition", "behavior": "gate", "severity": "critical", "title": "No high-severity Snyk findings", "query": { "build.metadata.snyk.high": { "$eq": 0 } } }, { "id": "security_signoff", "type": "manual", "behavior": "gate", "severity": "major", "title": "Security team sign-off", "description": "Required for changes that touch auth or PII.", "applies_when": { "scope.dimensions.environment": "production" } }, { "id": "cab_override", "type": "manual", "behavior": "override", "title": "CAB emergency override", "description": "Use only when a blocking item is misconfigured and the change must ship." } ] } ``` Conditions use the same query language and the same field paths as [policies](/docs/approvals/policies), so a predicate you already trust can move across unchanged. :::note Note the paths carry no `context.` prefix. The full item contract, including structured inputs, validations, and groups, is in the [items reference](/docs/approvals/checklist-items). ::: Next to `items`, the definition takes an optional `aggregation` block that says how the outcome is computed: | `aggregation.type` | What it does | |---|---| | `derived` (the default) | Nullplatform builds the expression from your items: every `gate` item must pass, or an `override` item must be approved | | `expression` | You supply the boolean expression yourself in an `expression` field. An escape hatch, not a normal crafting step | Leaving `aggregation` out entirely is the same as `derived`. See [how the outcome is computed](/docs/approvals/checklist-items#how-the-outcome-is-computed) for the expression language. The definition also takes an optional `execution_trigger`, which decides whether the action starts on its own once the request is approved. See [Choose what starts the approved action](#choose-what-starts-the-approved-action). ## Craft the specification Describing the spec to the **np-checklist** skill is the quickest way to get one, and [your first checklist](/docs/approvals/your-first-checklist) walks through that. To craft it yourself: 1. Go to **Platform settings > Approvals > Checklists** and click **+ New specification**. The editor opens on a working one-item spec: ```json { "name": "my-checklist-specification", "description": "", "definition": { "items": [ { "id": "manual_approval", "type": "manual", "behavior": "gate", "severity": "major", "title": "Manual approval", "require_comment": true } ], "aggregation": { "type": "derived" } } } ``` 2. Build your definition on top of it. The **Condition**, **Manual**, **External**, and **Group** buttons drop ready-made blocks into the editor, so you can write a spec without keeping the schema in your head. 3. Check it in the pane on the right, which revalidates while you type. **Checklist preview** renders the list a developer would see, and **Preview as** flips it through **Not started**, **In progress**, **Everything passed**, and **Everything failed**, plus **Failed, then overridden** when the spec has an `override` item. None of them creates a run. 4. Click **Create**. The spec appears in the **Checklists** list with its version, its status, and how many approval actions currently run on it. Send a [POST request](/docs/api/checklist-specification-create) with the definition in the request body: ```bash curl -L 'https://api.nullplatform.com/approval/checklist/specification' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "nrn": "organization=1:account=2:namespace=3", "name": "my-checklist-specification", "description": "", "definition": { "items": [ { "id": "manual_approval", "type": "manual", "behavior": "gate", "severity": "major", "title": "Manual approval", "require_comment": true } ], "aggregation": { "type": "derived" } } }' ``` The response carries the generated spec id, which looks like `spec_abc123def456ghij`, and the boolean expression nullplatform derived from your items to compute the outcome. Reading that expression back is the quickest way to confirm the spec enforces what you meant. ## Connect it to an approval action A spec does nothing until an approval action points at it. An action runs on a checklist **or** on policies, never both, so nothing can end up with two sets of rules disagreeing about the same request. Go to **Platform settings > Approvals > Settings** and click **+ New approval action**. The form creates the action with the checklist already attached. Choose the resource, entity, and action to protect, pick **Checklist** along with the specification and the version to pin it to, and set what happens once the run is evaluated. The form explains every field as you fill it in. To point an action that already exists at a spec, send a [POST request](/docs/api/approval-action-link-checklist-specification) to the action: ```bash curl -L 'https://api.nullplatform.com/approval/action/1842/checklist_specification' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "checklist_specification_id": "spec_abc123def456ghij" }' ``` If the action still has policies attached, the call fails with `409` and a message naming how many policies are in the way. Reach out to the nullplatform team to move an action that already runs on policies onto a checklist. ## Change a spec that's already live Updating a spec never edits it in place. Every update publishes a **new version**: a new spec id under the same name and NRN, with the version number bumped. Go to **Platform settings > Approvals > Checklists** and open the checklist you want to change. In its detail view, click **New version**: the spec editor reopens on the current definition, with the same validation and preview panes you had when you created it. Edit whatever you need and click **Save as new version**. The detail view lists the approval actions pinned to the version you're viewing. Its **Overview** tab shows the definition next to the checklist preview, with the expression the engine evaluates when the checklist finishes under **Approval rule**. The **Versions** tab holds the version history, where **Compare** shows any two versions side by side. Send a [PATCH request](/docs/api/checklist-specification-update) with the new definition: ```bash curl -L -X PATCH 'https://api.nullplatform.com/approval/checklist/specification/spec_abc123def456ghij' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "definition": { "items": [ ... ] } }' ``` The response is version 2 with its own `spec_…` id. Publishing doesn't switch anything over. Version 1 stays active and the action keeps evaluating the version id it holds, so a spec change is a two-step rollout: publish, then point the action at the new id. Pointing it back at the old id is the rollback, and runs already in flight finish on the version they snapshotted. **Compare** puts the two definitions side by side, so you can see exactly what moved before you point an action at the new version. ## Dry-run a spec before it governs anything A dry run resolves the same action a real request would resolve and previews the run without creating it. It is what you do right after connecting a spec, and again after every new version. Send a [POST request](/docs/api/approval-dry-run) describing the request you want previewed: ```bash curl -L 'https://api.nullplatform.com/approval/dry-run' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "nrn": "organization=1:account=2:namespace=3:application=4", "action": "deployment:create", "approval_mode": "checklist", "entity_id": 9876, "requested": { "scope_id": 5521, "release_id": 7710 } }' ``` `approval_mode` takes `auto` (the default: the checklist if the action has one, policies otherwise), `checklist`, or `policy`. Forcing `checklist` on an action with no spec attached is an error rather than a silent fallback, which is what you want when the thing you are verifying is the wiring. The response predicts the run item by item: ```json { "mode": "checklist", "specification_snapshot": { "id": "spec_abc123def456ghij", "name": "staging-deploy-gate", "version": 2 }, "preview": { "items": [ { "id": "coverage_gate", "type": "condition", "behavior": "gate", "predicted_status": "failed", "would_run": true, "message": "condition_not_met" }, { "id": "security_signoff", "type": "manual", "behavior": "gate", "predicted_status": "pending", "would_pend_for_user": true } ], "aggregate_prediction": { "best_case": "fail", "worst_case": "fail", "status": "resolved" } } } ``` Conditions are genuinely evaluated, so their `predicted_status` is the real verdict. Everything asynchronous reports what it would wait for instead: a manual item comes back `pending` with `would_pend_for_user`, an `on_demand` external comes back `latent`, and an `auto` external is only dispatched when it declares `dry_run_safe: true`. `aggregate_prediction` closes the loop: `best_case` says whether the request can be approved at all, and when it matches `worst_case` the outcome is already decided. Pass a `context` object in the body to preview against values of your choosing rather than the ones nullplatform would build from the request. That is how you check a threshold from both sides without producing a build that fails it. ## Choose what starts the approved action A run that passes approves the request, but approving it doesn't have to start the action behind it. Set `execution_trigger` in the definition, next to `items`, to decide: ```json { "execution_trigger": "automatic_approval", "items": [ ... ] } ``` | `execution_trigger` | What happens once the request is approved | |---|---| | `explicit` (the default) | The action waits for someone to start it: **Start deployment**, **Create scope**, or **Apply changes** in the UI, or a `POST` request to `/approval/{id}/execute` | | `automatic_approval` | The action starts on its own when no person or external system took part in the run. When one did, it waits, as with `explicit` | | `any_approval` | The action starts on its own on any approval: a run that resolves `approve` or `approve_with_override`, or a reviewer who approves the manual review | Leaving `execution_trigger` out, or setting it to `null`, is the same as `explicit`. Any other value fails validation when you save the spec: a `422` whose errors name `rule: execution_trigger.invalid`. For `automatic_approval`, what matters is whether any item involved a person or an external system. Conditions never do. Neither does a manual item that a static `applies_when` (its own or its group's) pruned when the run was created, anything `informational`, item or group, or an `override` item or group nobody used. Everything else does, and the action waits: any other manual item, however it ended; a manual item whose `applies_when` [reads other items](/docs/approvals/checklist-items#depend-on-another-item) (`run.items.*`); any `external` item; an override that was used; and a run that went to manual review or waited for an override. A `gate` group doesn't count by itself: its children count one by one. A few cases follow their own rule, whatever the trigger. Approving an action item transition (`action_item:resolve`, `action_item:defer`, or `action_item:reject`) always applies it right away. A deployment that belongs to a [deployment group](/docs/deployments/overview#deployment-groups) never starts on its own: the group's approval starts it, under these same rules. And an approval with nothing behind it to run, like `parameter:read-secrets`, is unaffected. The run reads `execution_trigger` from the spec version it snapshotted. Changing it works like [any other change](#change-a-spec-thats-already-live): publish a new version and point the action at it, and the runs created from then on use the new value. ### See what an approval will do The [run read](/docs/api/checklist-run-read) carries an `execution` block, so you don't have to work the rules out yourself: ```json { "execution": { "trigger": "automatic_approval", "automatic_run": true, "on_approval": "execute" } } ``` `trigger` is the value the run read from its spec, `explicit` when the spec doesn't set one. `automatic_run` applies the rules above to the run as it stands. `on_approval` is `execute` when approving starts the action, or `wait` when it waits for someone. Before the run resolves, it tells you what's going to happen. Afterwards, read it next to the request's `execution_status` to see what did. ## Choose what a failed run does By default, a failed blocking item doesn't page anyone. The request stays open and resumable: the developer reads which item failed, fixes the cause, and redeploys, or asks for a manual review when they genuinely need another human. Reviewers are only notified at that point. Set `on_checklist_fail` on the approval action to change what happens: | `on_checklist_fail` | What a failed run does | Ask for manual review | |---|---|---| | `deny` | The request is denied outright. Nobody is notified; the failed run stays attached and readable | Never offered | | `pending` (default) | The request stays open. The requester fixes and redeploys, cancels, or asks for manual review, which is when reviewers get notified | Offered | | `manual` | The request goes to manual review right away and reviewers are notified | Offered | Keep `pending` when the requester can fix what failed, like a coverage threshold or a compliance rule they can address in code. Reserve `manual` for checklists the requester has no way to resolve on their own, and `deny` for checklists that should never involve a human at all. ## Who can do what Configuring guardrails and executing them are deliberately different jobs. | Operation | Roles | |---|---| | Create, update, or delete checklist specs | `secops`, `ops`, `admin` | | Link a checklist spec to an approval action | `secops`, `ops`, `admin` | | [Dry-run](#dry-run-a-spec-before-it-governs-anything) a checklist spec against a context | `secops`, `ops`, `admin`, `developer` | | Sign off a manual item or retry an external one | the team that owns the run's NRN | | Read checklist specs and runs | all roles, including `member` and `troubleshooting` | See [authorization](/docs/authorization/) for how roles and grants work. ## What's next - [Items reference](/docs/approvals/checklist-items): the full contract of every item type, behavior, and validation rule - [Approval actions](/docs/approvals/actions): everything an action can govern beyond deployments --- ## Checklists URL: https://docs.nullplatform.com/docs/approvals/checklists # Checklists New :::info 🚀 Early release Checklists are in early release and may not be enabled in your organization yet. If you'd like to try them or want to know more, [reach out to us](https://docs.nullplatform.com/docs/support) and we'll get you set up. ::: A **checklist** is the list of requirements that must pass before a protected action goes through: a deployment, a secret read, a scope change. - Some items check facts automatically, - some wait for a person to sign off, and - some hand the check to one of your own systems. Every item shows its own status, so the developer who triggered the action sees what passed, what's still running, and who owes a signature. That list is where your organization's guardrails live. Instead of a runbook nobody reads, the rules that govern an action are declared once, applied on every request, and recorded item by item for the audit. ## Checklists vs policies :::warning Upcoming deprecation Policies will be deprecated in favor of checklists. Start any new guardrail as a checklist. If you want to migrate policies to checklists, [reach out to the nullplatform team](/docs/support). ::: Both guard the same actions from the same Approvals area. The difference is the shape of the answer. - A [**policy**](/docs/approvals/policies) answers one question: do these conditions hold, yes or no? All its conditions resolve to a single verdict. - A **checklist** turns that single verdict into the list of requirements behind it, each with its own status, owner, and result. A policy gives you the answer; a checklist shows the requirements that produced it, so the developer waiting knows which one is missing and the person signing off knows what they're signing. ### Migrate policies into checklists If you're already running on policies, you don't have to rebuild anything. A policy that grew past a single condition is usually a checklist waiting to happen, and checklists use the same condition language and the same field paths, so your predicates carry over unchanged. If you want to move an action from policies to a checklist, [reach out to the nullplatform team](/docs/support). ## What a checklist is made of Three pieces carry the feature: - A **checklist specification**, spec for short, is the versioned definition of the requirements: authored once, reused on every request. The nullplatform UI lists them under **Platform settings > Approvals > Checklists**, where **+ New specification** creates one. Specs and their lifecycle live in [Specs and actions](/docs/approvals/checklist-specs). - **Items** are the requirements inside a spec. There are four item types and one list can mix them, so a single checklist can check facts automatically, wait for a person, hand a check to one of your systems, and group related requirements together. - A **run** is one instantiation of the spec against one request: the thing developers actually see and auditors actually read. Here's a complete spec, the same two-item gate you'll build in [your first checklist](/docs/approvals/your-first-checklist): ```json { "name": "first-gate", "definition": { "items": [ { "id": "coverage_gate", "type": "condition", "behavior": "gate", "title": "Build coverage above 80%", "query": { "build.metadata.coverage.lines.percent": { "$gte": 80 } } }, { "id": "release_signoff", "type": "manual", "behavior": "gate", "title": "Release owner sign-off" } ] } } ``` In that spec, the `condition` item type resolves on its own the moment a run is created, and the `manual` item type waits for a person. Those are two of the four types: | Item type | What it does | |---|---| | `condition` | Evaluates a query against the request context, like coverage or environment | | `manual` | Waits for a person to sign off, with their identity recorded | | `external` | Hands the check to one of your systems, typically a workflow, and waits for the result | | `group` | Nests other items to organize the list | Each item also declares a **behavior**, which decides what its result does to the action: a `gate` item stops the action when it fails, an `informational` item reports without ever blocking, and an `override` lets an authorized person send the action through even though a `gate` item failed. The [items reference](/docs/approvals/checklist-items) covers every type, behavior, and status in detail. ## How a run works A run starts when a developer triggers one of the operations the checklist governs: creating a deployment, reading a secret, changing a scope. Which operations those are is decided by the [approval action](/docs/approvals/actions) the spec is attached to, so the same checklist can guard deploys in one account and secret reads in another. From there, nullplatform creates an approval request, instantiates the spec into a run, and starts resolving it: conditions evaluate immediately, external items are dispatched, and everything else waits for humans or callbacks. ```mermaid flowchart TD; A["Developer triggers a governed operation"] --> B["Run instantiated from the spec"]; B --> C["Conditions evaluated, external items dispatched"]; C --> D["Humans sign off, systems call back"]; D --> E["Outcome computed: approve, override, or fail"]; E --> F["Action proceeds or stays blocked"]; classDef np fill:#e6faf4,stroke:#00b894,color:#1a1a1a; classDef infra fill:#eef3fb,stroke:#274a86,color:#1a1a1a; class B,C,E np; class A,D,F infra; linkStyle default stroke:#9aa5b1,stroke-width:1.5px; ``` When the run is created, nullplatform snapshots both the spec and the request context into it. Those snapshots are immutable, which has two consequences worth knowing before you author anything: - **Conditions are evaluated once**, at the moment the request is created. They don't re-evaluate while the approval sits open, so a checklist judges the request as it was submitted. - **Editing a spec never rewrites history.** In-flight runs keep evaluating the version they started with, and the audit trail shows exactly what was checked. ## What your developers see Once a checklist is live, the developer who triggers the action gets the list instead of a verdict. Items that evaluate automatically already show their result, external checks report as they call back, and anything waiting on a person says so. Manual items collect a decision and a comment from a named person, and the run keeps that on record: who signed off, what they said, and when. An override never turns a failed item into a passing one. The item stays failed and the run resolves as `approve_with_override`, so the record shows that the action went through despite the failure, and who decided it should. A failing item doesn't end the request either. By default it stays open and resumable: the developer reads which item failed, fixes the cause, and redeploys, or asks for a manual review when the call needs a human. How failures route is [configurable per action](/docs/approvals/checklist-specs#choose-what-a-failed-run-does). Every transition lands in an event trail per run, and external items keep their own logs, which is what makes a checklist answerable to an auditor: not just what was decided, but what was checked, when, and by whom. ## Learning path :::tip Want the full contract? The [items reference](/docs/approvals/checklist-items) covers every item type, behavior, status, and validation rule, and [Specs and actions](/docs/approvals/checklist-specs) covers the API, failure routing, and roles. ::: --- ## Policies URL: https://docs.nullplatform.com/docs/approvals/policies :::warning Upcoming deprecation Policies will be deprecated in favor of [checklists](/docs/approvals/checklists). Start any new guardrail as a checklist: it uses the same condition language and the same field paths, so every predicate on this page carries over unchanged, and it adds human sign-offs, external validations, and per-item audit. If you want to migrate policies to checklists, [reach out to the nullplatform team](/docs/support). ::: ## What is a policy? **Policies** define the conditions under which approval actions are granted, denied, or flagged for manual review. - They help ensure that approvals align with business rules, security requirements, and compliance policies. - By leveraging metadata, policies can evaluate real-time information to make approval decisions. ## How policies are evaluated When an action is triggered, nullplatform builds a **context object** containing the relevant metadata for that action — the user, scope, build, application, and so on. The policy's conditions are then evaluated against that context. The evaluation returns one of three outcomes: | Outcome | Meaning | |---|---| | `approve` | All conditions passed. The action is auto-approved. | | `deny` | One or more conditions failed. The action is auto-denied. | | `manual` | The result is inconclusive and requires a human reviewer. | Which outcome is ultimately applied depends on the `on_policy_success` and `on_policy_fail` settings on the **approval action** linked to the policy. For example, an action with `"on_policy_success": "approve"` and `"on_policy_fail": "manual"` will auto-approve passing requests and send failing ones to a reviewer. ```mermaid flowchart LR A[Action triggered] --> B[Context is built] B --> C{Conditions evaluated} C -->|All pass| D[on_policy_success] C -->|Any fail| E[on_policy_fail] D --> F["approve or manual"] E --> G["deny or manual"] ``` ## Using metadata in policies Policies can leverage metadata from different entities in the catalog based on the action being evaluated. This allows for precise, context-aware approval decisions. For example, you can enforce approvals based on: - Code coverage in a build process. - The security status of a deployment. - Whether a resource supports auto-scaling. - And more. By using metadata, policies dynamically adapt to specific requirements. For more details, see the [Catalog entity](/docs/catalog/getting-started) docs. #### Available entities for policy configuration The following table shows which entities provide metadata for different approval actions: | Approval actions | Entities whose metadata is available for policy configuration | | -------------------------- | -------------------------------------------------------------------------------------- | | **deployment:create** | deployment, user, scope, release, build, application, namespace, account, organization | | **scope:create** | scope, user, application, namespace, account, organization | | **scope:recreate** | scope, user, application, namespace, account, organization | | **scope:write** | scope, user, application, namespace, account, organization | | **scope:delete** | scope, user, application, namespace, account, organization | | **scope:stop** | scope, user, application, namespace, account, organization | | **parameter:read-secrets** | parameter, user, application, namespace, account, organization | | **service:action** | user, application, namespace, account | This means that once you've defined a policy using metadata, you can associate it with an approval action based on the entities it references. For example, if your policy relies on **scope metadata**, you can link it to any approval action listed above, as scope metadata is broadly available. However, if your policy references **build metadata** — like in the example below — it can only be applied to a **deployment:create** action, since build metadata is exclusive to deployments. > **Note:** For `parameter:read-secrets`, the context also includes the `dimensions` of the access request (the dimensions of the values being revealed), so conditions can reference them directly — for example, `"dimensions.environment": "production"`. See [Accessing secret values](/docs/parameters/secret-visibility#set-a-policy) for a full example. ## Adding metadata to policies Before using metadata in policies, you need to define a **catalog specification**. This outlines the structure and rules for the data you want to enforce. For example, the following catalog specification tracks **code coverage** in a **build** entity: ```json { "name": "Coverage", "description": "Build coverage report", "nrn": "organization=1", "entity": "build", "metadata": "coverage", "schema": { "type": "object", "properties": { "percent": { "type": "number", "default": 0, "minimum": 80, "maximum": 100 }, "report": { "type": "string", "format": "uri" } }, "required": ["percent", "report"], "additionalProperties": false } } ``` See the API reference for more info on how to [create a catalog specification](/docs/api/catalog-specification-create). Now that the specification is created, policies can reference this metadata to enforce coverage requirements before approving deployments. ## Create a policy To create a policy, send a [POST request](/docs/api/policy-create) to the approval policy endpoint. The `conditions` object defines the rules that must be met for an approval action to proceed. :::info Conditions' syntax Policies use [MongoDB syntax](https://www.mongodb.com/docs/manual/introduction/) for conditions, allowing you to create flexible and complex rules. ::: ### Example: Enforcing approval conditions for deployments The following policy ensures that deployments are only approved if: - Code coverage is **at least 80%**. - Security issues do not exceed **4**. - The resource supports **auto-scaling**. - The environment is either **dev** or **qa**. ```bash np approval policy create --body '{ "nrn": "organization=1:account=2:namespace=3:application=4", "name": "Enforce coverage, security, and scaling for deployments", "conditions": { "build.metadata.coverage.percent": { "$gte": 80 }, "build.metadata.security.issues": { "$lte": 4 }, "scope.capabilities.auto_scaling": true, "$or": [ { "scope.dimensions.environment": "dev" }, { "scope.dimensions.environment": "qa" } ] }, "selector": {} }' ``` ```bash curl -L -X POST 'https://api.nullplatform.com/approval/policy' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ' \ -d '{ "nrn": "organization=1:account=2:namespace=3:application=4", "name": "Enforce coverage, security, and scaling for deployments", "conditions": { "build.metadata.coverage.percent": { "$gte": 80 }, "build.metadata.security.issues": { "$lte": 4 }, "scope.capabilities.auto_scaling": true, "$or": [ { "scope.dimensions.environment": "dev" }, { "scope.dimensions.environment": "qa" } ] }, "selector": {} }' ``` ### Example: Restricting scope creation by environment Use this pattern to require manual approval whenever a scope is created directly in production, while allowing development and staging environments to proceed automatically. ```json { "nrn": "organization=1:account=2:namespace=3", "name": "Block direct scope creation in production", "conditions": { "$nor": [ { "scope.dimensions.environment": "production" } ] }, "selector": {} } ``` When combined with an approval action that has `"on_policy_fail": "manual"`, any scope creation attempt in production is flagged for review instead of going through automatically. ### Example: Restricting access to parameter secrets This policy narrows who can read secrets by restricting access to a specific application. It's designed for use with the `parameter:read-secrets` action. ```json { "nrn": "organization=1:account=2:namespace=3:application=4", "name": "Require approval to read production secrets", "conditions": { "application.id": { "$eq": 4 } }, "selector": {} } ``` You can combine this with additional conditions — such as a metadata field on the parameter or application — to enforce more specific access rules. ### Example: Auto-approving secret access by environment Requests to read secret values carry the dimensions being revealed, so a policy can decide the outcome per environment. This policy passes only for lower environments: ```json { "nrn": "organization=1:account=2:namespace=3:application=4", "name": "Auto-approve secret access in lower environments", "conditions": { "dimensions.environment": { "$in": ["development", "staging"] } }, "selector": {} } ``` Pair it with a `parameter:read-secrets` approval action that has `"on_policy_success": "approve"` and `"on_policy_fail": "manual"`: requests for `development` or `staging` values are approved automatically, while anything else — including requests for the whole parameter — waits for manual review. Keep the conditions an allowlist (like `$in`) so requests that don't specify dimensions never auto-approve. For the full setup, see [Accessing secret values](/docs/parameters/secret-visibility#set-a-policy). #### Example response If the policy is successfully created, you'll receive a response like this: ```json { "id": 1234, "nrn": "organization=1:account=2:namespace=3:application=4", "name": "Enforce coverage, security, and scaling for deployments", "conditions": { "build.metadata.coverage.percent": { "$gte": 80 }, "build.metadata.security.issues": { "$lte": 4 }, "scope.capabilities.auto_scaling": true, "$or": [ { "scope.dimensions.environment": "dev" }, { "scope.dimensions.environment": "qa" } ] }, "selector": {} } ``` :::tip The `id` field contains the unique policy ID. Be sure to save it, as you'll need it in the next steps. ::: ## Using selectors A **selector** narrows down which requests a policy applies to, acting as a pre-filter before the conditions are evaluated. - If the selector **matches** the context, conditions are evaluated normally. - If the selector **doesn't match**, the policy is skipped entirely for that request. This is especially useful when multiple policies are attached to the same approval action and you want each one to apply only to a specific subset of requests — for example, targeting a particular service specification. :::note Using selectors for service actions We recommend using selectors for services, as they allow you to target specific service specifications. For example:
Service action example ```json { "nrn": "organization=1:account=2:namespace=3:application=4", "name": "This service requires approval", "conditions": { "$or": [ { "$and": { "action.slug": "provision-service", "action.parameters.endpoint_type": "private" } }, { "$nor": [ { "action.slug": "provision-service" } ] } ] }, "selector": { "service_specification.slug": "my-service" } } ```
::: ## Updating a policy To update a policy's conditions or selector, send a [PATCH request](/docs/api/policy-update) with the fields you want to change: ```bash curl -L -X PATCH 'https://api.nullplatform.com/approval/policy/1234' \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer ' \ -d '{ "conditions": { "build.metadata.coverage.percent": { "$gte": 90 }, "scope.capabilities.auto_scaling": true } }' ``` You can update `conditions`, `selector`, or both in a single request. Fields not included in the request body remain unchanged. Changes take effect immediately for all new approval requests. ## What's next Now that you've created a policy, the next step is to link it to an **approval action** so it can be evaluated. Learn how to [associate a policy with an action.](/docs/approvals/associate-policy-with-action) Policies will be deprecated, so if you're starting a new guardrail, build it as a [checklist](/docs/approvals/checklists) instead. Checklists use this same condition language, so the conditions above carry over unchanged, and [Your first checklist](/docs/approvals/your-first-checklist) walks through a gate like these in ten minutes. An action runs on policies or on a checklist, never both. --- ## Set up approval notifications URL: https://docs.nullplatform.com/docs/approvals/set-up-notifications To keep the right people informed about approval requests, set up notifications. Nullplatform supports Slack and HTTP notifications for approvals. :::info This page covers approval-specific notification setup. For the complete channel types reference, lifecycle management, and troubleshooting, see the [Notifications documentation](/docs/notifications). ::: ### Slack To receive notifications on Slack, follow these steps: 1. Install nullplatform's Slack application. Go to `https://.app.nullplatform.io/integration` and click **Connect Slack** to authorize the application. 2. Go to Slack and add the nullplatform application to the channels. You can use the `/invite @nullplatform` command. 3. Send a [POST request](/docs/api/notification-channel-create) to create your notification channel. :::tip Set up a notification channel for the entire organization to ensure that notifications always reach a channel. ::: #### Example request: ```bash np notification channel create --body '{ "nrn": "organization=1", "description": "My Slack channel for approval notifications", "source": ["approval"], "type": "slack", "configuration": { "channels": ["my-organization-private-channel"] }, "filters": { "action": "deployment:create" } }' ``` ```bash curl -L 'https://api.nullplatform.com/notification/channel' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ' \ -d '{ "nrn": "organization=1", "description": "My Slack channel for approval notifications", "source": ["approval"], "type": "slack", "configuration": { "channels": ["my-organization-private-channel"] }, "filters": { "action": "deployment:create" } }' ``` To notify specific teams, configure channels for namespaces, accounts, or other resources. For example: ```bash { "nrn": "organization=1:account=2:namespace=3", "source": ["approval"], "type": "slack", "configuration": { "channels": ["my-namespace-private-channel"] }, "filters": { "action": "deployment:create" } } ``` :::note Request details - `nrn`: The NRN identifies the organization for which the notification channel is being created. - `source`: This specifies the source of the notifications. Here, the source is `approval`, meaning the channel will receive notifications related to approval requests. - `type`: This defines the type of notification channel. It can be either `slack` or `http` for approvals. - `configuration`: This field contains configuration details specific to the notification channel type. - `filters`: An optional field that allows notifications to be filtered based on custom criteria. ::: Once configured, you’ll receive Slack messages to approve or deny requests. Here's an example: ## Configure an HTTP notification channel HTTP notifications allow systems to programmatically respond to approval requests. To set this up, send a [POST request](/docs/api/notification-channel-create) with the endpoint details. #### Example request ```bash np notification channel create --body '{ "nrn": "organization=1", "description": "My HTTP channel for approval notifications", "source": ["approval"], "type": "http", "configuration": { "url": "https://yourdomain.com/url-you-configured", "headers": { "Authorization": "Bearer the-token" } }, "filters": { "action": "deployment:create" } }' ``` ```bash curl -L 'https://api.nullplatform.com/notification/channel' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ' \ -d '{ "nrn": "organization=1", "description": "My HTTP channel for approval notifications", "source": ["approval"], "type": "http", "configuration": { "url": "https://yourdomain.com/url-you-configured", "headers": { "Authorization": "Bearer the-token" } }, "filters": { "action": "deployment:create" } }' ``` After setting up your custom notification channel, you'll start receiving requests like these: ```json { "id": 12, "source": "approval", "created_at": "2022-12-19T20:37:42.109Z", "notification": { "approval_id": 1234, "approve_url": "https://api.nullplatform.com/approval/1234/approve?token=the-token", "deny_url": "https://api.nullplatform.com/approval/1234/deny?token=the-token", "requested_by": { "user_id": 1, "email": "alex.doe.developer@yourdomain.com", "first_name": "alex", "last_name": "doe", "avatar": null }, "action": "deployment:create", "details": {...} } } ``` :::info See examples of context details for approvals in our [Event payloads docs](/docs/notifications/event-payloads#approvals). ::: #### Responding to HTTP notifications After evaluating an approval request, respond by approving or denying it based on the policy conditions. Use the provided `approve_url` or `deny_url` and include relevant details in your response. Here’s an example of how to deny an approval due to policy violations: ```bash curl -L 'https://api.nullplatform.com/approval/:id/deny' \ -H 'Content-Type: application/json' \ --data-raw '{ "reviewer": { "name": "Deployments Review Team", "email": "reviewer@yourdomain.com", "nullplatform_user_id": null }, "message": "Approval denied due to policy conditions not being met.", "details": { "restriction_reason": "Security policy violation: The number of security issues exceeds the allowed threshold.", "expected_max_security_issues": 4, "actual_security_issues": 6, "policy_name": "Enforce coverage, security, and scaling for deployments" } }' ``` :::note Request details - `reviewer`: Information about the decision maker (name, email, and optional user ID). - `message`: A short explanation for the decision. - `details`: Additional context for the decision. All fields are **optional**, but including them can help provide clarity to the requester in the platform. ::: :::info For more details on notifications, refer to the [Notifications](/docs/notifications) docs. ::: ## What's next Now that you've set up your notification channel, learn how [approval requests progress through statuses](/docs/approvals/approval-requests) — from pending through review, approval, and execution. --- ## Your first checklist URL: https://docs.nullplatform.com/docs/approvals/your-first-checklist ## What you'll build A small checklist on your staging deployments: one condition that checks build coverage, and one manual sign-off. Nothing to host, no workflow yet, no external system. What matters is the loop around it: describe, connect, dry-run, deploy, update. Every checklist you build after this one is that same loop with more interesting items, up to [fully autonomous compliance](/docs/tutorials/ai-architecture-compliance). ## Before you start You need three things: - **Checklists enabled for your organization.** It's an early release feature. If you don't see it yet, [reach out to us](https://docs.nullplatform.com/docs/support). - **A nullplatform API key.** See [API keys](/docs/authorization/api-keys) to create one. Creating specs takes the `secops`, `ops`, or `admin` role. - **The np-checklist skill**, part of the [nullplatform AI plugins](https://github.com/nullplatform/ai-plugins) for your coding assistant (Claude Code, Kiro, Cursor, and others). Follow [Set up the AI plugins](/docs/ai-ops/ai-plugins) to install them. ## 1. Describe the two checklist items you want In a session of your coding assistant with the plugin installed, describe the guardrail you want: ``` /np-checklist create a checklist named first-gate for all deployments in staging across my account: - A condition requiring build coverage above 80%. - A manual sign-off titled "Release owner sign-off", so a person confirms before the deploy goes out. Don't connect it to anything yet, I'll do that next. ``` :::tip Checklists scope as narrowly as you like. If a whole account feels too broad to experiment on, describe the same checklist for a single namespace or application, and widen it once you trust it. ::: The skill turns that into a **checklist specification**, spec for short: the versioned definition of your two items. Nullplatform validates the spec before saving it, so a malformed condition path or an item id that collides with a [reserved word](/docs/approvals/checklist-items#item-ids) comes back as an error in the conversation, not on your first deploy. Two details worth noticing in the definition it shows: - The condition's query addresses `build.metadata.coverage.lines.percent` with no `context.` prefix, the same [condition language policies use](/docs/approvals/policies). - Both items declare `behavior: gate`, so either one blocks the deploy when it fails. Items can also be `informational` (report without ever blocking) or `override` (let the deploy through even though a `gate` item failed). > 💡 **Tip:** Ask the skill to show the derived outcome expression. For this spec it reads `coverage_gate.passed AND release_signoff.passed`, which is exactly the sentence you meant. #### ✅ Checkpoint The skill reports the spec created, with an id like `spec_abc123def456ghij`, and its two items listed. ## 2. Connect it to your staging deploys A spec does nothing until something points at it. Attach it in the same conversation: ``` now connect first-gate so it governs those staging deploys (create whatever's needed) ``` The skill creates or reuses an [approval action](/docs/approvals/actions), which is the record that says which operations the checklist governs: here, creating a deployment in staging. You don't have to configure actions yourself for this tutorial. You will when you want finer control, like a different checklist per environment. One rule to know now: an action runs on a checklist **or** on [policies](/docs/approvals/policies) (to be deprecated), never both. If the action already has policies, the skill says so. Reach out to the nullplatform team to move it onto a checklist. #### ✅ Checkpoint The skill confirms the connection and reports the action pointing at your spec. From this moment, staging deploys in your account wait on the checklist. ## 3. Dry-run it against a sample build Your checklist is live. Before spending a real deploy on it, preview what one would face: ``` dry-run first-gate against a sample context where build coverage is 72% ``` The dry run previews the run without creating it, showing the list as a developer would see it: the coverage item failed at 72%, the sign-off pending, the run blocked. Run it again at 85% and the coverage item passes, leaving only the sign-off. Two things to take from this. First, **conditions are evaluated once, when the request is created**, against a snapshot of it. They don't re-evaluate while the approval sits open, so a checklist judges the request as it was submitted. Second, the dry run needs step 2: it resolves the approval action first, then evaluates whatever spec that action points at. That's why you connect before you dry-run. > 💡 **Tip:** Your checklist has a preview of its own in the nullplatform UI. Go to **Platform settings > Approvals > Checklists**, open `first-gate` from the list, and flip the preview through its states: **Not started**, **In progress**, **Everything passed**, and **Everything failed**. No request involved. #### ✅ Checkpoint You've seen the same spec block at 72% coverage and wait on the sign-off at 85%, without creating a single approval request. ## 4. Deploy, then sign off Trigger a deployment of any application in staging. Instead of going straight out, it shows your list: the coverage condition already resolved, because it evaluated when the request was created, and the sign-off waiting for a person. Now sign it off: open the manual item, leave a comment, and confirm. The item passes, the outcome is computed, and the deploy button unlocks. The run keeps your sign-off on record: who signed, what they said, and when. Had the coverage condition failed, the request would stay open and resumable by default: fix the build, redeploy, and the new request gets its own run. Failure routing is [configurable per action](/docs/approvals/checklist-specs#choose-what-a-failed-run-does). #### ✅ Checkpoint The deploy waited, you signed, and it went out: the run shows `approve` with both items passed and your name on the sign-off. ## 5. Update it with a new version Raise the coverage threshold, again in the same conversation: ``` update first-gate: require coverage above 85 instead of 80 ``` The skill doesn't edit the spec in place. It publishes **version 2**, a new spec id carrying the stricter condition, and points the action at it. Version 1 stays as it was. Three rules follow from that: - **Spec versions are immutable.** An update is always a new version, so nobody can quietly rewrite the rule a deploy was judged against. - **The action picks the version that governs.** Deploys kept facing version 1 until the action moved, which means a half-finished edit never leaks into a live deploy. - **Finished runs keep their snapshot.** The run you approved in step 4 still shows coverage above 80, because that's what it enforced. Opening a past run shows what was checked then, not what the spec says now. Dry-run it again at 82% to see the difference: the same context that passed version 1 now fails. The nullplatform UI tells the same story. Back on your checklist's page, under **Platform settings > Approvals > Checklists**, the version history lists both versions and compares any two of them side by side: #### ✅ Checkpoint The spec lists two versions, the action points at version 2, and your step 4 run still shows the version 1 items it was resolved against. All done! 🎉 When you're finished experimenting, ask the skill to detach the spec from the action so staging deploys stop waiting on it. ## Where to next You now know the whole loop: describe, connect, dry-run, deploy, update. Every checklist is this loop with more interesting items, so point it at a real guardrail: 🚀 - [Gate deploys on a Jira ticket your team already uses](/docs/tutorials/jira-approval-gate): swap the manual item for a ticket a workflow manages - [Items reference](/docs/approvals/checklist-items): everything an item can be, including structured inputs and validation rules like four-eyes - [Specs and actions](/docs/approvals/checklist-specs): the API behind what the skill just did --- ## Authentication URL: https://docs.nullplatform.com/docs/authentication/ Authentication is a fundamental aspect of securing access to nullplatform. This document provides an overview of the supported authentication methods: username and password, and SAML authentication with external identity providers (IDPs). ## Username & password Username and password authentication, commonly known as 'basic authentication,' is our default method for user verification. With this method, you have to invite organization users, and each new member will receive an invitation email containing a temporary password. ## SAML with external identity providers SAML (Security Assertion Markup Language) is a widely adopted standard for enabling Single Sign-On (SSO) across multiple applications. This document will guide you through the setup and configuration of SAML authentication with external IDPs, ensuring secure and seamless access to your application. In the following sections, we’ll walk through the configuration steps for implementing both authentication methods. ## API key API key authentication is an alternative method for securing access to nullplatform. This authentication method is particularly useful for programmatic access and automation scenarios. Users can obtain an API key to authenticate and interact with nullplatform programmatically. Read the [API key](../authorization/api-keys.md) section for more information about this method. --- ## Granting AWS access to nullplatform URL: https://docs.nullplatform.com/docs/authentication/aws_auth_modes Nullplatform uses the AWS API to interact with AWS services and supports several methods to establish secure access to AWS. ## Required users/roles Nullplatform divides responsibilities into three distinct parts, each requiring specific users or roles: - **Application Management**: This component handles the creation of AWS components during application creation or import. The most critical creation is the `ECR repository`. - **Telemetry**: Responsible for processing logs and metrics, communicating with AWS CloudWatch. - **Scope & Deployment Manager**: Controls scope and deployment, communicating with AWS EC2, Lambda, and Elastic Load Balancing APIs. ## Access key and secret key :::warning While this method is simple, it is NOT recommended. Please try [cross-account roles](#cross-account-roles-recommended) instead. ::: The simplest way to configure nullplatform's communication with the AWS API is by creating AWS users and generating API credentials for each user. To configure this authentication mode, the required `NRN` keys are as follows: - **Application Management**: - `aws.application_workflow_access_key` - `aws.application_workflow_secret_access_key` - **Telemetry**: - `aws.log_reader_access_key` - `aws.log_reader_secret_access_key` - **Scope & Deployment Manager**: - `aws.region` - `aws.scope_workflow_access_key` - `aws.scope_workflow_secret_access_key` ## Cross-Account Roles (recommended) In this approach, instead of using keys and secrets, nullplatform employs Amazon roles. These roles allow you to grant access to specific resources in another AWS account, ensuring that only authorized services (such as nullplatform instances) can connect to AWS. Nullplatform defines unique roles for each component, which need to be trusted in your AWS account. The roles are as follows: | **Component** | **Nullplatform Role** | | -------------------------- | ------------------------------------------------------- | | Application Management | arn:aws:iam::283477532906:role/application_manager | | Telemetry | arn:aws:iam::283477532906:role/telemetry_manager | | Scope & Deployment Manager | arn:aws:iam::283477532906:role/scope_and_deploy_manager | To configure this authentication mode, the required `NRN` keys are: - **Application Management**: - `aws.application_workflow_role` - (optional) `aws.application_workflow_role_external_id` - **Telemetry**: - `aws.log_reader_role` - (optional) `aws.log_reader_role_external_id` - **Scope & Deployment Manager**: - `aws.scope_workflow_role` - (optional) `aws.scope_workflow_role_external_id` *In all cases, the role is configured in your AWS account.* ### Granting access to multiple AWS accounts using an intermediate role In scenarios involving multiple AWS accounts, an `intermediate role` can be used. Nullplatform first authenticates with the intermediate role, and then uses that role to authenticate with the required component role. ```mermaid graph LR nullaccount[nullplatform role] --> intermediaterole(assume intermediate role) --> finalrole[assume final role] ``` To configure this authentication mode, the required `NRN` keys are: - **Application Management**: - `aws.application_workflow_role` - `aws.application_workflow_intermediate_role` - (optional) `aws.application_workflow_role_external_id` - (optional) `aws.application_workflow_intermediate_role_external_id` - **Telemetry**: - `aws.log_reader_role` - `aws.log_reader_intermediate_role` - (optional) `aws.log_reader_role_external_id` - (optional) `aws.log_reader_intermediate_role_external_id` - **Scope & Deployment Manager**: - `aws.scope_workflow_role` - `aws.scope_workflow_intermediate_role` - (optional) `aws.scope_workflow_role_external_id` - (optional) `aws.scope_workflow_intermediate_role_external_id` *In all cases, the roles are configured in your AWS account.* ### Required permissions for each role/user When a new nullplatform account is set up, our team configures these permissions: - **Application management** - ECR - `ecr-public:BatchCheckLayerAvailability` - `ecr-public:PutImage` - `ecr-public:CompleteLayerUpload` - `ecr-public:UploadLayerPart` - `ecr-public:InitiateLayerUpload` - `ecr:GetDownloadUrlForLayer` - `ecr:BatchGetImage` - `ecr:CompleteLayerUpload` - `ecr:UploadLayerPart` - `ecr:InitiateLayerUpload` - `ecr:BatchCheckLayerAvailability` - `ecr:PutImage` - `ecr:CreateRepository` - `sts:GetServiceBearerToken` - `ecr:GetAuthorizationToken` - `ecr-public:GetAuthorizationToken` - **Telemetry**: - `ec2:DescribeInstances` - `cloudwatch:GetMetricData` - `cloudwatch:ListMetrics` - `logs:Describe*` - `logs:Get*` - `logs:List*` - `logs:StartQuery` - `logs:StopQuery` - `logs:TestMetricFilter` - `logs:FilterLogEvents` - **Scope & Deployment manager**: - Lambda - `lambda:CreateFunction` - `lambda:DeleteProvisionedConcurrencyConfig` - `lambda:GetFunctionConfiguration` - `lambda:ListProvisionedConcurrencyConfigs` - `lambda:GetProvisionedConcurrencyConfig` - `lambda:ListLayers` - `lambda:ListLayerVersions` - `lambda:DeleteFunction` - `lambda:GetAlias` - `lambda:ListCodeSigningConfigs` - `lambda:UpdateFunctionUrlConfig` - `lambda:CreateFunctionUrlConfig` - `lambda:UpdateFunctionEventInvokeConfig` - `lambda:DeleteFunctionCodeSigningConfig` - `lambda:InvokeFunctionUrl` - `lambda:ListFunctions` - `lambda:GetEventSourceMapping` - `apigateway:*` - `lambda:ListAliases` - `lambda:GetFunctionUrlConfig` - `lambda:GetFunctionCodeSigningConfig` - `lambda:UpdateAlias` - `lambda:UpdateFunctionCode` - `lambda:ListFunctionEventInvokeConfigs` - `lambda:PutRuntimeManagementConfig` - `lambda:ListFunctionsByCodeSigningConfig` - `lambda:GetFunctionConcurrency` - `lambda:PutProvisionedConcurrencyConfig` - `lambda:ListEventSourceMappings` - `lambda:PublishVersion` - `lambda:DeleteEventSourceMapping` - `lambda:CreateAlias` - `lambda:ListVersionsByFunction` - `lambda:GetLayerVersion` - `lambda:PublishLayerVersion` - `lambda:GetAccountSettings` - `lambda:CreateEventSourceMapping` - `lambda:GetLayerVersionPolicy` - `lambda:PutFunctionConcurrency` - `lambda:DeleteCodeSigningConfig` - `lambda:ListTags` - `lambda:AddPermission` - `lambda:TagResource` - `lambda:GetRuntimeManagementConfig` - `lambda:DeleteLayerVersion` - `lambda:PutFunctionEventInvokeConfig` - `lambda:DeleteFunctionEventInvokeConfig` - `lambda:CreateCodeSigningConfig` - `lambda:PutFunctionCodeSigningConfig` - `lambda:UpdateEventSourceMapping` - `lambda:UpdateFunctionCodeSigningConfig` - `lambda:GetFunction` - `lambda:UpdateFunctionConfiguration` - `lambda:ListFunctionUrlConfigs` - `lambda:UpdateCodeSigningConfig` - `lambda:GetFunctionEventInvokeConfig` - `lambda:DeleteAlias` - `lambda:DeleteFunctionConcurrency` - `lambda:GetCodeSigningConfig` - `lambda:DeleteFunctionUrlConfig` - `lambda:GetPolicy` - Balancer - `ec2:DescribeAccountAttributes` - `ec2:DescribeAddresses` - `ec2:DescribeInternetGateways` - `ec2:DescribeSecurityGroups` - `ec2:DescribeSubnets` - `ec2:DescribeVpcs` - `ec2:DescribeVpcClassicLink` - `ec2:DescribeInstances` - `ec2:DescribeNetworkInterfaces` - `ec2:DescribeClassicLinkInstances` - `ec2:DescribeRouteTables` - `ec2:DescribeCoipPools` - `ec2:GetCoipPoolUsage` - `ec2:DescribeVpcPeeringConnections` - `cognito-idp:DescribeUserPoolClient` - `arc-zonal-shift:*` - Autoscaling - `ec2:DescribeAccountAttributes` - `ec2:DescribeAvailabilityZones` - `ec2:DescribeImages` - `ec2:DescribeInstanceAttribute` - `ec2:DescribeInstances` - `ec2:DescribeKeyPairs` - `ec2:DescribeLaunchTemplateVersions` - `ec2:DescribePlacementGroups` - `ec2:DescribeSecurityGroups` - `ec2:DescribeSpotInstanceRequests` - `ec2:DescribeSubnets` - `ec2:DescribeVpcClassicLink` - `ec2:RunInstances` - `ec2:CreateTags` - `ec2:CreateLaunchTemplate` - `ec2:CreateLaunchTemplateVersion` - `ec2:DeleteLaunchTemplate` - `ec2:DeleteLaunchTemplateVersions` - `ec2:DeleteTags` - Route53 - `route53:CreateHostedZone` - `route53:ChangeResourceRecordSets` - `route53:ListResourceRecordSets` - `route53domains:*` - ECR - `ecr:DescribeImages` - `ecr:DescribeRegistry` - `ecr:DescribeRepositories` - `ecr:GetDownloadUrlForLayer` - `ecr:ListImages` - `ecr:GetAuthorizationToken` - `ecr:BatchGetImage` - S3 (for parameters) - `s3:PutObject` - `s3:PutObjectAcl` - `s3:PutObjectVersionAcl` - `s3:PutObject` - `s3:PutObjectAcl` - `s3:PutObjectVersionAcl` - `s3:GetObject` - Secrets (for parameters) - `secretsmanager:GetSecretValue` --- ## Logging in with Google URL: https://docs.nullplatform.com/docs/authentication/google Nullplatform enables users to sign in through third-party federation, using external identity providers (IdPs) like Google. To add Google as an external IdP and allow your users to log in to nullplatform with their Google accounts, follow these steps: 1. **Add a custom SAML app on Google**: - Begin by adding a custom SAML (Security Assertion Markup Language) app in your Google account. - Configure this app according to the setup instructions provided. 2. **Before you start, contact us**: - [Reach out to our support team](/docs/support) to obtain the necessary information for configuration. You'll need the following details: - **ACS (Consumer) URL**: This is the Assertion Consumer Service URL, which allows nullplatform to receive authentication responses from Google. - **Entity ID**: The Entity ID uniquely identifies nullplatform as a service provider in the SAML exchange. By following these steps and obtaining the required information, you'll enable seamless Google-based login for your users on nullplatform. ## Add a Custom SAML App on Google 1. Log in to the [Google Workspace Admin Console](https://admin.google.com/) 2. Choose **Web and mobile app** under **Apps** on the left side of the page. 3. Click the **Add custom SAML app** under **Add app**. 4. Enter the **App name**, **Description**, and click **CONTINUE**. 5. Download IdP metadata as you will need it to share it with the nullplatform team, then click **CONTINUE**. 6. Next enter the **ACS URL** and **Entity ID**, both of which you obtained from the nullplatform support. 7. Select **EMAIL** from the drop-down menu for **Name ID Format**. 8. Select **Basic Information > Primary email** from the drop-down menu for **Name ID** and click **CONTINUE**. 9. On the Attributes tab, click **ADD MAPPING** to add additional parameters to include in the SAML payload. Mapping should be: | Google Directory attributes | App attributes | |----------------------------------- |---------------- | | Basic Information > Primary email | email | | Basic Information > First name | given_name | | Basic Information > Last name | family_name | 10. Click **FINISH** to complete. 11. Once the application is created, navigate to the **User access** card. 12. Set the **Service status** as **ON for everyone** and click **Save**. 13. IdP setup is now complete. Share your downloaded IdP metadata with the nullplatform support team, then you will see **Login with Google** in your nullplatform organization account. --- ## Logging in with Okta URL: https://docs.nullplatform.com/docs/authentication/okta Nullplatform enables users to sign in through third-party federation, using external identity providers (IdPs) like Okta. To add Okta as an external IdP and allow your users to log in to nullplatform with their Okta accounts, follow these steps: 1. **Create an app integration on Okta**: - Begin by adding an app integration in your Okta account. - Configure this app according to the setup instructions provided. 2. **Before you start, contact us**: - [Reach out to our support team](/docs/support) to obtain the necessary information for configuration. You'll need the following details: - **Single sign-on URL**: This URL is used to redirect users to Okta for authentication. It's a crucial part of the Single Sign-On (SSO) process. - **Audience URI (SP Entity ID)**: The Audience URI, also known as the Service Provider (SP) Entity ID, uniquely identifies your service within the SAML exchange. It ensures secure and trusted communication between Okta and your application. By following these steps and obtaining the required information, you'll enable seamless Okta-based login for your users on nullplatform. ## Create App Integration on Okta 1. Log in to [Okta](https://www.okta.com). 2. Go to the Admin dashboard by clicking the **Admin** button in the top-right corner of the page. 3. On the left-hand side of the page, under **Applications**, click **Applications**. 4. Click the **Create App Integration** button. 5. Select **SAML 2.0** for the sign-in method, then select **Next**. 6. Complete the **App name** and **App logo** (*optional*), then select **Next**. 7. The next page describes the SAML settings for your app. 8. The **Single sign-on URL** and **Audience URI (SP Entity ID)** are provided by nullplatform support. 9. Make sure the **Use this for Recipient URL and Destination URL** box is checked. 10. Leave the **Default RelayState** blank. 11. Select *Unspecified* for **Name ID format**. 12. Select *Email* for **Application username**. 13. Under **Attribute Statements**, configure the following: Name | Name format | Value :-----------: | :---------: | :--------------: email | Unspecified | user.email given_name | Unspecified | user.firstName family_name | Unspecified | user.lastName Mapping should be: 14. Click **Next**. 15. Select **I'm an Okta customer adding an internal app** for **Are you a customer or partner?**. 16. Select **It's required to contact the vendor to enable SAML** for **App Type**. 17. Fill **Which app pages did you consult to configure SAML?** input with `https://docs.nullplatform.com/docs/authentication/okta`. 18. Click **Finish**. 19. On the **Sign On** tab, click **Copy** under **Metadata details** and share the **Metadata URL** with nullplatform support. 20. On the **Assignments** tab, click **Assign** to assign the application to selected **People** or **Groups**. 21. IdP setup is now complete. Share your copied **Metadata URL** with the nullplatform support team, then you will see **Login with Okta** in your nullplatform organization account. --- ## Configure API keys with IaC tools URL: https://docs.nullplatform.com/docs/authorization/api-keys-iac You can create and manage API keys using Infrastructure as Code (IaC) tools like [OpenTofu](https://opentofu.org/) or [Terraform](https://developer.hashicorp.com/terraform). We've created a plugin to help you configure API keys: [API key resource plugin](https://registry.terraform.io/providers/nullplatform/nullplatform/latest/docs/resources/api_key). :::info You need an [Admin or Ops role](/docs/authorization/roles) to create and manage API keys. ::: ### Why use IaC for API keys? Managing API keys with IaC tools helps you: - Ensure consistent configurations across environments. - Save time by automating API key setup. - Track changes with version control. ## Get started Follow these steps to set up API keys using OpenTofu or Terraform: :::warning Secure your API keys - The API key's secret value is **only stored in the `tfstate` file** and **cannot be retrieved again**, even if you import the resource. - Ensure the `tfstate` file is **securely stored** and protected from unauthorized access. ::: 1. Create a configuration file. Below is an example configuration to set up an API key for the nullplatform API. Replace the placeholder values in the example with your details. ```hcl terraform { required_providers { nullplatform = { source = "nullplatform/nullplatform" } } } provider "nullplatform" {} resource "nullplatform_api_key" "my_api_key" { name = "Example API Key Name" grants { nrn = "organization=1:account=1" role_slug = "ops" } grants { nrn = "organization=1:account=1" role_slug = "admin" } tags { key = "example" value = "true" } } output "my_api_key_value" { value = nullplatform_api_key.my_api_key.api_key sensitive = true } ``` 2. Initialize the IaC tool. Run the initialization command for your chosen tool: ```bash tofu init ``` ```bash terraform init ``` 3. Review your changes. Preview the changes before applying them: ```bash tofu plan ``` ```bash terraform plan ``` 4. Apply the configuration. If your changes look good, apply the configuration: ```bash tofu apply ``` ```bash terraform apply ``` ## Verify API keys After applying the configuration: 1. Navigate to **Platform settings > API keys** in the nullplatform UI. 2. Verify that your API keys are configured correctly. --- ## Manage your API keys URL: https://docs.nullplatform.com/docs/authorization/api-keys An API key lets you interact with nullplatform programmatically and is ideal for automation and system tasks. For machine users like CI workflows, create API keys with specific roles to control access and permissions. To act as yourself instead, from your own scripts or an AI assistant, use a [personal access token](/docs/authorization/personal-access-tokens). **Keep in mind:** API keys are sensitive, so handle them carefully to prevent unauthorized access. :::info You need an [Admin or Ops role](/docs/authorization/roles) to create and manage API keys. ::: You can create and manage API keys through the UI, CLI, or API. ## How API keys fit into authorization API keys act as **machine users**, and each API key has one or more **grants**. #### Grants Grants define the API key's access: **where** it applies and **what the key may do** there. - **Resource**: The resource where permissions apply, for example *account: main*. - **Permissions**: What the key may do at that resource. Usually one or more roles, for example *Agent*. You can also start from roles and adjust single permissions, or hand-pick them one by one as **Custom** permissions. > ℹ️ **Note:** You can add multiple grants, including several on the same resource. Whichever way you define the permissions, each grant resolves to one set of actions the key may perform on that resource: ```mermaid flowchart LR K["**API key**"] G["**Grant**on a resource"] R["Roles"] A["Roles, adjustedby hand"] P["Hand-pickedpermissions"] E["**What the key may do**on that resource"] K --> G G --> R G --> A G --> P R --> E A --> E P --> E classDef np fill:#e6faf4,stroke:#00b894,color:#0b1e3f,stroke-width:1.5px; classDef shape fill:#eef3fb,stroke:#274a86,color:#0b1e3f,stroke-width:1.5px; class K,G,E np; class R,A,P shape; linkStyle 0,1,2,3 stroke:#0b1e3f,stroke-width:2px; linkStyle 4,5,6 stroke:#00b894,stroke-width:2px; ``` - **Roles**: the key holds exactly what the roles grant. The Role field lists them, for example *Ops, Developer*. - **Roles, adjusted by hand**: you start from roles and tick or untick single permissions in Customize permissions. - **Hand-picked permissions**: you tick the permissions one by one with no role selected. The Role field shows **Custom**. You can only hand out what you could grant yourself on that resource: the permissions you hold there, plus the ones carried by roles you may assign to an API key. For more details on grants and roles, see [Roles](/docs/authorization/roles) and [Grants and permissions](/docs/authorization/grants). ## Using the UI Go to **Platform settings > API keys** and click **Create API key**. Walk through the form below: :::warning Save your API key securely The API key is **displayed only once**, right after you generate it. Copy it and store it in a secure location, as it cannot be retrieved later. ::: To edit or delete a key later, open its **⋮** menu in the list and click **View or edit** or **Delete**. ## Using the CLI or API ### Create an API key Send a [POST request](/docs/api/api-key-create) to create an API key. Here's an example request: ```bash np api-key create \ --body '{ "name": "my-machine-process-that-will-access-nullplatform", "grants": [ { "nrn": "organization=1:account=2:namespace=3:application=4", "role_slug": "admin" // Alternatively, use "role_id". For example: "696188987" } ], "tags": [ { "key": "CI", "value": "main" } ] }' ``` ```bash curl -L -X POST 'https://api.nullplatform.com/api_key' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -d '{ "name": "my-machine-process-that-will-access-nullplatform", "grants": [ { "nrn": "organization=1:account=2:namespace=3:application=4", "role_slug": "admin" // Alternatively, use "role_id". For example: "696188987" } ], "tags": [ { "key": "CI", "value": "main" } ] }' ``` Where: - `grants` defines the access permissions for the API key. - `nrn` is where the API key's role is assigned. - `role_slug` is the slug of the role assigned to the API key for the specified NRN. You can also provide the `role_id` instead of `role_slug`. #### Example response You'll receive a `2XX` response like this: ```json { "id": "123", "name": "my-machine-process-that-will-access-nullplatform", "api_key": "AAAA.1234567890abcdef1234567890abcdefPTs=", // Your new API key. "masked_api_key": "AAAA.xxxxxxxxxxxxxxxxxxxxxPTs=", "tags": [ { "key": "CI", "value": "main" } ], "grants": [ { "nrn": "organization=1:account=2:namespace=3:application=4", "role_slug": "admin", "role_id": 696188987 } ], "owner_id": 1595, "last_used_at": null, "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` Where: `api_key` is your newly created API key. :::warning Save your API key securely. The API key is **displayed only once**. Make sure to store it in a secure location, as it cannot be retrieved later. ::: ### Grant access to API keys In the UI, a grant is one row of the form: the resource, the roles, and whatever you changed in **Customize permissions**. Through the API, that same grant is an item of the `grants` field, in one of three shapes: - Roles only: `role_slug` or `role_id`, one item per role. - Roles adjusted in Customize permissions: `inherits`, plus `actions.add` and `actions.remove`. - Custom permissions, ticked with no role selected: `actions`. To grant a role, include the following in your request to [create](/docs/api/api-key-create) or [update](/docs/api/api-key-update) an API key: - `nrn` specifies the resource where the API key's role will be assigned. - `role_slug` is the slug of the role you want to assign to the API key for the specified NRN. You can also provide the `role_id` instead of `role_slug`. ```json "grants": [ { "nrn": "organization=1:account=2:namespace=3:application=4", "role_slug": "admin" // Alternatively, use "role_id": "696188987" } ] ``` In this example, the `admin` role is assigned to the NRN `organization=1:account=2:namespace=3:application=4`. This allows the API key to handle tasks like creating, modifying, and deleting applications and their scopes for the resource. :::info For the complete list of available role IDs, see [Roles](/docs/authorization/roles). ::: To grant several roles on the same resource, repeat the item: ```json "grants": [ { "nrn": "organization=1:account=2", "role_slug": "ops" }, { "nrn": "organization=1:account=2", "role_slug": "developer" } ] ``` :::warning Name roles one way Use `role_id` in every grant of a key, or `role_slug` in every grant, never one in some and the other in others. Every grant of a key is resolved the same way, so a mixed request fails. ::: ### Grant custom permissions When no existing role is the right size, a grant can name actions directly, which is what the UI shows as **Custom** in the Role field. Instead of `role_slug`, list them in `actions`: ```json "grants": [ { "nrn": "organization=1:account=2:namespace=3", "actions": ["application:read", "deployment:create"] } ] ``` This key can read applications and create deployments in that namespace, and nothing else. Nullplatform creates a role private to the key to hold those actions. You don't manage it, it never appears among your roles, and it's deleted with the key. :::info You can only grant what you could hand out yourself At a given resource, that's the actions you hold there plus the ones carried by roles you may assign to an API key there. To see that ceiling for an NRN, [list your roles](/docs/api/user-role-list) with `include=actions`. ::: ### Inherit existing roles and adjust them A grant can also start from roles that already exist and change the result. Use `inherits` for the roles, then `actions.add` and `actions.remove`: ```json "grants": [ { "nrn": "organization=1:account=2", "inherits": ["ops", "developer"], "actions": { "add": ["application:delete"], "remove": ["deployment:create"] } } ] ``` This key gets everything `ops` and `developer` grant, plus `application:delete`, minus `deployment:create`. Listing two roles in one `inherits` is not the same as granting them as two items. Two items give the key two independent roles, and there's nothing to subtract from. One `inherits` produces a single role, and that role is what `actions.remove` acts on. An inherited grant follows its roles. If one of them later gains an action, the key gains it too, unless it's in `actions.remove`. That's the difference from a literal `actions` list, which never changes on its own. A few rules apply. You need to be allowed to assign each inherited role to an API key at that resource, the same check a grant by `role_slug` passes. What you add by hand is measured against what you can hand out there, while removing never is. `actions.remove` can only name actions the inherited roles carry, an action can't be in both lists, and the grant can't end up empty. You can inherit by role ID instead of slug, but a role private to another API key can't be inherited. ### What the API returns for these grants A grant you wrote with `actions` or `inherits` doesn't come back in the shape you sent. The response reports what the grant resolved to. This example is an inherited grant, which carries the most fields: ```json "grants": [ { "nrn": "organization=1:account=2", "role_id": 841203556, "role_slug": "apikey:123:0", "actions": ["application:read", "application:delete"], "inherits": [ { "id": 708509758, "slug": "ops", "organization_id": null }, { "id": 700317756, "slug": "developer", "organization_id": null } ], "added": ["application:delete"], "removed": ["deployment:create"] } ] ``` Where: - `actions` is the effective set: everything the grant authorizes, after inheritance and removals. - `inherits` names the roles that were merged. `organization_id` is `null` for a platform role, and your organization's ID for a role of your own or your override of a platform one. `added` and `removed` report the adjustment you sent. - `role_id` and `role_slug` name the private role holding all this. It's an implementation detail: nullplatform recreates it whenever you replace the key's grants, so don't store it or reference it anywhere. A grant you wrote with `actions` returns `nrn`, `actions`, `role_id` and `role_slug` only: there's nothing inherited to report, so `inherits`, `added` and `removed` are absent. A grant naming an existing role returns just `nrn`, `role_id` and `role_slug`. :::note Aliased actions come back under the alias Some actions have an alias, a second name they're exposed under. Such an action is listed by its alias, not by the name it's stored as. ::: ### Generate an access token Use your new API key to create an access token, which allows you to authenticate and make API calls with nullplatform. :::note These instructions are for API keys and machine users. If you're a human user, check the [Authorization](/docs/authorization/) article. ::: Send a [POST request](/docs/api/access-token-create) to get your token. You can use either: - your API key, or - your username and password Here are examples: ```bash curl -L -X POST 'https://api.nullplatform.com/token' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -d '{ "api_key": "AAAA.1234567890abcdef1234567890abcdefPTs=" }' ``` ```bash np token create \ --body '{ "api_key": "AAAA.1234567890abcdef1234567890abcdefPTs=" }' ``` Instead of an API key, you can send the username, password, and organization ID. Like this: ```json { "username": "alex.doe@email.com", "password": "your_password", "organization_id": "1234" } ``` **Response** You'll receive a `2XX` response like this: ```json { "refresh_token": "epJjdHky...", // A long-lived token used to obtain new access tokens. "access_token": "epJraWQy...", // A short-lived bearer token for making API calls. "organization_id": 123245, // The unique ID of the organization. "token_expires_at": 1700000000000 } ``` ### Renew an access token When your access token expires, you can make a [POST request](/docs/api/access-token-create) to create a new one. You'll need your `refresh_token` and `organization_id` for the request. Example request: ```bash np token create \ --body '{ "refresh_token": "epJjdHky...", "organization_id": "123245" }' ``` ```bash curl -L -X POST 'https://api.nullplatform.com/token' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -d '{ "refresh_token": "epJjdHky...", "organization_id": "123245" }' ``` Example response: ```json { "access_token": "eyJra3JQb...", // Your renewed access_token for making API calls. "organization_id": 123245 } ``` ### Update your API key To update an existing API key, send a [PATCH request](/docs/api/api-key-update) with the details you want to change. If you include `grants`, the list you send replaces the key's grants. Only what changes is measured against what you can hand out, so a grant you send back unchanged is kept even if it holds permissions you don't. :::note You'll need the API key `id` for this request. To get the API key ID, send a [GET request](/docs/api/api-key-list) to list all the API keys. ::: Here's an example request to update the API key name and tags: ```bash np api-key patch \ --id 123 \ --body '{ "name": "updated-machine-process", "tags": [ { "key": "CI", "value": "updated" } ] }' ``` ```bash curl -L -X PATCH 'https://api.nullplatform.com/api_key/123' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -d '{ "name": "updated-machine-process", "tags": [ { "key": "CI", "value": "updated" } ] }' ``` Where: - `id`: The unique ID of the API key you want to update (e.g., `123`). - `name`: The **updated** name for the API key. - `tags`: The **updated** tags for the API key. Example response: ```json { "id": "123", "name": "updated-machine-process", "masked_api_key": "AAAA.xxxxxxxxxxxxxxxxxxxxxPTs=", "tags": [ { "key": "CI", "value": "updated" } ], "grants": [ { "nrn": "organization=1:account=2:namespace=3:application=4", "role_id": 696188987, "role_slug": "admin" } ], "owner_id": 1595, "last_used_at": "2025-01-10T00:00:00Z", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2025-01-10T00:00:00Z" } ``` Where: - `name`: The **updated** name for the API key. - `tags`: The **updated** tags for the API key. - `updated_at`: The time when the API key was last updated. ### Delete your API key To permanently remove an API key, send a [DELETE request](/docs/api/api-key-delete). :::note You'll need the API key `id` for this request. To get the API key ID, send a [GET request](/docs/api/api-key-list) to list all the API keys. ::: Here's an example request: ```bash np api-key delete \ --id 123 \ --auth "Bearer {{access_token}}" ``` ```bash curl -L -X DELETE 'https://api.nullplatform.com/api_key/:id' \ -H 'Authorization: Bearer ' ``` You'll receive a `204` response, confirming your API key has been removed successfully. --- ## Authorization URL: https://docs.nullplatform.com/docs/authorization/ Nullplatform uses role-based access control (RBAC). > 💡 The key idea is simple: a role defines what actions are allowed, and a grant attaches that role to a user at a specific resource. ## How authorization works - **Users** – Every action is initiated by a user (a person or a machine user). - **Actions** – Each API endpoint maps to an action (for example, `scope:read`). - **Roles** – A role is a bundle of actions assigned to a user. - **Grants** – A grant attaches a role to a user at a specific resource (NRN). Without a grant, the role doesn’t apply. ## Roles Every user has one or more roles that regulate which actions that user can execute. We have roles for managers, developers, operations, security, CI, and more. You can read more about this in the [Roles docs](/docs/authorization/roles). ## Grants and permissions Grants are what turn roles into actual access. A single user can hold multiple grants, each tied to a specific NRN. Grants also apply to child resources under the NRN. For more information on how to manage grants, see [Grants and permissions](./grants.md). ## API keys for machine users If you have a system that will interact with nullplatform programmatically (for example, a CI workflow), you need to create an API key and grant it what it may do on each resource: one or more roles, or a fine-grained set of permissions built from roles or picked by hand. See: - [**Manage your API keys**](/docs/authorization/api-keys) to create and manage your API keys from the UI, the CLI, or the API. - [**Configure API keys with IaC**](/docs/authorization/api-keys-iac) if you prefer to configure your API keys using infrastructure-as-code (IaC) tools. ## Personal access tokens When a script, the CLI, or an AI assistant should act as you rather than as a machine user, create a [personal access token](/docs/authorization/personal-access-tokens). It follows your access or a set of grants you choose, and it can never do more than you. ## Access tokens Most API endpoints require an access token sent in the HTTP header: ```bash Authorization: Bearer ``` The way you obtain the token depends on the type of user. ### Human users To retrieve the access token of your current session: 1. Log in to nullplatform. 2. Click your **user menu** (the circle with your initials/user avatar) in the top-right corner. 3. From the dropdown menu, select **Copy session token**. The session token is short-lived. For a token you can keep using from scripts or tools, create a [personal access token](/docs/authorization/personal-access-tokens). :::warning Deprecation notice The legacy [Chrome extension](https://github.com/nullplatform/chrome-extension) used to retrieve the token is deprecated and no longer maintained or supported. ::: ### Machine users **Machine users** (scripts, automations, or integrations) must authenticate using an **API key** and obtain access tokens from it. See the [API keys docs](/docs/authorization/api-keys) to learn how to generate and use API keys. ## SCIM provisioning If your organization uses an identity provider like Azure Entra ID, you can automate user lifecycle management and group-based role assignments through SCIM 2.0. See [SCIM provisioning](/docs/authorization/scim/scim-overview). --- ## Grants and permissions URL: https://docs.nullplatform.com/docs/authorization/grants Grants are the core of how authorization works in nullplatform. A grant is what gives a user permission to act on a resource. In short: - A **role** is a bundle of actions. - A **grant** attaches that role to a user at a specific resource (NRN). - The grant is what turns a role into real access. If you’re new to roles, start with [Roles](/docs/authorization/roles). ## How grants work A grant always has three parts: - **User** – the person or machine user receiving access - **Role** – the bundle of actions they can perform - **NRN** – the resource where the role applies Grants apply to the selected resource **and its child resources**. For example, a grant on an account applies to all namespaces and applications under that account. :::tip Grants are **additive** Users can have multiple grants across different resources and roles. ::: ## Manage grant permissions You can manage grants in these ways: - **From the UI** – Go to **Namespace** and open the **Team Management** dashboard. You can add or remove user grants there. - **Using the CLI or API** – Use the authorization API endpoints for grants and roles. ### Grant access to API keys (machine users) An API key's grant works like a user's, with one extra option: besides a role, it can hold a fine-grained set of permissions, either roles adjusted by hand or custom permissions picked one by one. See the [API keys docs](/docs/authorization/api-keys) to learn how to create and manage API keys and their grants. ### Grant access to human users **Create a single grant** when you’re giving access to one resource. You can use either the CLI or cURL. Send a [POST request](/docs/api/grants-create) like the example below. ```bash np authz grants create \ --body '{ "nrn": "organization=1:account=1:namespace=1:application=4", "user_id": 12345, "role_slug": "developer" }' ``` ```bash curl -L 'https://api.nullplatform.com/authz/grants' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ' \ -d '{ "nrn": "organization=1:account=1:namespace=1:application=4", "user_id": 12345, "role_slug": "developer" }' ``` Where: - `user_id` is the ID of the user receiving the grant. - `nrn` is the resource where the role is assigned. - `role_slug` is the slug of the role assigned to the user for the specified NRN. You can also provide the `role_id` instead of `role_slug`. ## When to use bulk or replace When managing grants in bulk through the API, you have a few options: - **create grants in bulk** when you need to add multiple grants at once. - **Replace all grants** when you want to reset access for a user in one request. This removes all existing grants and replaces them with the new list (or removes everything if you send an empty array). Send a [POST request](/docs/api/user-grants-bulk-create) like the example below. ```bash curl -L 'https://api.nullplatform.com/user/:id/grant' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ' \ -d '[ { "nrn": "organization=1:account=2", "role_slug": "developer" }, { "nrn": "organization=1:account=3", "role_slug": "ops" } ]' ``` ## Who can grant roles Granting a permission means assigning a role to a user on a specific NRN and its child resources. For example, you can be assigned the Admin role on an application, namespace, account, or the entire organization. Users can grant permissions to other users according to the following rules: - Users can only grant permissions at the same or lower NRN level where they have their own grant. - The role being granted must be one that the granter is authorized to assign: | Granter | Roles that can be granted | | ------- | ------------------------------------------------------ | | Admin | Admin, Member, Ops, Developer, SecOps, Insights viewer | | Ops | CI, Secrets Reader, Agent | The same table caps what you can hand out to an API key: the fine-grained permissions you can grant on a resource are the ones you hold there plus the ones carried by the roles you may assign to a key there. ## Grant removal - Only users with the **Admin** role can remove grants. - You can only remove grants at or below the NRN level where you hold the **Admin** role. :::warning Roles vs. approvals & policies Having a role on a certain resource doesn’t always mean you can act immediately. Your organization may require approvals for some actions. See [Approvals](/docs/approvals/) for more details. ::: --- ## Personal access tokens URL: https://docs.nullplatform.com/docs/authorization/personal-access-tokens A personal access token acts as you from the CLI, your own scripts, or an AI assistant connected through the [MCP server](/docs/mcp-server). Whatever access you give it, it can never do more than you can. ## When to use one Use a personal access token when a script or an agent should act **as you**: your own scripts, your local tools, or an AI assistant. What it does is traceable to you, and it stops working when you lose the access it relies on. For automation that runs without you, or that shouldn't depend on one person, like a team's CI pipeline, use an [API key](/docs/authorization/api-keys) instead. It belongs to a machine user, so it keeps working when people join or leave the team. > **Note:** The **Copy session token** option in your user menu is something else: it copies the short-lived token of your current login, for a quick API call. ## What a token can do When you create a token, you choose its access: - **Same access as me**: the token follows your access in the organization. Roles you get later are added, and roles you lose stop working for the token right away. - **Choose grants**: the token gets only the resources, roles, or permissions you pick. Each grant works like an [API key grant](/docs/authorization/api-keys#how-api-keys-fit-into-authorization): a resource, plus roles, roles adjusted by hand, or hand-picked permissions. Either way, every check combines the token's access with **yours at that moment**. If you lose a role, the tokens that rely on it lose it too, and if your user is deactivated, all your tokens stop working. You can only grant what you hold yourself: roles you have on that resource or one above it, and permissions you have there. ## Using the UI Open your user menu (your initials, top right) and select **User settings**. Walk through the page below: :::warning Save your token securely The token is **displayed only once**, right after you generate it. Copy it and store it in a secure location, as it cannot be retrieved later. ::: To change a token, open its **⋮** menu and click **View or edit**. You can rename it, change its access and tags, and extend its expiration. **Delete** removes it for good, and whatever uses it stops working right away. A token shows **Expiring soon** in its last 7 days. Once it expires, you can't edit it or extend it anymore, only delete it. ## Using the API You manage personal access tokens with your own login session, never with an API key: requests authenticated by any API key, a personal access token included, are rejected. To get a session token for these requests, open your user menu and click **Copy session token**. The CLI has no commands for personal access tokens, so the examples below use cURL. ### Create a token Send a [POST request](/docs/api/pat-create) to `https://api.nullplatform.com/pat`. This one creates a token with your own access: ```bash curl -L -X POST 'https://api.nullplatform.com/pat' \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer ' \ -d '{ "name": "local-scripts", "expires_in": 2592000, "access": "same_as_user", "tags": [ { "key": "team", "value": "platform" } ] }' ``` Where: - An expiration is required, at most a year away. Send it as `expires_in`, in seconds (at least 60; here, 30 days), or as `expires_at`, a date like `"2027-01-06T00:00:00Z"`. Not both: that's a `400`. - Prefer `expires_in`. It counts from the server's clock, so your own clock doesn't matter. The response only includes `expires_at`, the date it resolves to. - `access` is `same_as_user` for a token that follows your access, or `grants` (the default) for one limited to the grants you send. To limit the token instead, send `grants` with `access` set to `grants`. A grant has the same shapes as in an [API key](/docs/authorization/api-keys#grant-access-to-api-keys): a role, custom actions, or roles adjusted with `inherits`. This one can only read what's in one account: ```json { "name": "read-only-reports", "expires_in": 2592000, "access": "grants", "grants": [ { "nrn": "organization=1:account=2", "role_slug": "member" } ] } ``` You'll receive a `201` response like this: ```json { "id": 1234, "name": "local-scripts", "api_key": "AAAA.1234567890abcdef1234567890abcdefPTs=", // Your new token. "masked_api_key": "AAAAxxxxxPTs=", "access": "same_as_user", "tags": [ { "key": "team", "value": "platform" } ], "grants": [ { "id": 5678, "nrn": "organization=1:account=2", "role_id": 700317756, "role_slug": "developer" } ], "personal": true, "owner_id": 42, "acting_user_id": 42, "last_used_at": null, "expires_at": "2026-11-07T00:00:00.000Z", "expired": false, "created_at": "2026-10-08T00:00:00.000Z", "updated_at": "2026-10-08T00:00:00.000Z" } ``` Where: - `api_key` is your new token. It's returned only in this response. - `grants` is what the token can do. For a `same_as_user` token, those are your own grants at this moment. - `acting_user_id` is you: the user the token acts as. :::warning Save your token securely The token is **displayed only once**. Make sure to store it in a secure location, as it cannot be retrieved later. ::: ### Use the token A personal access token works like an API key: exchange it for an access token with a [POST request](/docs/api/access-token-create) to `https://api.nullplatform.com/token`. See [Generate an access token](/docs/authorization/api-keys#generate-an-access-token). ```bash curl -L -X POST 'https://api.nullplatform.com/token' \ -H 'Content-Type: application/json' \ -d '{ "api_key": "AAAA.1234567890abcdef1234567890abcdefPTs=" }' ``` Tools that take an API key, like the `np` CLI with `--api-key` or the MCP server's `X-API-Key` header, take a personal access token too. ### List your tokens Send a [GET request](/docs/api/pat-list) to `https://api.nullplatform.com/pat`. You only ever see your own tokens: ```bash curl -L 'https://api.nullplatform.com/pat?search=mcp&status=active,expiring_soon&sort=name:asc' \ -H 'Authorization: Bearer ' ``` These query parameters are all optional: - `search` matches a token's name, or the key or value of one of its tags. - `status` takes one or more of `active`, `expiring_soon` (expires within 7 days), and `expired`, separated by commas. - `expires_at:range` takes two dates, `from,to`. `expires_at:gte` and `expires_at:lte` take one. - `sort` is `name:asc`, `name:desc`, `last_used_at:asc_nulls_last`, or `last_used_at:desc_nulls_last`. - `limit` (30 by default, 100 at most) and `offset` page through the results. ### Update a token Send a [PATCH request](/docs/api/pat-update) to `https://api.nullplatform.com/pat/` with what you want to change: `name`, `tags`, `access`, `grants`, or the expiration as `expires_in` or `expires_at`. This one extends the token to 90 days from now: ```bash curl -L -X PATCH 'https://api.nullplatform.com/pat/1234' \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer ' \ -d '{ "expires_in": 7776000 }' ``` A few rules apply: - The expiration can only move later, up to a year from now. Send it as `expires_in` or `expires_at`, not both. An expired token can't be extended. - `grants` replaces all of the token's grants. - Switching to `same_as_user` drops the token's grants. Switching back to `grants` requires the grants it should have. ### Delete a token Send a [DELETE request](/docs/api/pat-delete) to `https://api.nullplatform.com/pat/`: ```bash curl -L -X DELETE 'https://api.nullplatform.com/pat/1234' \ -H 'Authorization: Bearer ' ``` You'll receive a `204` response. The token stops working right away, including any access token already issued with it. ## Next steps - [Connect an AI assistant](/docs/mcp-server): every connection runs on a personal access token, a new one or one you already have - [Manage your API keys](/docs/authorization/api-keys): keys for shared automation that belong to a machine user - [Roles](/docs/authorization/roles): what each role allows --- ## Roles URL: https://docs.nullplatform.com/docs/authorization/roles Every user has one or more roles that regulate which actions they can execute. We have roles geared toward managers, developers, operations, security, CI, and more. ## How roles are applied Roles don’t grant access by themselves. A role only takes effect when it’s attached to a user through a **grant** at a specific resource (NRN). If you’re looking for how to assign roles or manage grants, see [Grants and permissions](/docs/authorization/grants). ## Management roles * **Admin** * **Insights viewer** | Role | ID | Slug | Description | | --------------- | ----------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Admin | `696188987` | `admin` | - Create and destroy resources and invite users with other roles.- Manage the organization, account, or namespace.- Create, modify, and delete applications and their scopes.- Admin roles automatically include **Developer** permissions. | | Insights admin | `1131675813` | `insights-admin` | - Create, edit, and publish dashboards.- Manage Insights features and settings. | | Insights viewer | `724959298` | `insights-viewer` | - View reports and insights about the organization. | ## Developer-centric roles * **Developer** * **Member** | Role | ID | Slug | Description | | --------- | ----------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Developer | `700317756` | `developer` | - Manage applications in a namespace.- Create applications, parameters, deployments.- Can create builds, releases, scopes, and start them.- View logs, performance, metadata, and troubleshoot.- Cannot make changes at the organization or account level.- Includes **Member** permissions by default. | | Member | `704380989` | `member` | - Read-only access to resources.- Can view organization, account, namespace, and application information.- Included by default in all roles.- Cannot make changes. | ## DevOps, infrastructure, security, and FinOps-centric roles * **Ops** * **SecOps** * **CI** | Role | ID | Slug | Description | | ------ | ----------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Ops | `708509758` | `ops` | - Configure the infrastructure for the organization, account, namespace, or application.- Includes **Member** permissions by default. | | SecOps | `712638527` | `secops` | - Configure security-related features for the organization, account, namespace, or application.- Includes **Member** permissions.- Includes permission to read secret values. | ## Machine user roles These roles are intended for machine users and API keys. * **Secrets Reader** * **Agent** * **CI** | Role | ID | Slug | Description | | -------------- | ------------ | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Secrets Reader | `679739447` | `secrets-reader` | - Role for client environment to read secret parameters. | | Agent | `618135592` | `controlplane:agent` | - Role to be used by nullplatform agents. | | CI | `1855672260` | `organization:machine:ci` | - Machine user that performs continuous integration in an organization.- Create builds, assets, releases, and metadata.- Includes **Member** permissions by default. | --- ## Monitor provisioning activity URL: https://docs.nullplatform.com/docs/authorization/scim/azure-entra-id/monitor After enabling SCIM provisioning, you can track sync activity and review how changes in Azure Entra ID are reflected in nullplatform. ## Azure Entra ID provisioning logs 1. In the application, go to **Provisioning** → **Provisioning logs**. 2. View real-time provisioning events, including: - User creations and updates - Group assignments - Errors and warnings ## Common provisioning scenarios | Scenario | Action | Result | |----------|--------|--------| | **New user assigned** | User is added to Azure Entra ID and assigned to the app | User is created in nullplatform | | **User properties updated** | User's email or name changes in Azure Entra ID | User is updated in nullplatform | | **User unassigned** | User is removed from app assignment | User is deactivated in nullplatform | | **User deleted** | User is deleted from Azure Entra ID | User is deactivated in nullplatform | | **Group assigned** | Azure group is added to provisioning scope | Role assignments are applied based on group mappings | ## Next steps - [Troubleshooting](/docs/authorization/scim/azure-entra-id/troubleshooting) - [SCIM provisioning overview](/docs/authorization/scim/scim-overview): supported attributes, group mapping, and limitations --- ## Configure provisioning URL: https://docs.nullplatform.com/docs/authorization/scim/azure-entra-id/setup This guide walks you through configuring automatic user and group provisioning from Azure Entra ID to nullplatform using SCIM 2.0. ## Prerequisites - **Azure tenant admin access**: Global Administrator or Application Administrator role - **SCIM connection details**: your **Tenant URL** and **Secret Token**, provided by nullplatform when your SCIM integration is set up. These values are unique to your organization. - **Network connectivity**: Azure Entra ID must be able to reach the Tenant URL provided by nullplatform ## Steps ### Step 1: Add a new enterprise application 1. Sign in to the [Azure portal](https://portal.azure.com) as a Global Administrator or Application Administrator. 2. Navigate to **Enterprise applications**. 3. Click **New application**. 4. Search for **SCIM** or select **Create your own application**. 5. Choose **Integrate any other application you don't find in the gallery**. 6. Name the application (e.g., "nullplatform SCIM") and click **Create**. ### Step 2: Configure SCIM connectivity 1. In the new application, go to **Provisioning** in the left sidebar. 2. Click **Get started**. 3. Set **Provisioning Mode** to **Automatic**. 4. Under **Admin Credentials**, enter the values nullplatform gave you: - **Tenant URL**: your SCIM endpoint (looks like `https:///scim/v2`) - **Secret Token**: the Bearer token for your integration :::info The **Tenant URL** and **Secret Token** are provisioned per organization. Nullplatform runs a dedicated SCIM endpoint for each customer, so both values are generated and shared with you during onboarding, they are not the same for every organization. If you don't have them yet, request them from nullplatform before configuring the connection. ::: ### Step 3: Test the connection 1. Click **Test Connection**. 2. Azure Entra ID will attempt to reach the SCIM ServiceProviderConfig endpoint. 3. If successful, you'll see a green checkmark. If the test fails: - Verify the **Tenant URL** matches the one nullplatform provided and includes `/scim/v2`. - Confirm the **Secret Token** matches the one nullplatform provided. - Ensure the Tenant URL host is reachable from Azure. - Check firewall rules and network access. ### Step 4: Configure attribute mappings Attribute mappings define how Azure Entra ID user attributes map to SCIM user attributes. 1. In **Provisioning**, click **Edit attribute mappings**. 2. Review the default mappings. Common mappings include: | Azure Entra ID Attribute | SCIM Attribute | Required | |---------------------------|----------------|----------| | `mail` | `userName` | Yes | | `displayName` | `displayName` | No | | `givenName` | `name.givenName` | No | | `surname` | `name.familyName` | No | | `userPrincipalName` | `externalId` | No | | `accountEnabled` | `active` | No | 3. Customize mappings as needed for your use case. 4. Click **Save**. Example mapping for email-based provisioning: ```json { "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"], "userName": "john.doe@example.com", "displayName": "John Doe", "name": { "givenName": "John", "familyName": "Doe" }, "emails": [ { "value": "john.doe@example.com", "primary": true } ], "active": true } ``` ### Step 5: Enable group provisioning (optional) If you want group membership from Azure Entra ID to drive role assignments in nullplatform: 1. In **Provisioning**, scroll to **Mappings**. 2. Click **Provision Azure Active Directory Groups** to enable group sync. 3. Assign the relevant groups to the application. Azure sends group membership information via SCIM. The actual mapping from groups to nullplatform roles is configured server-side in nullplatform. See [Group membership and role mapping](/docs/authorization/scim/scim-overview#group-membership-and-role-mapping) for details. :::info Ensure group-to-role mappings are configured in nullplatform **before** enabling provisioning so that role assignments take effect from the first sync. ::: ### Step 6: Enable provisioning 1. Back in **Provisioning**, set **Provisioning Status** to **On**. 2. Click **Save**. Azure Entra ID will immediately begin provisioning users and groups to nullplatform. ## Next steps - [Monitor provisioning activity](/docs/authorization/scim/azure-entra-id/monitor) - [Troubleshooting](/docs/authorization/scim/azure-entra-id/troubleshooting) --- ## Troubleshooting URL: https://docs.nullplatform.com/docs/authorization/scim/azure-entra-id/troubleshooting Common issues you may encounter when provisioning users from Azure Entra ID to nullplatform, with causes and solutions. ## Provisioning fails with "Invalid credentials" **Cause**: The API secret token is incorrect or expired. **Solution**: 1. Verify the token in your configuration is correct. 2. Generate a new token if needed. 3. Update the **Secret Token** in Azure Entra ID provisioning settings. 4. Retry the provisioning operation. ## Users are not syncing **Cause**: Users may not be assigned to the application in Azure Entra ID. **Solution**: 1. In the application, go to **Users and groups**. 2. Click **Add user/group**. 3. Assign users or groups to the application. 4. Wait for the next provisioning cycle (typically every 40 minutes) or manually trigger a sync. ## "User not found" errors **Cause**: The SCIM API cannot locate a user to update. **Solution**: 1. Check the **userName** mapping is correct (usually matches email or UPN). 2. Verify user was successfully created on initial provisioning. 3. Check provisioning logs for validation errors. ## Attribute mappings not working **Cause**: Attribute values may be missing or incorrectly mapped. **Solution**: 1. Verify the Azure Entra ID user has values for mapped attributes. 2. Check the SCIM schema supports the attribute. 3. Review the provisioning logs for specific mapping errors. 4. Test with a sample user first. ## Next steps - [SCIM provisioning overview](/docs/authorization/scim/scim-overview): supported attributes, group mapping, and limitations - [Azure Entra ID provisioning documentation](https://learn.microsoft.com/en-us/azure/active-directory/app-provisioning/user-provisioning) - [Manage users in nullplatform](/docs/authorization/users) --- ## SCIM provisioning URL: https://docs.nullplatform.com/docs/authorization/scim/scim-overview SCIM (System for Cross-domain Identity Management) is an open standard that lets your identity provider automatically create, update, and deactivate users in nullplatform. Instead of managing users manually, you **connect your IdP once and keep both systems in sync**. Nullplatform implements [SCIM 2.0 (RFC 7644)](https://tools.ietf.org/html/rfc7644) and supports: - **Automated user lifecycle**: provision, update, and deactivate users from your IdP - **Group-based role assignment**: map IdP groups to nullplatform roles - **Attribute synchronization**: keep names, emails, and status consistent across systems - **Gradual rollout**: start with a subset of users while you validate the setup ## Prerequisites - **SCIM connection details**: a **Tenant URL** and **Secret Token**, provided by nullplatform when your SCIM integration is set up. These values are unique to your organization. - **Identity provider**: a SCIM 2.0-compliant IdP with admin access - **Network connectivity**: your identity provider must be able to reach the Tenant URL provided by nullplatform ## How it works ```mermaid flowchart LR A["Identity provider(e.g. Azure Entra ID)"] -- "SCIM 2.0" --> B["nullplatform"] B --> C["Users and rolesin sync"] ``` 1. Your **identity provider** sends SCIM requests whenever users or groups change. 2. **Nullplatform** receives these requests at a SCIM endpoint dedicated to your organization (your **Tenant URL**), authenticated with your **Secret Token**. 3. [Users](/docs/authorization/users) are created, updated, or deactivated automatically. 4. Group membership changes trigger the corresponding [role assignments](/docs/authorization/grants). :::info Nullplatform runs a dedicated SCIM endpoint per customer. Your **Tenant URL** and **Secret Token** are generated and shared with you during onboarding, so request them from nullplatform before configuring your identity provider. ::: ## Supported user attributes | SCIM Attribute | Description | Required | |---|---|---| | `userName` | Primary identifier, typically the user's email | Yes | | `externalId` | Unique identifier from the identity provider | No | | `name.givenName` | First name | No | | `name.familyName` | Last name | No | | `displayName` | Display name | No | | `emails` | Email addresses (primary email is used) | No | | `active` | Whether the user is active | No | | `groups` | Group memberships (read-only, computed) | N/A | :::note **Unsupported attributes:** phone, address, photos, timezone, locale, and password. ::: ## Group membership and role mapping Groups in nullplatform are **computed from role grants**, not stored directly. When your IdP sends group membership information via SCIM, nullplatform translates those groups into role assignments based on a server-side configuration. This means: - Groups appear as **read-only** SCIM resources. Your IdP cannot create groups in nullplatform directly. - A user's group membership is determined by whether they hold the required roles. - The mapping between IdP groups and nullplatform roles is configured server-side in nullplatform, not in the IdP. :::info **AND logic** A user belongs to a group only when they hold **all** required roles for that group. For example, if a group requires both `developer` and `member` roles, the user must have both to appear in the group. ::: ## Known limitations - No bulk operations (each user or group change is a separate SCIM request) - No sorting on list queries - No password management via SCIM - Maximum 200 results per query - Single-attribute filters only (no compound `AND`/`OR` expressions) ## Best practices - **Configure group mappings first**: verify that [role definitions](/docs/authorization/roles) and group-to-role mappings are in place before enabling provisioning. - **Start small**: begin with a small set of users to validate attribute mappings and role assignments before expanding. - **Limit provisioning scope**: assign only necessary users and groups in your IdP to avoid unintended changes. - **Monitor provisioning logs**: review your IdP's provisioning logs after each sync cycle for errors. - **Implement deprovisioning**: ensure users are deactivated when they leave the organization. ## Supported identity providers Nullplatform supports any SCIM 2.0-compliant identity provider. The Azure Entra ID guide can serve as a reference for configuring other providers. 👉 [Set up Azure Entra ID SCIM provisioning](/docs/authorization/scim/azure-entra-id/setup) ## Next steps - [Manage users in nullplatform](/docs/authorization/users) - [Authorization and roles](/docs/authorization) - [SCIM 2.0 RFC 7644 specification](https://tools.ietf.org/html/rfc7644) --- ## Users URL: https://docs.nullplatform.com/docs/authorization/users A **user** represents a person (member) in nullplatform. Users receive **roles**, and those roles take effect through **grants** tied to specific resources. ## Prerequisites You need an [Admin role](/docs/authorization/roles) at the **Organization** level to create users. ## Create users You can create users using the UI, CLI, or API. 1. Log in to nullplatform as an **admin** user. 2. Navigate to **Team management**. 3. Click **+ Invite new member**. 4. Fill in the user details, assign permissions, and click **Send invite**. To create a user with the API, send a [POST request](/docs/api/user-create) to the User endpoint. Here’s an example: ```bash np user create \ --body '{ "email": "alex.doe@email.com", "first_name": "Alex", "last_name": "Doe", "organization_id": 1234, "avatar": "https://my-avatar-url.com", "send_invite": true //Optional field; defaults to `true` }' ``` To create a user with the API, send a [POST request](/docs/api/user-create) to the User endpoint. Here’s an example: ```bash curl -L 'https://api.nullplatform.com/user' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ' \ --data-raw '{ "email": "alex.doe@email.com", "first_name": "Alex", "last_name": "Doe", "organization_id": 1234, "avatar": "https://my-avatar-url.com", "send_invite": true //Optional field; defaults to `true` }' ``` The user will receive an invitation email to join nullplatform. ## Pre-provision users before first SSO login You can create users **before their first SSO login** so you can assign roles and grants in advance. This is useful when you want access in place on day one and you do not want to send an invitation email. To pre-provision a user, create the user with `send_invite=false`. See [Create a user](/docs/api/user-create) for the full request and response schema. > ℹ️ **Note:** You can’t pre-provision users from the UI. Use the API or CLI instead. ```bash np user create \ --body '{ "email": "alex.doe@email.com", "first_name": "Alex", "last_name": "Doe", "organization_id": 1234, "send_invite": false }' ``` ```bash curl -L -X POST 'https://api.nullplatform.com/user' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ' \ -d '{ "email": "john.doe@email.com", "first_name": "John", "last_name": "Doe", "organization_id": 1234, "send_invite": false }' ``` :::important Set `send_invite` to `false` to avoid sending an invitation email. ::: ### About `send_invite` - `send_invite` is a boolean field on user creation. - The default value is `true`. - When set to `false`, nullplatform does not send an invitation email. ## Next steps - To automate user provisioning from your identity provider instead of creating users manually, see [SCIM provisioning](/docs/authorization/scim/scim-overview). --- ## Generate built-in UI components URL: https://docs.nullplatform.com/docs/catalog/built-in-components Nullplatform includes built-in UI components that render automatically based on specific catalog keys, like `links` or `about`. These don’t require any custom UI configuration or rendering logic. If you attach an instance using one of these keys, the platform will: - Recognize it - Apply default visual styling - Place it in the appropriate part of the UI :::tip These are built-in catalogs You don't need to create catalog specifications for `links` and `about` keys. ::: ## Built-in catalog keys | Catalog key | Entity type(s) | Description | | ----------- | -------------------------- | ----------------------------------------------- | | `links` | `namespace`, `application` | Renders grouped links to dashboards, docs, etc. | | `about` | `user` | Shows user profile info (title, role, contact) | > 💡 These are optional, but when used correctly, they enhance discoverability and visibility across the platform. ## The `links` component ### Namespace To create catalog components of entities in your namespace, send a [POST request to create a catalog instance (single key)](/docs/api/catalog-instance-single-create) for **links** like the following: ```bash curl -L 'https://api.nullplatform.com/metadata/namespace/123/links' \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer ' \ -d '[ { "link_identity": { "name": "Trello", "icon": "logos:trello" }, "destination": { "url": "https://trello.com/w/my-organization-trello/home", "description": "Workspace" } }, { "link_identity": { "name": "Github", "icon": "bi:github" }, "destination": { "url": "https://github.com/my-organization-github", "description": "Homepage" } }, { "link_identity": { "name": "Calendar", "icon": "logos:google-calendar" }, "destination": { "url": "https://calendar.google.com/", "description": "Link" } } ]' ``` In the namespace dashboard, they appear above the application list: ### Application In the application dashboard, they are in the **Useful links** section. For example, to add a Datadog component to your application UI, send [POST request to create a catalog instance (single key)](/docs/api/catalog-instance-single-create) for **links** like the following: ```bash curl -L 'https://api.nullplatform.com/metadata/application/123/links' \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer ' \ -d '[ { "link_identity": { "name": "Datadog", "icon": "simple-icons:datadog" }, "destination": { "url": "https://app.datadoghq.com/dashboard/example", "description": "Application performance & monitoring" } } ]' ``` This will be rendered in the UI like this: ### Built-in catalog spec ```json { "type": "array", "minItems": 1, "items": { "type": "object", "properties": { "link_identity": { "type": "object", "title": "Link Identity", "description": "Set a name and icon to help recognize this link.", "properties": { "name": { "type": "string", "title": "Link name", "placeholder": "Enter a link name" }, "icon": { "type": "string", "title": "Icon", "placeholder": "Enter an icon name" } }, "required": ["name", "icon"] }, "destination": { "type": "object", "title": "Destination", "description": "Set the destination URL and a short description for this link.", "properties": { "url": { "type": "string", "title": "URL", "format": "url", "placeholder": "Enter the url where your link will redirect" }, "description": { "type": "string", "title": "Description", "placeholder": "Enter a description" } }, "required": ["url"] } }, "required": ["link_identity", "destination"] } } ``` ## The `about` component (user profiles) Use the `about` catalog key to display structured profile information for a platform user. This is rendered on the member profile page and can include title, role, contact info, or other structured data. ### Users To use it, you just need to [Create a catalog instance (single key)](/docs/api/catalog-instance-single-create) for the built-in **about** specification. Here's a request example: ```bash curl -L 'https://api.nullplatform.com/metadata/user/123/about' \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer ' \ -d '[ { "title": "Position", "description": "System Administrator" }, { "title": "Role", "description": "Product Owner" }, { "title": "Contact", "description": "+12 123 456 789" } ] ``` Here's a user profile example: ### Built-in catalog specs ```json { "type": "array", "items": { "type": "object", "properties": { "title": { "type": "string", "minLength": 4 }, "description": { "type": "string", "minLength": 4 } }, "required": [ "title", "description" ] } } ``` --- ## Define catalog specifications URL: https://docs.nullplatform.com/docs/catalog/define-catalog-specs A **catalog specification** defines the structure, validation rules, and display behavior for metadata attached to entities in nullplatform, like applications, builds, and namespaces. Each specification acts as a **template**: it tells the platform what fields should be available for a given entity, how those fields should behave (e.g., enum vs. free text), and how they should be displayed. Once a spec is defined, nullplatform can: - Autogenerate form sections during entity creation - Render editable metadata in dashboards - Enforce validation for all updates—whether from the UI or API :::info Catalog specs are created via API Once defined, they automatically power the UI for forms and metadata display. ::: ### What a catalog specification includes Each spec is a JSON object with the following fields: | Field | Description | | ------------- | ------------------------------------------------------------------------------ | | `entity` | The entity type the spec applies to (e.g. `build`, `application`, `namespace`) | | `metadata` | A unique key for this metadata group (e.g. `coverage`, `quality_insights`) | | `schema` | A [JSON Schema](https://json-schema.org) defining the fields and rules | | `nrn` | The scope this spec applies to (e.g. an org, namespace, or account) | | `name` | Human-readable name (used in forms and dashboards) | | `description` | Optional description for users filling out the metadata | ## How to create a specification (API) You can create a spec with a [POST request to create a catalog specification](/docs/api/catalog-specification-create) ```bash curl -L -X POST 'https://api.nullplatform.com/metadata/metadata_specification' \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer ' \ -d '{ "name": "Namespace tags", "description": "Add tags as key-value object to namespace entities", "nrn": "organization=1", "entity": "namespace", "metadata": "tags", "schema": { "type": "object", "properties": { "my_string_property": { "type": "string", "tag": true }, "my_number_property": { "type": "number", "default": 0, "tag": "my_custom_tag_name" }, "my_object_property": { "type": "object", "properties": { "my_nested_property": { "type": "string" } } } }, "required": [ "my_string_property" ], "additionalProperties": false } }' ``` ## Example: autogenerate forms during entity creation When you create an application, build, or namespace, nullplatform checks for a matching catalog specification. If one exists, it automatically adds a metadata form to the creation flow, with no manual UI work needed. Users fill out fields defined in the schema, with live validation. Once saved, metadata is attached to the entity and available in dashboards or for editing later. This means structured **data collection becomes a natural part of onboarding**, with no extra steps. #### A metadata form is shown if: - A catalog spec exists for the entity type (`application`, `build`, or `namespace`) - The spec has at least one field - That field is visible on creation (`visibleOn: ["create"]`) or has no `visibleOn` at all If no matching spec is found, the form step is skipped. *Example of a form to create a namespace:* ```json { "entity": "namespace", "metadata": "operational", "nrn": "organization=1:account=2", "description": "Add operational and organizational information as tags to namespace entities", "name": "Namespace Operational Tags", "schema": { "type": "object", "properties": { "business_unit": { "type": "string", "title": "Business Unit", "description": "The business unit or division owning this namespace", "tag": true }, "compliance": { "type": "string", "visibleOn": [ "read", "update", "create", "list" ], "enum": [ "PCI", "HIPAA", "GDPR", "None" ], "title": "Compliance Requirements", "description": "Regulatory standards applicable to this namespace", "tag": "compliance_tag" }, "deployment_frequency": { "type": "string", "enum": [ "daily", "weekly", "monthly", "on_demand" ], "title": "Deployment Frequency", "description": "How often this namespace is deployed", "tag": true }, "incident_history": { "type": "number", "title": "Incident Count (Last 90 days)", "description": "Number of incidents involving this namespace in the last 90 days", "tag": true }, "last_audit": { "type": "string", "format": "date", "title": "Last Audit Date", "description": "The most recent date this namespace was audited" } }, "required": [ "business_unit", "deployment_frequency" ], "additionalProperties": false } } ``` ## Example: application type spec This example defines a simple dropdown for selecting an application type when creating or editing an app. ```json { "name": "Application type", "entity": "application", "metadata": "apptype", "nrn": "organization=123", "schema": { "type": "object", "properties": { "application_type": { "type": "string", "oneOf": [ { "const": "public", "title": "Public Application" }, { "const": "private", "title": "Private Application" }, { "const": "internal", "title": "Internal Application" } ], "visibleOn": ["create", "read", "update"] } }, "required": ["application_type"], "additionalProperties": false } } ``` Once saved, this spec: - Renders a dropdown during app creation. - Appears under the **Metadata** panel on the application dashboard. - Can be edited later using the **Show all** button. ## Example: build quality insights This spec defines metrics shown in the build detail view (like `coverage_percent`, `technical_debt_minutes`, and `complexity`): ```json { "name": "Build Quality Insights", "entity": "build", "metadata": "quality_insights", "nrn": "organization=123", "schema": { "type": "object", "properties": { "technical_debt_minutes": { "type": "integer", "description": "Time-based tech debt metric", "visibleOn": ["create", "update", "read", "list"] }, "coverage_percent": { "type": "integer", "description": "Coverage % for this build", "visibleOn": ["create", "update", "read", "list"] }, "is_stable": { "type": "boolean", "description": "Indicates whether the build is considered stable.", "visibleOn": ["create", "update", "read", "list"] }, "complexity": { "type": "integer", "description": "Code complexity score", "visibleOn": ["read", "list"] }, "vulnerabilities": { "type": "integer", "description": "Known vulnerabilities in this build", "visibleOn": ["read", "update"] } }, "required": ["coverage_percent", "technical_debt_minutes", "complexity","is_stable", "vulnerabilities"], "additionalProperties": false } } ``` Once attached to a build, this spec: - Renders directly in the "Quality Insights" section of the build dashboard. - Some fields are editable, some are read-only. - Taggable and usable in filters if `tag` is also defined. ## Adding catalog data to list views You can display catalog fields directly inside an entity list view, by adding `"list"` to a field’s `visibleOn` configuration. > ℹ️ This feature is only available for **build** and **deployment** entities. When `"list"` is included, the platform automatically creates a new column in the corresponding table and renders the field value for each entity. The configuration shown in the example spec above produces the columns shown in the following screenshot, enabling teams to assess build health directly from the overview page without opening every build. :::tip Column naming rules List-view columns follow the same naming hierarchy used across all schema/UI schema rendering in nullplatform. When generating the column label, the platform checks: 1. **`uiSchema` (UI schema)** — If the field defines a custom label, it takes priority. See our [UI schema docs](/docs/json-ui-schema/ui-schema) for more info on this. 2. **`title` (JSON Schema)** — If no UI label is present, the schema’s `title` is used. 3. **Property key (humanized)** — If neither a UI label nor a title is provided, nullplatform uses the property name and automatically humanizes it (e.g., `is_stable` → **Is Stable**). ::: ## Tagging fields for filters and dashboards You can mark any field in a schema as a **tag**, which makes it available for: - Dashboard filtering - Metadata-based search - Integration with third-party tools (e.g., FinOps, inventory systems) To tag a field, use the `"tag"` key: ```json { "business_unit": { "type": "string", "tag": true }, "platform_type": { "type": "string", "enum": ["web", "mobile"], "tag": "platform" } } ``` - `"tag": true` uses the field name as the tag key. - `"tag": "custom_name"` lets you define your own key. For a complete guide on tagging—including use cases, API queries, and external integrations—see [Tags in catalogs](/docs/catalog/tags). ## Next steps Once your catalog spec is defined: - [Attach metadata to entities via UI or API](/docs/catalog/instances) --- ## Autogenerate forms during entity creation URL: https://docs.nullplatform.com/docs/catalog/forms-entity-creation Nullplatform can automatically render metadata input forms during entity creation, like when you're adding a new application, build, or namespace. These forms use catalog specifications and require no manual UI work. If a catalog spec exists for the entity type, the platform: - Adds a metadata section to the creation flow - Validates inputs live as the user fills the form - Creates a metadata instance once the entity is saved This means structured data collection becomes a natural part of onboarding, with no extra steps. ## When are forms rendered? A form will appear automatically if: - A catalog specification exists for the entity type (e.g., `application`, `build`, `namespace`) - The schema includes at least one field - The field is marked `visibleOn: ["create"]` or left unscoped (which defaults to full visibility) > 💡 You don’t need a full `uiSchema`, but adding one improves layout and widgets (like radios or grouped sections). > See our [Designing the UI](/docs/json-ui-schema/overview) page for more info. If no matching spec is found, the form step is skipped. ## Supported entities Forms can be autogenerated during creation for: - Applications - Namespaces - Builds After creation, users can still view and edit these fields via the **Show all** button on the entity detail page. ## Real-world example: build creation Let’s say you’ve defined the following catalog spec for the `build` entity: ```json { "title": "Build Quality", "type": "object", "properties": { "coverage_percent": { "type": "integer", "description": "Code coverage %" }, "vulnerabilities": { "type": "integer", "description": "Known vulnerabilities" } }, "required": ["coverage_percent", "vulnerabilities"] } ``` When a user creates a new build, this form will automatically appear: - `coverage_percent` (validated as an integer) - `vulnerabilities` (required field) All values will be attached as a metadata instance once the build is created. --- ## Catalog URL: https://docs.nullplatform.com/docs/catalog/getting-started :::tip Heads up: metadata is now called catalog We’ve rebranded *metadata* as *catalog* to better reflect how structured data works across nullplatform. ::: The **catalog** feature in nullplatform lets you extend and enrich your entities, like builds, applications, and namespaces—with structured, validated catalogs that power forms, dashboards, and automation across the platform. With just a few schema definitions, you can: - Add custom forms to entity creation flows - Display dynamic catalogs in dashboards (like "Quality Insights" for builds) - Validate inputs automatically ## What is the catalog? The catalog is a system that brings structure and validation to your platform data. You use it to define **custom fields**, like "compliance level", "application type", or "test coverage", and attach them to any entity. These fields are defined using [JSON Schema](https://json-schema.org), which means you can: - Enforce consistent input across teams - Generate forms automatically - Control how and where the data appears It’s: - **Flexible** – You decide what catalog is relevant for your team. - **Validated** – Every field follows rules you define in the schema. - **Scalable** – Specs can be scoped globally or to specific teams using [NRNs](/docs/NRN). ## Core building blocks The catalog is built on two core components: - **Specifications**: A JSON Schema that defines what catalog fields an entity can have (and how they behave). - **Instances**: A filled-out version of that spec, attached to a specific entity. ## Create and edit catalogs directly in the UI You can now create and manage catalogs **entirely from the UI**, with no API required for most use cases. Here’s how it works: - **When you create an entity** (like a namespace or application), nullplatform will show an autogenerated form based on your catalog spec. - **After creation**, the catalog appears in the entity’s detail view, where users can click **"Show all"** to review or update values. > 💡 This is available for `namespace`, `application`, and `build`. Specs are still defined via API for now. ## How it works Here’s the typical workflow: 1. **Define a catalog specification** Use the API to define the schema, what fields should appear and how they should behave. Here's an example of a catalog schema for builds: ```json { "schema": { "type": "object", "properties": { "coverage_percent": { "type": "integer", "description": "Coverage % for this build", "visibleOn": ["create", "update", "read", "list"] }, "technical_debt_minutes": { "type": "integer", "description": "Time-based tech debt metric", "visibleOn": ["create", "update", "read"] }, "complexity": { "type": "integer", "description": "Code complexity score", "visibleOn": ["read"] }, "vulnerabilities": { "type": "integer", "description": "Known vulnerabilities in this build", "visibleOn": ["read", "update"] } } } } ``` 2. **Create catalog instances (via UI or API)** Users fill out data in the autogenerated form during entity creation. Later, they can update fields from the catalog section in the UI. 3. **Use catalog throughout the platform** The data appears in dashboards (like “Quality Insights” for builds), filters, and policies. ### Catalog flow ```mermaid graph TD A[Define catalog spec - JSON Schema via API] --> B[Entity is created in the UI] B --> C[Check for existing catalog spec] C --> D[If spec exists - show autogenerated form] D --> E[User fills catalog form] E --> F[catalog instance is saved] C --> G[If no spec - skip form] F --> H[Catalog shown in UI - detail view] H --> I[Used in dashboards, filters, policies] ``` ## Next steps Here’s what to explore next: - [See where the catalog appears in the UI](/docs/catalog/where-catalog-appears) - [Define catalog specifications](/docs/catalog/define-catalog-specs) - [Create and edit catalog instances](/docs/catalog/instances) - [Generate built-in components](/docs/catalog/built-in-components) --- ## Create and edit instances URL: https://docs.nullplatform.com/docs/catalog/instances # Create and edit catalog instances Once a catalog specification exists for an entity, like an application, build, or namespace, you can start adding actual values to it. These values are known as **catalog instances**. A catalog instance is a structured set of data that: - Follows the rules defined in the catalog spec (JSON Schema) - Is attached to a specific entity (e.g., a particular application) - Can be created and edited from the **UI**, **API**, or **CLI** ## When is an instance created? Catalog instances are created in one of two moments: - **During entity creation** If a spec exists for that entity type, nullplatform automatically renders a form so users can fill in the catalog on the spot. - **Later, in the entity’s dashboard** After creation, the catalog appears in the catalog section of the entity. Click **"Show all"** to view or edit all fields from the associated specs. > 💡 This UI-based flow is available for `application`, `build`, and `namespace`. ## Example: catalog in the application dashboard When a developer creates a new application, nullplatform will: 1. Detect available catalog specs for `application` 2. Render a form for fields like `application_type`, `description`, or any other item 3. Let the user fill them out and save Later, on the application detail page: - Each catalog spec appears as a box in the right sidebar - Clicking **“Show all”** opens a modal where the fields can be edited *Example:* - “Apptype” → dropdown rendered from enum values - “Description” → freeform string field ## Example: build catalog via quality insights For builds, the catalog shows up as part of the **Quality Insights** section: ```json { "quality_insights": { "coverage_percent": 78, "technical_debt_minutes": 84, "complexity": 65, "vulnerabilities": 3 } } ``` Each of these fields is defined in the catalog spec, and: - Is rendered automatically in the build UI - Can be edited (if `visibleOn` includes `update`) - Can also be synced from CI pipelines using the API *Example:* ## Creating instances via the API If you're integrating with CI/CD or automating catalog population, you can use the API to create or update catalog data directly using either the [Catalog instance (multiple keys) API](/docs/catalog-instance-multiple-api-index) or the [Catalog instance (single key) API](/docs/catalog-instance-single-api-index). #### Multiple keys (batch creation) Use this approach when you want to send data for **several metadata keys at once**. This is ideal for creating or updating all catalog values for an entity in a single request. ```bash np metadata create --id :id --entity build --body '{ "coverage": { "percent": "95", "report": "https://reports.my-organization.com/coverage/build-a82c3d45-19e6-4f2b-b7d0-63f9a1842c1e.html" }, "vulnerabilities": { "high": 0, "medium": 0, "low": 2, "report": "https://reports.my-organization.com/vulnerabilities/build-d4e5f678-2a31-4c89-9b01-ef45d32a7b90.html" } }' ``` ```bash curl -L -X POST 'https://api.nullplatform.com/metadata/build/:id' \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer ' \ -d '{ "coverage": { "percent": "95", "report": "https://reports.my-organization.com/coverage/build-f1b4be69-1147-4870-a35b-6e718b029f80.html" }, "vulnerabilities": { "high": 0, "medium": 0, "low": 2, "report": "https://reports.my-organization.com/vulnerabilities/build-f1b4be69-1147-4870-a35b-6e718b029f80.html" } }' ``` #### Single key Use this approach when you only need to **create or update one metadata key** for an entity. This is best for fine-grained updates where only one catalog value changes. To send data for a single catalog key (e.g., "apptype"), use the [Create catalog instance (single key) API](/docs/api/catalog-instance-single-create): ```bash curl -L -X POST 'https://api.nullplatform.com/metadata/build/:id/coverage' \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer ' \ -d '{ "application_type": "internal" }' ``` ## Reading instances To fetch all catalog data for a given entity, use the [Read catalog instance (all keys) API](/docs/api/catalog-instance-multiple-read): ```bash np metadata read --id :id --entity build ``` ```bash curl -L 'https://api.nullplatform.com/metadata/build/:id' \ -H 'Authorization: Bearer ' ``` This returns a complete view of the entity's catalog data, enriched with schema and tag information. ## Catalog data validation Whether filled in via the UI or API, catalog data is always validated against the JSON Schema defined in the catalog specification. Validation rules include: - Required fields (`required`) - Enum values - Field type matching (`string`, `number`, etc.) - Disallowing extra fields (`additionalProperties: false`) If validation fails, the platform will return an error detailing which parts of the input didn't match the spec. --- ## Tags in catalogs URL: https://docs.nullplatform.com/docs/catalog/tags When you have many applications, builds, or namespaces, finding and organizing them becomes challenging without a way to group or search by meaningful properties. **Tags** solve this by marking specific catalog fields as searchable, filterable metadata that can be: - **Surfaced in dashboards**: filter applications by compliance level, business unit, or cost center - **Exposed to external tools**: sync with FinOps platforms, inventory systems, or cloud governance tools - **Queried via API**: retrieve all entities matching specific tag criteria Instead of burying important metadata in read-only catalog displays, tags make it discoverable and actionable. ## How tags work A regular catalog field stores metadata on an entity, but only tags are promoted to the platform's filtering and integration layer. When you mark a field with the `"tag"` key in a catalog specification: 1. **In the UI**, the field becomes available for filtering, grouping, and sorting in dashboards and list views. 2. **In the API**, when you query an entity with `?include=tags`, the platform collects all tagged fields and returns them together in a single `tags` object. 3. **In integrations**, external systems can consume the tags to sync metadata. For example, mapping cost centers to your cloud provider's billing tags. Fields **not** marked as tags still appear in catalog instances but are excluded from filters and the `tags` API response. ## Defining tags in a catalog specification To tag a field, add the `"tag"` key to any property in your schema. Here's a specification with a mix of tagged and untagged fields: ```json { "name": "Application metadata", "entity": "application", "metadata": "operational", "nrn": "organization=123", "schema": { "type": "object", "properties": { "business_unit": { "type": "string", "enum": ["Engineering", "Finance", "Operations"], "title": "Business Unit", "tag": true }, "cost_center": { "type": "string", "title": "Cost Center ID", "tag": "cost_center" }, "compliance_level": { "type": "string", "enum": ["PCI", "HIPAA", "SOC2", "None"], "title": "Compliance Requirements", "tag": "compliance" }, "owner": { "type": "string", "title": "Application Owner" } }, "required": ["business_unit"] } } ``` In this example, `business_unit`, `cost_center`, and `compliance_level` are tags. They'll appear in dashboard filters and the `?include=tags` API response. The `owner` field is regular metadata: visible in the catalog but not promoted to the filtering or integration layer. ### Tag naming - **`"tag": true`** uses the property name as the tag key. The field `business_unit` produces a tag called `business_unit`. - **`"tag": "custom_name"`** assigns a custom key. For example, `"tag": "compliance"` exposes the field as `compliance` regardless of the property name. :::tip Use custom names to standardize Custom names let you keep tag keys consistent across different specifications. If two specs track compliance (one for applications, one for namespaces), giving both `"tag": "compliance"` means external integrations can rely on a single, predictable key. ::: ### Supported field types You can tag strings (including enums), numbers, booleans, and arrays. Avoid tagging deeply nested objects. Tag individual fields within them instead. ## Querying tags via API When fetching an entity, add `?include=tags` to collect all tagged fields into a single `tags` object. ```bash curl -L -X GET 'https://api.nullplatform.com/applications/my-app-id?include=tags' \ -H 'Authorization: Bearer ' ``` The response includes only the tagged fields from every catalog specification attached to that entity: ```json { "id": "my-app-id", "name": "my-application", "tags": { "business_unit": "Engineering", "cost_center": "CC0042", "compliance": "SOC2" } } ``` Notice that `owner` (not tagged in the specification above) does not appear here. This makes `?include=tags` useful for integrations that only need the filterable metadata without pulling full catalog instances. ## Common use cases ### Cost allocation and FinOps Tag applications with `cost_center`, `project_code`, or `owner`, then sync those tags to your cloud provider's billing system or FinOps platform. This enables accurate chargeback by department, budget enforcement per business unit, and cost trend reporting. ```json { "cost_center": { "type": "string", "title": "Cost Center", "description": "Finance cost center for billing and allocation", "tag": "cost_center" }, "project_code": { "type": "string", "title": "Project Code", "tag": "project_code" } } ``` ### Compliance and governance Mark applications with the compliance frameworks they're subject to, then filter and report on them across your organization. ```json { "compliance_framework": { "type": "string", "enum": ["PCI", "HIPAA", "GDPR", "SOC2", "None"], "title": "Applicable Compliance Framework", "tag": "compliance" } } ``` With this tag in place, you can audit which applications fall under a specific framework, run compliance scans only on in-scope applications, and generate reports for auditors. Filter on the `compliance` tag in dashboards or query it via the API. ## Integrating tags with external systems Tags become most powerful when you sync them to external platforms. A typical workflow looks like this: 1. **Define tags** in your catalog specification, for example `cost_center`, `environment`, `compliance`. 2. **Query the API** with `?include=tags` to retrieve the tags for each entity. 3. **Map tags** to the external system's format: AWS resource tags, Azure resource labels, or a FinOps tool's cost allocation dimensions. 4. **Apply tags** to the external resources using that platform's SDK or API. This pattern works for cloud provider billing tags (AWS, Azure, GCP), FinOps platforms (CloudHealth, Kubecost, Vantage), inventory and CMDB systems, and custom integrations like Slack notifications enriched with application metadata or automated ticket creation with compliance context. ## Best practices - **Use descriptive names.** `cost_center` instead of `cc`, `compliance_framework` instead of `comp`. - **Align across specs.** Use the same custom tag key (e.g., `"tag": "compliance"`) everywhere the same concept appears. - **Add descriptions.** Help users understand why they're filling out a tagged field. - **Use enums.** Consistent values make queries and integrations reliable. ## Next steps - [Define catalog specifications](/docs/catalog/define-catalog-specs): learn how to create specs with tags - [Create and edit catalog instances](/docs/catalog/instances): attach metadata values to entities - [Where catalogs appear in the UI](/docs/catalog/where-catalog-appears): see how tagged fields power dashboards and filters --- ## Where catalogs appear in the UI URL: https://docs.nullplatform.com/docs/catalog/where-catalog-appears This page explains where and how catalogs appear in the platform, and how you can control their visibility and layout using fields like `visibleOn`, `tag`, and `uiSchema`. Once a catalog instance is attached to an entity, nullplatform automatically surfaces it in the UI without any custom development. The platform uses the associated catalog specification to determine: - **Where** the catalog appears (tabs, side panels, etc.) - **How** it's displayed (tags, read-only sections, editable forms) - **When** it's visible (on create, update, list, read—based on your schema) This makes it easy to build structured, discoverable, and reusable experiences across teams. ## Entity dashboards ### Namespace dashboard - **Form section during creation** If one or more specs exist for `namespace`, a dynamic form will be shown when creating one. - **Catalog blocks on show all modal** Each catalog group (e.g., `operational`, `portfolio`, `managers`) appears as a section. Users can click **Show all** to open the full form for editing. - `links` renders a dedicated “Resources” block at the top of the page. *Example:* ### Application dashboard - **Form section during creation** Specs like `apptype`, `description`, or `platform_info` generate form fields when a new application is created. - **Sidebar catalog** Catalog instances show up as compact summary blocks in the right sidebar. Users can click **Show all** to open the full form for editing. - **Tags and filters** If fields are tagged in the schema, they appear: - In application filters - As chips in table views - As sorting and grouping options - **Built-in blocks** - `links` → renders a **Useful links** component for external tools or dashboards *Example:* ### Build detail page - **Quality Insights section** When a `quality_insights` spec is attached, for example, fields like `coverage_percent`, `complexity`, or `vulnerabilities` are rendered in a dedicated block. - **Visual filters and tags** If a catalog field is marked with `"tag"`, it becomes available for: - Dashboard filters - Custom reporting - Smart grouping in list views - `links` renders a dedicated “Resources” block at the top of the page. *Example:* ### User profiles - If the `about` catalog spec is attached to a user, it renders as a profile summary (e.g., role, contact info, job title) on their member page. - This catalog is read-only in most cases and used for directory browsing and user context. *Example:* ## Controlling field visibility with `visibleOn` Each field in a catalog spec can use a `visibleOn` array to control where it shows up in the UI and platform views. ```json "coverage_percent": { "type": "integer", "description": "Coverage % for this build", "visibleOn": ["create", "update", "read", "list"] } ``` Accepted values: - `create`: shows up in the entity creation form - `read`: shows up in the detail view - `update`: appears when editing catalog - `list`: appears in tables and overviews > 💡 If `visibleOn` is not set, the field defaults to being visible in all views except for `list` views. :::info You’ll define `visibleOn` in the schema of a catalog spec. We dive into specs in the next page. ::: ### Optional UI layout overrides: `uiSchema` In most cases, nullplatform will automatically generate a form layout from your JSON Schema. If you need to customize the layout, such as rendering a field as a radio group, hiding a field, or grouping fields in sections—you can define an optional `uiSchema`. Example: ```json "uiSchema": { "type": "VerticalLayout", "elements": [ { "type": "Control", "scope": "#/properties/application_type", "options": { "format": "radio" } } ] } ``` Use `uiSchema` to: - Override default layout (e.g., horizontal vs. vertical) - Render enums as radio buttons or dropdowns - Control spacing and grouping > 💡 The platform uses JSON forms behind the scenes for rendering and layout. See our > [Designing the UI docs](/docs/json-ui-schema/overview) for more info. ## Tag-driven behavior Any catalog field marked with `"tag": true` (or a custom string like `"tag": "portfolio"`) is automatically exposed to: - Filtering and grouping in dashboards - Sorting in tables - External integrations that rely on tags (e.g., for FinOps, cost reporting, inventory mapping) Example: ```json "portfolio": { "type": "string", "enum": ["Online", "Payments", "RBI"], "tag": "portfolio" } ``` See [Tags in catalogs](/docs/catalog/tags) for detailed use cases, API queries, and integration patterns. ## Next steps - [Define catalog specifications](/docs/catalog/define-catalog-specs) --- ## CLI URL: https://docs.nullplatform.com/docs/cli/ The nullplatform CLI allows you to: - Manage your nullplatform resources directly from the command line. - Interact with the nullplatform API. - Work with your CI/CD pipelines and automate workflows. ## Installation ### Linux and macOS - To install or update the **latest version** of the nullplatform CLI: ```bash curl https://cli.nullplatform.com/install.sh | sh ``` > 💡 Run `np --version` to check which version you have installed. - *(Not recommended)* To install a **previous version**, run: ```bash curl https://cli.nullplatform.com/install.sh | VERSION= sh ``` Replace `` with the desired version number, for example `1.2.3`. ### Windows Use one of the following options to install the CLI on Windows. #### Option 1: PowerShell (recommended) - To install the nullplatform CLI: ```powershell Invoke-WebRequest -Uri https://cli.nullplatform.com/install.ps1 -OutFile install.ps1; .\install.ps1 ``` - Install a specific version: ```powershell Invoke-WebRequest -Uri https://cli.nullplatform.com/install.ps1 -OutFile install.ps1; .\install.ps1 -Version "n.n.n" ``` #### Option 2: Batch script (Command Prompt) 1. Download the installer: ```cmd curl -o install.bat https://cli.nullplatform.com/install.bat ``` 2. Run the installer: ```cmd install.bat ``` - Install a specific version: ```cmd install.bat --version beta ``` After installation, restart your terminal so the PATH changes take effect. ### Upgrading the CLI Once the CLI is installed, you can update it in place without re-running the install script: ```bash np upgrade ``` The command checks whether a newer version is available and installs it for you. ## Authentication Before using the CLI, you need to authenticate. You can log in interactively with `np login`, or use an API key or an access token. ### Log in from the terminal If your organization uses SSO, log in with your browser straight from the terminal: ```bash np login --np-url https://.app.nullplatform.io ``` The command opens your browser to authenticate against your organization's SSO and stores the session locally, so subsequent commands authenticate automatically. You can also provide the URL through the `NP_LOGIN_URL` environment variable instead of the flag. For organizations without SSO, log in with your username and password: ```bash np login --username --password --organization-id ``` :::note `np login` is available from CLI version `2.8.0`. Run `np upgrade` if you don't have it yet. ::: ### API keys You can generate a personal or machine API key to use with the CLI. This is a persistent authentication method that can be used to generate access tokens. - Use an API key as a flag: ```bash np [command] --api-key your_api_key ``` - Or export it as an environment variable: ```bash export NULLPLATFORM_API_KEY=your_api_key_here ``` For more info on API keys, see [Manage API keys](/docs/authorization/api-keys.md). A [personal access token](/docs/authorization/personal-access-tokens) works the same way, if the CLI should act as you. ### Access token You can also authenticate with the access token of your current session. - As with API keys, you can use an access token as a flag: ```bash np [command] --access-token your_access_token ``` - Or export it as an environment variable: ```bash export NP_TOKEN=your_access_token ``` #### How do I get an access token? To retrieve the access token of your current session: 1. Log in to nullplatform. 2. Click your **user menu** (the circle with your initials/user avatar) in the top-right corner. 3. From the dropdown menu, select **Copy session token**. :::warning Deprecation notice The legacy [Chrome extension](https://github.com/nullplatform/chrome-extension) used to retrieve the token is deprecated and no longer maintained or supported. ::: ### Profiles If you work with more than one nullplatform organization, or you keep separate sessions for different environments, use profiles. Each profile stores its own session, so you can switch between them without logging in again. Every command accepts `--profile`. Log in once per profile: ```bash np login --np-url https://.app.nullplatform.io --profile staging ``` Then pass the same flag to the commands you run: ```bash np application list --profile staging ``` You can also select the profile with the `NP_PROFILE` environment variable. This is useful in scripts, and for tools that inherit your shell environment, like the [MCP server](/docs/cli/mcp): ```bash export NP_PROFILE=staging np application list ``` When you don't specify a profile, the CLI uses the one named `default`. There's no registration step: logging in with a name you haven't used before creates the profile. Sessions are stored in your OS keychain when one is available, and in `~/.np/config..yml` otherwise. To log out of a profile, clear its session: ```bash np login --clear --profile staging ``` ## How to use the CLI Check out our [API reference](/docs/api-getting-started), where you'll find CLI examples for every supported endpoint. Simply search for the endpoint you need to see an example for. :::tip Use `np --help` to get information on all available commands. ::: Below is a full example for reference: ```bash title="Create an account in your organization" # Using environment variable authentication export NULLPLATFORM_API_KEY=your_api_key_here np account create --body '{ "name": "mocked-name-1", "slug": "mocked-slug-1", "organization_id": 123, "status": "active" }' # Or using command-line flag np account create --body '{ "name": "mocked-name-1", "slug": "mocked-slug-1", "organization_id": 123, "status": "active" }' --format json --access-token your_access_token # Example response: { "id": 515151, "name": "mocked-name-1", "nrn": "organization=123", "organization_id": 123, "slug": "mocked-slug-1", "status": "active" } ``` ## Browsing your organization Use `np browse` to explore your organization from the terminal without jumping to the dashboard: ```bash np browse ``` It opens an interactive tree of your hierarchy: expand accounts, namespaces, applications, and scopes, and the panel on the right shows the selected entity's details, like its id, slug, status, NRN, and, for scopes, the domain it serves. A few keys cover most of what you'll need: | Key | Action | | --- | --- | | `↑` `↓` | Move through the tree | | `Enter` / `→` | Expand an entity | | `Backspace` / `←` | Go back | | `/` | Filter the tree | | `o` | Open the selected entity in your browser | | `c` | Copy the selected entity's data | | `i` | Show or hide inactive entities | | `q` | Quit | It's the fastest way to find an application or scope, grab its id or NRN for another command, or jump straight to it in the dashboard. --- ## Using the CLI as an MCP server URL: https://docs.nullplatform.com/docs/cli/mcp The `np` binary can run as a [Model Context Protocol](https://modelcontextprotocol.io) server. Once it's connected, your AI coding agent can list, read, and describe nullplatform entities directly, instead of guessing at API calls or asking you to run commands and paste the output back. There's no infrastructure to deploy and no second set of credentials. The server runs locally as a subprocess of your AI client, and it authenticates with the same session `np` already uses. :::tip Don't use the CLI? Nullplatform also hosts an MCP server you connect with one URL and a browser sign-in, from any AI assistant, including the ones that don't run on your machine. See [Connect an AI assistant to nullplatform](/docs/mcp-server). ::: :::info `np mcp` is available from [np 2.9.0](/changelog/cli-2.9.0). Run `np upgrade` to update, then check with `np --version`. ::: ## What your agent can do The server exposes nullplatform as a small set of tools rather than one tool per API operation, so your agent's context stays usable. | Tool | What it does | |---|---| | `np_list` | List entities, with filters and pagination | | `np_read` | Read a single entity | | `np_describe` | Report an operation's parameters and request body fields | | `np_lake_query` | Run read-only SQL against the Customer Lake | | `np_auth_status` | Report whether the session works, and for which profile | | `np_skills`, `np_skill_read` | Browse and read nullplatform's [AI skills](/docs/ai-ops/ai-plugins) | | `np_create`, `np_update`, `np_patch`, `np_delete` | Create and modify entities. Only with `--allow-writes` | Your agent works within your permissions. The server can't do anything your user can't do from the terminal. ## Install it in your AI client Run `np mcp install`. It detects which AI clients you have and configures each one: ```bash np mcp install ``` ``` Installing MCP server "nullplatform" binary /Users/you/.local/bin/np profile default (written as NP_PROFILE in each client's env) mode read-only ✓ Claude Code Claude Code user config (managed by `claude mcp`) ✓ Cursor /Users/you/.cursor/mcp.json ``` Restart your AI client afterwards so it picks up the new server. Three clients are detected automatically: Claude Code, Cursor, and Codex. If you use Kiro, Claude Desktop, or any other client, configure it by hand with the snippet from [Any other MCP client](#any-other-mcp-client). Use `--client` to configure just one: ```bash np mcp install --client cursor ``` The installer writes the **absolute path** of your `np` binary. That matters more than it sounds: applications launched from Finder or the dock don't inherit your shell's `PATH`, so a configuration that just says `np` works in a terminal-based client and fails in a graphical one with nothing but a connection error. Re-running `np mcp install` updates the entry instead of duplicating it, so it's also how you repair one that points at an old binary. ### Any other MCP client For clients without a dedicated adapter, such as Kiro and Claude Desktop, `--print-config` writes the standard MCP snippet to standard output with the path and profile already resolved: ```bash np mcp install --print-config ``` ```json { "mcpServers": { "nullplatform": { "type": "stdio", "command": "/Users/you/.local/bin/np", "args": ["mcp", "serve"], "env": { "NP_PROFILE": "default" } } } } ``` Paste that into your client's MCP configuration. ## Profiles The `np` CLI supports multiple [profiles](/docs/cli/#profiles), each with its own stored session. The MCP server uses them the same way, with one difference that's easy to miss. Graphical applications don't inherit your shell environment, so an `export NP_PROFILE=prod` in your shell profile is invisible to a server your editor launches. To avoid that, the installer writes `NP_PROFILE` directly into the client's configuration, and tells you which profile it anchored. To install against a specific profile, pass `--profile`: ```bash np login --profile prod np mcp install --profile prod ``` The entry is named `nullplatform-prod` rather than `nullplatform`, so profiles don't collide. You can create as many profiles as you need. There's no limit and no registration step: `np login --profile ` creates one. ### One profile per client Because each client has its own configuration, you can point each at a different profile. Run the installer once per client: ```bash np mcp install --client claude-code --profile prod np mcp install --client cursor --profile staging ``` :::note Without `--client`, the installer acts on every detected client using the same profile. Pass `--client` when you want them to differ. ::: ### Several profiles in one client You can also register more than one server in the same client, which is often more useful. MCP clients namespace tools per server, so your agent sees both and picks: ```bash np mcp install --profile prod np mcp install --profile staging --allow-writes ``` That gives you an environment policy your agent can act on: read-only against production, writes allowed against staging. :::tip If a profile has no usable session yet, `np mcp install` still writes the entry and tells you to run `np login --profile `. You can set everything up before logging in. ::: ## Read-only by default The server starts read-only. The tools that create, update, patch, and delete entities are not registered at all, so there's nothing for an agent to call by mistake. The boundary is the absence of the tools, not a check that could be talked around. To enable them, install with `--allow-writes`: ```bash np mcp install --allow-writes ``` The mode is per installation, which is why two entries on different profiles work well: your agent can read production and write to staging in the same session, without either being a decision it makes on its own. :::warning With `--allow-writes`, your agent can create, modify, and delete real nullplatform entities. Enable it deliberately, and prefer scoping it to a non-production profile. ::: ## Checking and removing `np mcp status` reports what each client is actually configured with, reading it back from the client rather than assuming the last install still holds: ```bash np mcp status ``` ``` MCP server "nullplatform" — profile default ✓ Claude Code installed Command: /Users/you/.local/bin/np Args: mcp serve Environment: NP_PROFILE=default · Cursor not detected — /Users/you/.cursor does not exist ``` To remove an entry: ```bash np mcp uninstall np mcp uninstall --client cursor np mcp uninstall --name nullplatform-staging ``` `np mcp install` and `np mcp uninstall` both accept `--dry-run`, which prints what would change and writes nothing. ## Headless and CI For non-interactive environments, set `NULLPLATFORM_API_KEY` in the process environment that launches the server. Nullplatform never writes credentials into a client configuration, only the profile name. That's deliberate. Project-scoped MCP configurations such as `.mcp.json` and `.cursor/mcp.json` are commonly committed to git, and an installer that wrote an API key into one would publish your credential into your repository. ```bash NULLPLATFORM_API_KEY=AAAA.1234567890abcdef1234567890abcdefPTs= np mcp serve ``` ## Related - [CLI](/docs/cli/): installing `np`, authentication, and profiles - [Connect an AI assistant to nullplatform](/docs/mcp-server): the hosted MCP server, with browser sign-in - [Set up the AI plugins](/docs/ai-ops/ai-plugins): the skills catalog your agent can also use --- ## Announcements URL: https://docs.nullplatform.com/docs/communications/announcements Announcements let platform leaders tell everyone in the organization about a release, a maintenance window, or an incident. An announcement shows in the **Announcements** tab of each person's bell, as a banner across the product, in your Slack channels, or any combination of them. :::info Announcements always reach their audience. People can't turn them off in their [inbox settings](/docs/communications#inbox-settings). ::: :::note Only platform leaders can create, edit, close, or delete announcements. ::: ## Create an announcement Go to **Platform Settings > Communications > Announcements** and click **New announcement**. Walk through the form below: Without a schedule, an announcement is published right away and stays until you close it. Its category is one of **Release**, **Changelog**, **Maintenance**, **Incident**, **Advisory**, or **Other**, and its severity sets the icon and the color everywhere it shows, as listed in [Severity levels](#severity-levels). :::warning There's no undo Once published, an announcement lands in every matching **Announcements** tab at once. Closing it later hides it, but not from anyone it already reached. ::: :::tip Planning a maintenance window? Set a schedule so the announcement goes up ahead of time and expires on its own when the window closes. ::: ### Severity levels | Severity | Color | Use it for | |----------|-------|------------| | **Informational** | Blue | General announcements, feature updates, or tips | | **Maintenance** | Yellow | Planned maintenance windows or scheduled changes | | **Degraded** | Orange | Partial service degradation or intermittent issues | | **Outage** | Red | Critical incidents or full service outages | ## Show it as a banner A banner is an announcement shown as a strip across the top of the product. To show one, turn on **Banner** under **Where it shows** and pick the sections it shows in: **Development**, **Platform settings**, or **Insights**. - **Target a resource**: a banner shown only in **Development** can target one resource. Only people with the selected roles on that resource, on what contains it, or on what it contains see it. - **Dismissal**: people can dismiss a banner unless you turn off its switch. Dismissal is saved to each person's account, so a dismissed banner stays hidden in every browser. - **Expiry**: a banner disappears on its own once its schedule expires. :::tip Two active banners rotate every 10 seconds, so each one gets about half the attention. The form warns you when another banner is already live. ::: ## Post it to Slack Turn on **Slack** under **Where it shows** to also post the announcement to up to 10 channels. If Slack isn't connected yet, the block offers **Connect Slack**: it installs the nullplatform app in your Slack workspace, and you only do it once per organization. Invite the app to each channel you post to. :::warning Everyone in the channel reads it Slack doesn't know about roles. If the announcement is only for some roles, everyone in the channel still reads it. ::: Editing the announcement updates its Slack messages, and closing it keeps them. Deleting it also deletes them. Removing a channel stops new posts there, and what was already posted stays. ## Manage announcements The list shows each announcement's state: | State | Meaning | |-------|---------| | **Scheduled** | Its publish date hasn't come yet. Nobody has seen it, so you can edit it freely. | | **Active** | It's showing now. | | **Expired** | Its expiry date passed, and it stopped showing on its own. | | **Closed** | You closed it by hand. | From an announcement's row, you can **Edit**, **Close**, **Reopen**, **Duplicate**, or **Delete** it. Closing hides it without deleting it, and whoever already saw it keeps it. :::tip An expired announcement can't be reopened. Duplicate it to publish a fresh one with new dates. ::: ## Create announcements with the API Publish an announcement from a script or a pipeline, for example to open a maintenance window. Send a [POST request](/docs/api/communications-notification-create) to `https://api.nullplatform.com/communications/notification` with `kind` set to `broadcast`: ```bash np communications notification create \ --body '{ "kind": "broadcast", "category": "platform.maintenance", "title": "Scheduled maintenance on Saturday", "body": "Deployments are paused from 06:00 to 08:00 UTC.", "severity": "maintenance", "surfaces": [ { "type": "inbox" }, { "type": "banner", "sections": ["development"], "dismissible": true } ], "publish_at": "2026-10-30T09:00:00Z", "expires_at": "2026-10-31T08:00:00Z" }' ``` ```bash curl -L -X POST 'https://api.nullplatform.com/communications/notification' \ -H "Authorization: Bearer $NP_ACCESS_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "kind": "broadcast", "category": "platform.maintenance", "title": "Scheduled maintenance on Saturday", "body": "Deployments are paused from 06:00 to 08:00 UTC.", "severity": "maintenance", "surfaces": [ { "type": "inbox" }, { "type": "banner", "sections": ["development"], "dismissible": true } ], "publish_at": "2026-10-30T09:00:00Z", "expires_at": "2026-10-31T08:00:00Z" }' ``` The fields match the form: - **`category`**: `platform.release`, `platform.changelog`, `platform.maintenance`, `platform.incident`, `platform.advisory`, or `platform.other`. - **`surfaces`**: where it shows. `inbox` is the bell's **Announcements** tab, and `banner` takes the `sections` it shows in. Without `surfaces`, it shows only in the bell. - **`audience`** (optional): `{ "grants": [...] }` reaches only the people who hold one of those roles. Leave it out to reach everyone. - **`places`** (optional): the Slack channels it's also posted to, by channel ID and name. - **`publish_at`** and **`expires_at`** (optional): the schedule. Without them, it's published right away and stays until you close it. :::tip Retries are safe with a key Add a `key`, like `"acme.maintenance:2026-10-31"`, so a retry of the same request returns the announcement it already created instead of publishing a second one. ::: ### Manage announcements with the API or the CLI | Request | CLI command | What it does | |---------|-------------|--------------| | [`GET /communications/notification/manage?kind=broadcast`](/docs/api/communications-notification-list) | `np communications notification list --kind broadcast` | Lists your announcements, newest first. | | [`GET /communications/notification/`](/docs/api/communications-notification-read) | `np communications notification read --id ` | Returns one announcement. | | [`PATCH /communications/notification/`](/docs/api/communications-notification-patch) | `np communications notification patch --id --body '{"status": "closed"}'` | Edits it. Set `status` to `closed` to close it, or to `open` to reopen it. | | [`DELETE /communications/notification/`](/docs/api/communications-notification-delete) | `np communications notification delete --id ` | Deletes it for everyone, with its Slack messages. This can't be undone. | :::note The same endpoint also sends a personal notification to specific users: set `kind` to `personal`, list their IDs in `recipients.users`, and set `category` to an inbox rule's ID so people's inbox settings for that rule apply. See [Create a notification](/docs/api/communications-notification-create). ::: ## Next steps - [Post inbox rules to Slack](/docs/communications/inbox-rules#post-inbox-rules-to-slack): connect Slack and send inbox rules to your channels - [Inbox rules](/docs/communications/inbox-rules): decide which notifications reach each person's inbox - [Communications](/docs/communications): the inbox and inbox settings --- ## Communications URL: https://docs.nullplatform.com/docs/communications/ Communications is how nullplatform tells people what needs their attention, right in the product: an approval waiting on them, a deployment that failed, or a maintenance window your platform leaders announced. Communications has three parts: - **Your inbox**: the bell in the header, where each person reads what's for them. - **Announcements**: messages your platform leaders publish to the whole organization, in the bell, as a banner, or both. - **Inbox rules**: what triggers a notification, who receives it, and what it says. nullplatform ships built-in rules, and you can create your own. Beyond the bell, people can also get their notifications [by email](#email), and you can post [inbox rules](/docs/communications/inbox-rules#post-inbox-rules-to-slack) and [announcements](/docs/communications/announcements#post-it-to-slack) to Slack channels. :::note For platform leaders You manage announcements and inbox rules, including what gets posted to Slack, from **Platform Settings > Communications**. ::: ## Your inbox Click the bell in the header to open your notifications: what involves your work in **Inbox**, and news from your platform leaders and from nullplatform in **Announcements**. Walk through it below: Inbox.', anchor: 'inbox-settings-link', placement: 'bottom', align: 'end', }, { title: '5. Pick a mode for each rule', body: 'Off, Involving me, or Everything, in the bell and by email. Your Default applies across the whole organization.', anchor: 'inbox-settings-modes', placement: 'bottom', align: 'end', }, { title: '6. Customize a resource', body: 'Change the settings for one account, namespace, or application. The most specific resource wins.', anchor: 'inbox-settings-resources', placement: 'right', }, ]} > ## Inbox settings Inbox settings let you choose which inbox rules reach you, in the bell and [by email](#email). Open them from the settings icon in the bell, or from **User settings > Inbox**. Each mode decides what you get from a rule: | Mode | What you receive | |------|------------------| | **Off** | Nothing from this rule. | | **Involving me** | What involves you: your own requests, items assigned to you, and approvals you can decide. | | **Everything** | Everything from this rule, whether it involves you or not. Only available for rules where it makes sense. | ### Settings per resource The **Default** settings apply across your whole organization. To change them for one account, namespace, or application, click **Customize a resource** and pick it. A customized resource starts with every rule set to **Inherit**, which takes the value from the closest resource above it. The most specific resource wins. For example, you can turn deployments **Off** for an account and set **Everything** for the one application you own inside it. :::tip Follow an application closely Customize the application and set the rules you care about to **Everything**. You'll get those notifications for anything in that application, not only what involves you. ::: Each customized resource also has a **Mute everything from this resource** switch. Use **Remove customization** to go back to the inherited settings. :::note Changing a setting affects new notifications only. What's already in your inbox stays. ::: ### Email :::note Email is **Off** by default for every inbox rule. You only get emails for the rules you turn on. ::: Each notification arrives as its own email, right away, with its title, its body, and a button to open it in nullplatform. The footer links to your inbox settings, so you can change what reaches you by email at any time. :::info Announcements aren't sent by email. They show in the bell, as a banner, or in Slack. ::: ### Built-in inbox rules These rules come with nullplatform. Rules that are on by default reach you when they involve you. The rest stay off until you turn them on. | Group | Inbox rule | On by default | |-------|------------|---------------| | Approvals | Approval pending, Approval resolved | Yes | | Action items | Action items assigned, Action item resolved | Yes | | Deployments | Deployment failed | Yes | | Deployments | Deployment started, Deployment finished | No | | Builds | Build failed | Yes | | Builds | Build finished | No | | Scopes | Scope failed | Yes | | Scopes | Scope ready | No | | Service actions | Service action failed | Yes | | Service actions | Service action finished | No | | API keys & personal access tokens | Expiring soon | Yes | The inbox rules your organization creates show up in your inbox settings next to the built-in ones. :::info Announcements always reach their audience. They aren't listed in inbox settings, so they can't be muted. ::: ## Next steps - [Announcements](/docs/communications/announcements): publish news to your organization, in the bell, as a banner, or both - [Inbox rules](/docs/communications/inbox-rules): tune the built-in rules, create your own, and send them from your pipelines --- ## Inbox rules URL: https://docs.nullplatform.com/docs/communications/inbox-rules An inbox rule describes something worth telling people about: what triggers it, who receives it, and what it says. nullplatform ships built-in rules for approvals, deployments, builds, and more. Create your own rules to announce events nullplatform doesn't observe, such as a security finding from your CI scanner or a budget alert from your cost tool. Every rule also shows up in each person's [inbox settings](/docs/communications#inbox-settings), so people can tune or mute your rules like the built-in ones. ## Manage inbox rules from the UI Go to **Platform Settings > Communications > Inbox rules**. The list shows the built-in rules and your organization's own, with what triggers each one and where it applies. Use the **Rule** filter to show only **Built-in** or **Custom** rules. Walk through the whole flow, from the list to sending a rule from your pipeline: ### Built-in rules From a built-in rule's actions menu, you can: - **Open** it and edit it for your organization. An edited rule shows an **Edited** chip, and **Reset to default** discards your changes. - **Deactivate** it, so nobody in your organization receives it until you activate it again. - **Duplicate** it to start a new rule from it. A built-in rule keeps its ID and source, so this is how you get a different trigger. ### Create your own rule Click **New inbox rule** to open the editor. It validates the YAML on the left as you type, and **Overview**, next to it, explains in plain words when the rule fires and who it notifies, with a preview of the notification. :::tip Start from an example Pick one of the **Examples** instead of starting from scratch: custom events that your workflows or CI send, or platform events that fire on what happens in nullplatform. ::: Once the rule is saved, its **Emission** tab shows a ready-to-run `curl` command, and you can **Send test to me** to see it in your own inbox. To also post the rule to Slack, add a channel under **Also post to**, at the end of **Overview**. See [Post inbox rules to Slack](#post-inbox-rules-to-slack). Without permission to manage inbox rules on a rule's resource, the editor opens in view-only mode. **Deactivate** stops a rule right away and frees one of your slots, and you can activate it again later. Notifications already sent stay in people's inboxes. :::warning **Delete** removes the rule and everyone's inbox settings for it. To stop a rule for a while, deactivate it instead. ::: :::info Limits An organization can have up to 50 active rules of its own, edited built-in rules included, and up to 10 active rules watching the same entity, like deployments. ::: ## Post inbox rules to Slack Besides the people a rule notifies, you can post every notification of an inbox rule to Slack channels, from **Also post to** in the rule's editor. Each channel gets the rule wherever it happens in your organization. The rule has to be saved first. The first time, **Also post to** offers **Connect Slack**: it installs the nullplatform app in your Slack workspace, and you only do it once per organization. Announcements use the same connection. :::warning Invite the app to each channel The nullplatform app only posts to channels it's a member of, private channels included. Invite it to every channel you want to post to. The channel picker marks the ones it isn't in yet with **App not in channel**. ::: If the connection stops working, for example because the app was removed from your workspace, **Also post to** shows **Slack is disconnected** and a **Reconnect Slack** button. Nothing is posted until you reconnect it, and the rule keeps its channels. ### What a Slack message looks like Each post carries the notification's title in bold, its body, and a button that opens it in nullplatform. Maintenance, degraded, and outage notifications also show their severity. ## What triggers a rule A rule's `source` says what makes it fire: | Source | Fires when | Example | |--------|------------|---------| | `{ "kind": "invoked" }` | Your code calls the emission endpoint. | A CI scanner reports a vulnerability. | | A source name, like `"deployment"` | The entity changes status in nullplatform. | A deployment in a given namespace fails. | | `{ "kind": "stale", ... }` | An entity stays in a status for too long. | A deployment has been running for more than an hour. | A [GET request](/docs/api/inbox-rule-source-list) to `/communications/inbox_rule/source`, or `np communications inbox rule source list`, lists the source names you can use and the statuses and fields each one exposes. The rest of this page focuses on **invoked** rules, the only kind you can send through the API. ## Create an invoked rule with the API Send a [POST request](/docs/api/inbox-rule-create) to `https://api.nullplatform.com/communications/inbox_rule` with the rule definition: ```bash np communications inbox rule create \ --body '{ "id": "acme.security_finding", "source": { "kind": "invoked" }, "context_schema": { "type": "object", "required": ["id", "service", "owner_id"], "properties": { "id": { "type": "string", "description": "The scanner finding ID" }, "service": { "type": "string", "description": "The affected service" }, "summary": { "type": "string", "description": "One line on what was found" }, "owner_id": { "type": "integer", "description": "The nullplatform user who owns the service" } } }, "category": { "group": "security", "label": "Security findings", "description": "Vulnerabilities your CI scanners report.", "configurable": true, "allows_all": true, "default_mode": "involved" }, "severity": "degraded", "audience": [ { "as": "owner", "who": { "user": "${{ context.owner_id }}" }, "action": "acme.security_finding", "content": { "title": "Security finding on ${{ context.service }}", "body": "${{ context.summary }}" } } ] }' ``` ```bash curl -L -X POST 'https://api.nullplatform.com/communications/inbox_rule' \ -H "Authorization: Bearer $NP_ACCESS_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "id": "acme.security_finding", "source": { "kind": "invoked" }, "context_schema": { "type": "object", "required": ["id", "service", "owner_id"], "properties": { "id": { "type": "string", "description": "The scanner finding ID" }, "service": { "type": "string", "description": "The affected service" }, "summary": { "type": "string", "description": "One line on what was found" }, "owner_id": { "type": "integer", "description": "The nullplatform user who owns the service" } } }, "category": { "group": "security", "label": "Security findings", "description": "Vulnerabilities your CI scanners report.", "configurable": true, "allows_all": true, "default_mode": "involved" }, "severity": "degraded", "audience": [ { "as": "owner", "who": { "user": "${{ context.owner_id }}" }, "action": "acme.security_finding", "content": { "title": "Security finding on ${{ context.service }}", "body": "${{ context.summary }}" } } ] }' ``` :::tip To validate the definition without saving it, add `--dry-run` to the CLI command or `?dry_run=true` to the URL. ::: ### The parts of a rule - **`id`**: lowercase words with exactly one dot, like `acme.security_finding`. It's also the rule's ID in inbox settings, and it can't change later. It can't start with `platform.`, and using a built-in rule's ID edits that built-in rule instead of creating a new one. - **`context_schema`**: a JSON Schema for the data you send each time. An invoked rule must require `id`, because sends are idempotent by it. Only basic keywords are allowed: `type`, `properties`, `required`, `enum`, `minLength`, `maxLength`, `minimum`, `maximum`, `items`, `writeOnly`, `description`, and a boolean `additionalProperties`. - **`category`**: how the rule shows up in inbox settings. All six fields are required. `description` is one line of up to 70 characters. `allows_all` lets people choose **Everything** for this rule under a resource, and it requires `configurable`. `default_mode` (`off`, `involved`, or `all`) is what people get until they change it. For email, set `channels.email.default_mode` too: it defaults to `off`. - **`severity`**: `informational`, `maintenance`, `degraded`, or `outage`. It sets the badge color. - **`audience`**: one entry per reading. Each entry is a separate notification with its own wording, named by `as`. `action` must be the rule ID or start with it (for example, `acme.security_finding.unowned`). - **`visible_to`** (optional): the one NRN the rule applies under, like `["organization=1:account=2"]`. It defaults to the organization root. Sends for resources outside it are refused. Templates read the context you send with `${{ context. }}`. Titles and bodies accept inline Markdown (`**bold**`, `*italic*`, `` `code` ``), and bodies also accept links and lists. To word a reading differently by email or in Slack, add `content.email` (`subject`, `title`, `body`) or `content.slack` (`title`, `body`) next to its title and body. Any field you leave out falls back to the reading's own text. ### Who receives it Each audience entry picks its recipients with `who`: - `{ "user": "${{ context.owner_id }}" }`: one user ID from the context. Pass a list of templates and the first one that renders wins. - `{ "can": "approval_request:approve" }`: everyone who holds that action on the resource. - No `who`: only the people who chose **Everything** for this rule on the resource or above it. Add a `fallback` entry, with the same fields, to reach someone else when `who` resolves nobody. For example, a security lead when the service has no owner. ## Send a notification Once the rule exists, send it from your pipeline, workflow, or script. Send a [POST request](/docs/api/inbox-rule-emit) to `https://api.nullplatform.com/communications/inbox_rule//emission` with the resource the event is about (`nrn`) and the data the rule's templates read (`context`): ```bash np communications inbox rule emission create \ --id acme.security_finding \ --body '{ "nrn": "organization=1:account=2:namespace=3:application=4", "context": { "id": "finding-8812", "service": "payments-api", "summary": "CVE-2026-1234 in a transitive dependency", "owner_id": 50 } }' ``` ```bash curl -L -X POST 'https://api.nullplatform.com/communications/inbox_rule/acme.security_finding/emission' \ -H "Authorization: Bearer $NP_ACCESS_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "nrn": "organization=1:account=2:namespace=3:application=4", "context": { "id": "finding-8812", "service": "payments-api", "summary": "CVE-2026-1234 in a transitive dependency", "owner_id": 50 } }' ``` A successful send answers `201`: ```json { "emitted": [{ "id": "6f1c2b9e-3a41-4f0e-9d55-2b7c1e0a9f13", "as": "owner", "recipients": 1 }], "rejected": 0 } ``` > **Note:** `recipients` and `rejected` are counts, never user IDs. ### What happens to the recipients - **Access is checked.** A recipient who can't see the resource at `nrn` doesn't receive the notification and is counted in `rejected`. - **Inbox settings are applied.** People who turned the rule off for that resource don't receive it. - **Followers are added.** People who chose **Everything** for the rule on that resource, or above it, receive it too, when the rule allows it and at least one reading reached someone. They get the reading without `who` if the rule declares one, or else the first reading that reached someone. ### Retries are safe Sends are idempotent by `context.id`, so your pipeline can retry without notifying anyone twice: - Sending the same `id` for the same `nrn` again answers `200`, marks each entry in `emitted` with `"existing": true`, and creates nothing. - Sending the same `id` for a different `nrn` answers `409`. :::warning Use a distinct `context.id` per resource. Reusing one under another resource is refused. ::: ### Test before you send :::tip Try it without notifying anyone - Add `--dry-run` to the CLI command, or `?dry_run=true` to the URL, to see what would be sent, to how many people, and why a reading would reach nobody. Nothing is written. - Add `"deliver_to": "me"` to the body to send the first reading to yourself only. ::: ### Responses | Status | Meaning | |--------|---------| | `201` | At least one notification was created. | | `200` | A retry of an earlier send, or nobody was left to notify. | | `202` | The rule declares a delay or batch behavior, so delivery was deferred. | | `400` | The context doesn't match `context_schema`, the NRN isn't valid or is outside your organization, or the rule isn't invoked. | | `404` | The rule doesn't exist, is deactivated, or doesn't apply to that NRN. | | `409` | That `context.id` was already sent for another resource. | ## Manage rules with the API or the CLI | Request | CLI command | What it does | |---------|-------------|--------------| | [`GET /communications/inbox_rule`](/docs/api/inbox-rule-list) | `np communications inbox rule list` | Lists the built-in rules and your organization's own. | | [`GET /communications/inbox_rule/`](/docs/api/inbox-rule-read) | `np communications inbox rule read --id ` | Returns one rule. | | [`PUT /communications/inbox_rule/`](/docs/api/inbox-rule-update) | `np communications inbox rule update --id --body ` | Replaces one of your rules. Add `--dry-run` or `?dry_run=true` to validate only. | | [`DELETE /communications/inbox_rule/`](/docs/api/inbox-rule-deactivate) | `np communications inbox rule delete --id ` | Deactivates one of your rules. | | [`DELETE /communications/inbox_rule//permanent`](/docs/api/inbox-rule-delete-permanently) | `np communications inbox rule purge --id ` | Deletes one of your deactivated rules for good, with everyone's inbox settings for it. | | [`PUT /communications/inbox_rule//organization_state`](/docs/api/inbox-rule-state-update) | `np communications inbox rule state update --id --body '{"enabled": false}'` | Turns a built-in rule on or off for your organization. | | [`DELETE /communications/inbox_rule//customization`](/docs/api/inbox-rule-customization-delete) | `np communications inbox rule customization delete --id ` | Discards your edits to a built-in rule, like **Reset to default**. | | [`POST /communications/inbox_rule/validate`](/docs/api/inbox-rule-validate) | `np communications inbox rule validate --body ` | Checks a definition and returns every problem found, without saving it. | | [`GET /communications/inbox_rule/schema`](/docs/api/inbox-rule-schema) | `np communications inbox rule schema` | Returns the JSON Schema of a rule definition, for editor completion. | :::note Editing a rule never changes notifications that were already sent. ::: ## Next steps - [Communications](/docs/communications): your inbox, inbox settings, and announcements - [API keys](/docs/authorization/api-keys): create the key your scripts use to get an access token --- ## Connect with a database client URL: https://docs.nullplatform.com/docs/data-lake/connect You can browse and query the data lake from [DBeaver](https://dbeaver.io/) or [DataGrip](https://www.jetbrains.com/datagrip/) using the ClickHouse JDBC driver. The connection goes through the nullplatform API proxy, so the same authentication, authorization, and tenant isolation rules apply. ## Connection parameters Every database client needs the same set of parameters: | Parameter | Value | |---|---| | JDBC URL | `jdbc:clickhouse://api.nullplatform.com:443/data/lake/query/customers_lake?ssl=true&compress=0` | | Host | `api.nullplatform.com` | | Port | `443` (HTTPS) | | Database | `customers_lake` | | SSL | Required (`ssl=true`) | | Authentication | Driver property: `bearer_token` = `` | The token is a short-lived JWT. If you receive a `401 Unauthorized` error, the token has expired and you need to obtain a new one. See [API keys](/docs/authorization/api-keys) for how to generate tokens. ## DBeaver 1. Open DBeaver and go to **Database > New Database Connection**. 2. Search for **ClickHouse** in the driver list and select it. If prompted, let DBeaver download the JDBC driver automatically. 3. In the **Main** tab, enable manual URL mode and enter the JDBC URL: ``` jdbc:clickhouse://api.nullplatform.com:443/data/lake/query/customers_lake?ssl=true&compress=0 ``` Leave **Username** and **Password** empty. 4. Switch to the **Driver Properties** tab and add: | Property Name | Value | |---|---| | `bearer_token` | `` | 5. Click **Test Connection** to verify, then **Finish**. :::note The `compress=0` parameter is required. Without it, the JDBC driver attempts to negotiate compression with the HTTP proxy, which causes connection errors. :::