---
sidebar_label: Archive services and links
toc_max_heading_level: 3
doc_id: 78ded825-29fa-4772-af5c-3f6f43cec3c2
description: >-
  Archive services and links instead of deleting them. Archived instances are
  hidden from lists and keep their configuration, so you can restore them at
  any time.
keywords:
  - services
  - links
  - archive
  - restore
  - soft delete
  - lifecycle
---

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# Archive services and links <span className="heading-release-pill">New</span>

:::info 🚀 Early release

Archiving is in early release and may not be available in your organization yet. If you'd like to try it or want to know more, [reach out to us](https://docs.nullplatform.com/docs/support) and we'll get you set up.

:::

Archiving is a soft delete for services and links. An archived instance stops showing up in your service lists and its link parameters are removed, but the instance and all its attributes stay in nullplatform, so you can restore it whenever you need it.

Use archive when you're decommissioning gradually, pausing a service you may need again, or cleaning up lists without losing configuration. Hard delete is still available, with its usual guards, for when you're sure you won't need the service again.

## How archiving works

When you archive a service or link:

- The instance moves to the `archived` status, and `archived_at` records when it happened.
- If the specification has an `archive` action, nullplatform runs it so your infrastructure can react (for example, quarantining a queue or pausing a database).
- For links, nullplatform removes the link's parameters from the application. Restoring the link recreates them from the service and link attributes as they are at restore time, not from a snapshot taken when you archived.
- The instance and its attributes stay stored and restorable at any time.

Restoring works the same way in reverse: the instance moves to `updating` and then back to `active`, running the `unarchive` action if the specification has one.

```mermaid
stateDiagram-v2
    state "active / failed / cancelled" as source
    source --> archiving: archive
    archiving --> archived
    archiving --> failed: archive action fails
    archived --> updating: restore
    updating --> active
    archived --> deleting: delete
```

### Rules and guards

| Rule | Detail |
| --- | --- |
| Archivable statuses | You can archive from `active`, `failed`, or `cancelled`. |
| Services with links | A service can only be archived when all of its links are already archived. |
| Restoring links | A link can only be restored while its parent service is `active`. |
| Deleting | Delete works on archived instances too. Deleting a service still requires removing its links first, including archived ones. |
| Duplicates | You can't create a service or link that matches an archived one (same specification, entity, and dimensions). The error names the archived instance, so you can restore it or delete it first. |

:::info
Archiving requires the ordinary **write** permission on the service, not the delete permission. Archive is recoverable, so it doesn't need the stricter permission that hard deletes require.
:::

### What happens to link parameters

Archiving a link removes its parameters from the application, and restoring it recreates them. Archiving doesn't keep a copy of the removed values: on restore, nullplatform rebuilds the parameters from the current attributes of the link and of its service.

:::info Restored parameters reflect the current attributes
If the service or link attributes changed while the link was archived, the restored parameters carry the current values, not the ones the link had when you archived it.
:::

## Archive and restore

Archiving is a status change: set the instance's `status` to `archived`, and back to `active` to restore it. You can do it from the UI, from the CLI, or with a PATCH request to the [service](/docs/api/service-update) or [link](/docs/api/link-update) API.

<Tabs
defaultValue="archive-ui"
values={[
{ label: 'UI', value: 'archive-ui' },
{ label: 'CLI', value: 'archive-cli' },
{ label: 'cURL', value: 'archive-curl' },
]}>

<TabItem value="archive-ui">

1. Go to **Platform settings** and open **Services** → **List**.
2. Find the service you want to archive and open its actions menu. **Archive** is only offered for services in `active`, `failed`, or `cancelled` status.
3. Confirm in the dialog. It asks you to type `archive`, and reminds you that all of the service's links must be archived or deleted first.

<img alt="The services list with a row's actions menu open, showing Archive between Set desired version and the destructive Mark as failed and Delete options, and the Show archived toggle above the table" src="/img/services/archive-service-menu.png" width="100%" className="helper-image" />

To restore a service, first bring the archived ones into view: turn on the **Show archived** toggle, or pick **Archived** in the status filter. The same actions menu then offers **Restore**, also behind a confirmation.

Links work the same way from the service's links list: **Archive** removes the link's parameters from the application, and **Restore** recreates them from the current service and link attributes. Restore is only available while the parent service is active.

