---
sidebar_label: Behavior-based overrides
doc_id: ede0fb6a-0c08-4fc0-88f0-f4ba824848d1
description: >-
  Customize scope runtime behavior by overriding workflow steps - skip, replace,
  or add new logic beyond config changes.
keywords:
  - behavior overrides
  - workflows
  - scope customization
  - lifecycle steps
  - YAML configuration
---

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

# Make behavior-based overrides

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