Developers can also archive and restore from the **Development** view: open the application, go to **Services**, and use the actions menu under **Owned by this application**. The confirmation there is a plain dialog with no word to type, and it can show what archiving does to that particular service (see [Explain what archiving does to your service](#explain-what-archiving-does-to-your-service)).

While an instance is `archiving`, its status chip shows the operation in progress and destructive actions stay disabled until it finishes.

</TabItem>

<TabItem value="archive-cli">

Archive a service by patching its `status` to `archived`:

```bash
np service patch \
  --id <SERVICE_ID> \
  --body '{ "status": "archived" }'
```

Restore it by patching the status back to `active`:

```bash
np service patch \
  --id <SERVICE_ID> \
  --body '{ "status": "active" }'
```

Links follow the same pattern with `np link patch`:

```bash
np link patch \
  --id <LINK_ID> \
  --body '{ "status": "archived" }'
```

</TabItem>

<TabItem value="archive-curl">

Archive a service by patching its `status` to `archived`:

```bash
curl -L -X PATCH 'https://api.nullplatform.com/service/<SERVICE_ID>' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <token>' \
-d '{ "status": "archived" }'
```

Restore it by patching the status back to `active`:

```bash
curl -L -X PATCH 'https://api.nullplatform.com/service/<SERVICE_ID>' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <token>' \
-d '{ "status": "active" }'
```

Links follow the same pattern against `/link/<LINK_ID>`:

```bash
curl -L -X PATCH 'https://api.nullplatform.com/link/<LINK_ID>' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <token>' \
-d '{ "status": "archived" }'
```

</TabItem>
</Tabs>

If you manage your services as code, see [OpenTofu/Terraform](#opentofu-terraform) for archiving them from your configuration.

### What happens after the request

Archiving resolves in one of three ways, depending on the specification. This mirrors how delete behaves:

- **The specification has a managed `archive` action** → nullplatform creates and runs the action for you in the same request. The instance answers with status `archiving` (or `updating` on restore) and the action appears in `actions_in_progress`.
- **The specification has an unmanaged `archive` action** → the request returns `400`, and you run the archive action yourself, the same way you run any other [service action](/docs/services/craft-a-service/service-actions).
- **The specification has no `archive` action** → the status changes directly, with the same guards applied.

An archive request can't include `attributes` (there is no workflow to apply them to). Other metadata fields in the same patch apply immediately.

:::note
A restore that resolves as a direct status change enforces the specification's required attributes, like any other direct write to `active`. If an archived instance has an incomplete attribute set, restore it through the `unarchive` action, or complete its attributes first.
:::

## Filtering archived instances

The API lists archived instances like any other status, so they show up when you [list services](/docs/api/service-list) or [list links](/docs/api/link-list) until you filter them out. Hiding them is what the UI's **Show archived** toggle does for you.

To list everything except archived instances, exclude them with `status:ne`:

```bash
curl -L 'https://api.nullplatform.com/service?nrn=<NRN>&status:ne=archived' \
-H 'Authorization: Bearer <token>'
```

To list only the archived ones, filter by `status`:

```bash
curl -L 'https://api.nullplatform.com/service?nrn=<NRN>&status=archived' \
-H 'Authorization: Bearer <token>'
```

Both filters work the same way against `/link`. The `status:ne` filter combines with `status` as an AND, so `?status=archived&status:ne=archived` returns nothing. Fetching an instance by ID works regardless of its status.

Archiving also changes what the `has_links` filter returns. It counts only links that are neither archived nor deleted, so a service whose links are all archived comes back under `?has_links=false`. A link that is still `archiving` counts as a link, because the operation hasn't landed yet.

## Enable archive on your specifications

Whether archive runs a workflow depends on the service specification:

- **New specifications** created with `use_default_actions` get the `archive` action generated automatically, alongside create, update, and delete. The `unarchive` action is never auto-generated.
- **Existing specifications** are never modified automatically. You opt in explicitly by [creating the action specification](/docs/api/service-specification-actions-create), from the CLI or the API.

<Tabs
defaultValue="optin-cli"
values={[
{ label: 'CLI', value: 'optin-cli' },
{ label: 'cURL', value: 'optin-curl' },
]}>

<TabItem value="optin-cli">

```bash
np service specification action specification create \
  --serviceSpecificationId <SPEC_ID> \
  --body '{
    "name": "Archive service",
    "type": "archive"
  }'
```

</TabItem>

<TabItem value="optin-curl">

```bash
curl -L -X POST 'https://api.nullplatform.com/service_specification/<SPEC_ID>/action_specification' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <token>' \
-d '{
  "name": "Archive service",
  "type": "archive"
}'
```

</TabItem>
</Tabs>

Create a second one with `"type": "unarchive"` to run a workflow on restore too. Link specifications work the same way, through their own [action specifications](/docs/services/craft-a-service/link-action-specs).

For specifications using default actions, nullplatform generates the action's parameters and results schemas from the specification's attributes and keeps them in sync on every specification update. Sending your own `parameters` or `results` returns a `400`.

Once you've opted in, nullplatform owns the action's content: `PATCH` is refused, and `DELETE` is how you opt out. For agent-backed specifications (`use_default_actions: false`), you author both action types with your own schemas, like any other action.

:::tip
Opt in to both `archive` and `unarchive` together. If only `archive` exists, archiving runs your workflow but restoring falls back to a direct status change, so nothing ever tells your infrastructure to undo the archive.
:::

### OpenTofu/Terraform {#opentofu-terraform}

If you manage your specifications as code with the [nullplatform IaC provider](/docs/iac-provider), the opt-in is a [`nullplatform_action_specification`](https://registry.terraform.io/providers/nullplatform/nullplatform/latest/docs/resources/action_specification) resource carrying only the `name`, the `type`, and the parent specification reference. Leave `parameters` and `results` out: nullplatform generates them, and sending your own is refused.

```hcl
resource "nullplatform_action_specification" "unarchive_queue" {
  name                     = "Restore queue"
  type                     = "unarchive"
  service_specification_id = "<SPEC_ID>"
}
```

Use `link_specification_id` instead for a link specification.

Which of the two actions you declare depends on where the specification came from:

- **Created with `use_default_actions`**: the `archive` action already exists, so declaring it fails with `There is already an action of type archive`. Adopt the generated one with `terraform import` if you want it in state, and declare only `unarchive`.
- **Created before archiving existed, or with `use_default_actions: false`**: neither action is generated, so declare both.

You don't need `ignore_changes` here. The provider treats `parameters` and `results` as computed, so the schemas nullplatform generates land in state without showing up as drift. Creating the resource is the opt-in and destroying it is the opt-out, so `tofu destroy` and `terraform destroy` behave as expected: archiving falls back to a direct status change.

#### Archiving the services themselves

The provider archives service instances too, not just the specification's opt-in. Set `archive_on_destroy` and `terraform destroy` archives the service instead of deleting it, leaving the row, its attributes, and its infrastructure in place:

```hcl
resource "nullplatform_service" "orders_queue" {
  name               = "orders-queue"
  specification_id   = "<SPEC_ID>"
  entity_nrn         = "<APPLICATION_NRN>"
  archive_on_destroy = true

  timeouts {
    delete = "10m"
  }
}
```

To archive or restore on demand instead, set `status` to `archived` or `active` on a service that already exists, and give the resource an `update` timeout so the apply can wait for the transition. Leave `status` out of your configuration the rest of the time, so a service archived outside your code isn't restored by the next unrelated apply. The read-only `archived_at` attribute is available as an output.

:::warning
`archive_on_destroy` is read from state, so the apply that sets it has to run before the destroy that relies on it. Adding the flag and destroying in the same run still hard-deletes the service.
:::

The [`nullplatform_service` resource](https://registry.terraform.io/providers/nullplatform/nullplatform/latest/docs/resources/service) documents the full destroy behavior, including how `force_destroy` and `import` interact with `archive_on_destroy`.

## Explain what archiving does to your service

"Archive" means something different for every specification. Archiving an SQS queue might quarantine it, while archiving a database might pause it. Developers don't see any of that from the confirmation dialog: on its own, it only tells them that the service will be hidden from the list and can be restored later.

As the specification author, you decide what else that dialog says. The `archive` action's parameters schema can carry a `uiSchema` with a markdown label, and the **Development** view renders it inside the archive confirmation, right below nullplatform's built-in message. The developer reads what will happen to that particular service before confirming, without leaving the dialog. The Platform settings dialogs don't render it yet: they show only the built-in message.

### What developers see

The dialog has two parts. The message at the top is nullplatform's, and you can't change it. Everything below it is your label, rendered from the action specification:

<img alt="The archive confirmation dialog in the Development view: the built-in message says the service is hidden and can be restored, and the specification's label below it explains that the queue is quarantined, producers and consumers are denied, the queue and its messages are kept, and restore lifts the deny policy" src="/img/services/archive-confirmation.png" width="80%" className="helper-image" />

### Add the explanation to the archive action

This works on agent-backed specifications (`use_default_actions: false`), where you author the action's content. The explanation lives in the `parameters.schema` of the `archive` action specification. Set it when you create the action, or update the action if it already exists:

<Tabs
defaultValue="explain-ui"
values={[
{ label: 'UI', value: 'explain-ui' },
{ label: 'CLI', value: 'explain-cli' },
{ label: 'cURL', value: 'explain-curl' },
]}>

<TabItem value="explain-ui">

1. Go to **Platform settings** and open **Services** → **Specifications**.
2. Click **New service specification**, or open the specification you want to edit. You land on the specification view: the JSON editor on the left, and a preview of what developers get on the right.

   <img alt="The service specification view in Platform settings: the Specification data tab shows the SQS Queue specification JSON in a code editor, the Preview tab on the right renders the create form, and the Action specifications and Link specifications buttons sit at the top right" src="/img/services/service-specification-view.png" width="100%" className="helper-image" />

3. Click **Action specifications** at the top right. Then click **New action specification**, or open the existing `archive` action from the list.
4. Fill in the three editor tabs. In **Specification data**, name the action and set its type. Leave `results` and `parameters` as the editor shows them: the parameters schema is edited in the next two tabs.

   ```json
   {
     "name": "Archive queue",
     "type": "archive"
   }
   ```

   In **Schema**, declare an object with no properties:

   ```json
   {
     "type": "object",
     "properties": {}
   }
   ```

   In **UI schema**, paste the layout with your label:

   ```json
   {
     "type": "VerticalLayout",
     "elements": [
       {
         "type": "Label",
         "options": { "format": "markdown" },
         "text": "**What happens when the archive completes**\n\nThe queue is quarantined, not deleted:\n\n- Producers and consumers are **denied** while the queue is archived.\n- The queue, its **ARN** and its **messages** (within the retention period) are kept.\n- **Restore** lifts the deny policy and the queue resumes working as it was."
       }
     ]
   }
   ```

5. Check the **Preview** tab on the right: it renders your text the way the confirmation dialog will. Click **Create**, or **Save** if you're editing an existing action.

   <img alt="The Create action specification view with the UI schema tab active: the editor on the left holds the VerticalLayout with the markdown Label, and the Preview tab on the right renders it as a bold heading, a paragraph and a three-item list" src="/img/services/action-specification-editor.png" width="100%" className="helper-image" />

</TabItem>

<TabItem value="explain-cli">

Create the `archive` action with the explanation in its parameters schema:

```bash
np service specification action specification create \
  --serviceSpecificationId <SPEC_ID> \
  --body '{
    "name": "Archive queue",
    "type": "archive",
    "parameters": {
      "schema": {
        "type": "object",
        "properties": {},
        "uiSchema": {
          "type": "VerticalLayout",
          "elements": [
            {
              "type": "Label",
              "options": { "format": "markdown" },
              "text": "**What happens when the archive completes**\n\nThe queue is quarantined, not deleted:\n\n- Producers and consumers are **denied** while the queue is archived.\n- The queue, its **ARN** and its **messages** (within the retention period) are kept.\n- **Restore** lifts the deny policy and the queue resumes working as it was."
            }
          ]
        }
      },
      "values": {}
    }
  }'
```

If the `archive` action already exists, patch its parameters instead. List the specification's actions to find its ID:

```bash
np service specification action specification list \
  --serviceSpecificationId <SPEC_ID>
```

```bash
np service specification action specification patch \
  --serviceSpecificationId <SPEC_ID> \
  --id <ACTION_SPEC_ID> \
  --body '{
    "parameters": {
      "schema": {
        "type": "object",
        "properties": {},
        "uiSchema": {
          "type": "VerticalLayout",
          "elements": [
            {
              "type": "Label",
              "options": { "format": "markdown" },
              "text": "**What happens when the archive completes**\n\nThe queue is quarantined, not deleted:\n\n- Producers and consumers are **denied** while the queue is archived.\n- The queue, its **ARN** and its **messages** (within the retention period) are kept.\n- **Restore** lifts the deny policy and the queue resumes working as it was."
            }
          ]
        }
      },
      "values": {}
    }
  }'
```

</TabItem>

<TabItem value="explain-curl">

[Create](/docs/api/service-specification-actions-create) the `archive` action with the explanation in its parameters schema:

```bash
curl -L -X POST 'https://api.nullplatform.com/service_specification/<SPEC_ID>/action_specification' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <token>' \
-d '{
  "name": "Archive queue",
  "type": "archive",
  "parameters": {
    "schema": {
      "type": "object",
      "properties": {},
      "uiSchema": {
        "type": "VerticalLayout",
        "elements": [
          {
            "type": "Label",
            "options": { "format": "markdown" },
            "text": "**What happens when the archive completes**\n\nThe queue is quarantined, not deleted:\n\n- Producers and consumers are **denied** while the queue is archived.\n- The queue, its **ARN** and its **messages** (within the retention period) are kept.\n- **Restore** lifts the deny policy and the queue resumes working as it was."
          }
        ]
      }
    },
    "values": {}
  }
}'
```

If the `archive` action already exists, [update](/docs/api/service-specification-actions-update) its parameters instead. List the specification's actions to find its ID:

```bash
curl -L 'https://api.nullplatform.com/service_specification/<SPEC_ID>/action_specification' \
-H 'Authorization: Bearer <token>'
```

```bash
curl -L -X PATCH 'https://api.nullplatform.com/service_specification/<SPEC_ID>/action_specification/<ACTION_SPEC_ID>' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <token>' \
-d '{
  "parameters": {
    "schema": {
      "type": "object",
      "properties": {},
      "uiSchema": {
        "type": "VerticalLayout",
        "elements": [
          {
            "type": "Label",
            "options": { "format": "markdown" },
            "text": "**What happens when the archive completes**\n\nThe queue is quarantined, not deleted:\n\n- Producers and consumers are **denied** while the queue is archived.\n- The queue, its **ARN** and its **messages** (within the retention period) are kept.\n- **Restore** lifts the deny policy and the queue resumes working as it was."
          }
        ]
      }
    },
    "values": {}
  }
}'
```

</TabItem>
</Tabs>

Whichever path you take, three things make this work:

- **Empty `properties`**. The archive action doesn't need input from the developer, so the schema declares no fields. The dialog only shows your text.
- **A `Label` element with `options.format` set to `markdown`**. That's what turns the `text` into a heading, a paragraph, and a list. Without the format option, a label renders as a plain heading, which only suits short titles.
- **A `text` that answers the developer's question**. Say what happens to the underlying resource, what is kept, and what restore does. Write it for the person about to click **Archive**, not for whoever operates the module.

Link specifications work the same way through their own [action specifications](/docs/services/craft-a-service/link-action-specs): the label shows up when a developer archives a link from the service's links list.

### Cover the restore too

Give your `unarchive` action its own label describing what restore does, so both directions explain themselves. The restore confirmation renders it the same way:

<img alt="The restore confirmation dialog in the Development view: the built-in message says the service comes back to active, and the specification's label below it explains that the deny policy is removed, the queue keeps its name, ARN and messages, and the service returns to active" src="/img/services/restore-confirmation.png" width="80%" className="helper-image" />

### Keep in mind

- The `uiSchema` here is the same [UI schema](/docs/json-ui-schema/ui-schema) every action form uses, and the dialog renders it with the same form engine. Labels are just one element type: layouts, controls bound to your parameter properties, and the rest of the [UI elements](/docs/json-ui-schema/reference-options) work too.
- The dialog renders the whole parameters schema. If your action also declares parameter properties, the confirmation shows those fields too and sends the values as the action's parameters.
- On specifications published as a versioned package, the explanation is frozen with the rest of the action's content: each instance shows the copy from its pinned package revision. Publish a new revision for updated text to reach existing instances.
- Specifications using default actions can't carry a custom explanation: nullplatform generates and owns their action content, and the dialog shows only the built-in message.

## Implement archive in your service workflows

For agent-backed services, `archive` and `unarchive` actions run through the same workflow and in the same working directory as your create and update actions. Archive is one more state your module applies, not a separate code path: the same apply runs, and your code decides what "archived" means for the underlying resource.

A typical pattern:

1. Derive an `archived` flag in your module from the action being executed, falling back to the service's status so that a later update doesn't accidentally un-archive the resource.
2. Use that flag to apply the archived state conditionally. For example, an SQS queue module can attach a queue policy that denies sending and receiving messages while archived. The queue keeps its name, ARN, and messages; restoring removes the policy.
3. Remember that archive and unarchive actions don't carry creation parameters. If your module reads inputs from action parameters, resolve them from stored state or outputs when parameters are empty.

:::warning
Make sure every execution path of your service treats `archive` and `unarchive` as an apply. If your automation only maps known action types and defaults everything else to a plan or a no-op, an archive action will report success without touching your infrastructure.
:::

Deploy your workflow changes before you create the `archive` and `unarchive` action specifications. If the actions exist first, archiving runs an apply with no changes: the service ends up marked as archived while your infrastructure stays untouched.

## Related docs

- [Manage and provision services](/docs/services/manage-services): provision services and link them to your applications
- [Action specifications](/docs/services/craft-a-service/service-actions): define the actions a specification exposes
- [Craft a service](/docs/services/craft-a-service/overview): build and publish your own service specifications
- [Special schema keys and UI schema](/docs/json-ui-schema/overview): design the forms and labels your actions render
