---
title: Populate form fields with live infrastructure data
description: "🎯 Set up external field resolution so specification fields pull real-time values from your infrastructure through the nullplatform agent."
toc_max_heading_level: 2
sidebar_label: Dynamic values with external fields
doc_id: 4a8e92c1-7d3f-4b16-a5e0-d2f19c6b8a47
tutorial_type: tutorial
tutorial_category: services
tutorial_time: 20 min
tutorial_featured: false
tutorial_cover: /img/tutorials/covers/pull-real-time-values-from-your-infrastructure.svg
keywords:
  - external context
  - dynamic schemas
  - agent
  - notification channel
  - specifications
  - additionalKeywords
tags:
  - services
  - agent
  - automation
---

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

# Populate form fields with live infrastructure data

> 🎯 **Goal:** Create a service specification whose `owner` field is dynamically populated with real Postgres users fetched from your infrastructure by the nullplatform agent.

## Introduction

By default, specification schemas in nullplatform are static: enum values and defaults are defined at design time. [External fields](/docs/services/craft-a-service/external-context) changes this by fetching live data from your infrastructure every time a specification is read.

In this tutorial you'll wire up all the pieces end to end: a service specification, an action specification with an `external` field, a notification channel, and an agent handler script. By the end, reading the specification (from the dashboard or the REST API) returns an `owner` dropdown populated with actual database users.

## What you'll set up

By the end of this tutorial you'll have:

- A **service specification** for a Postgres database
- An **action specification** with external fields that fetches users from your database
- A **notification channel** that routes external field requests to your agent
- A **handler script** that queries Postgres and returns the result as JSON

## Prerequisites

- The [nullplatform CLI](/docs/cli/) installed
- The [nullplatform agent installed](/docs/agent/installation) in your infrastructure
- An [API key with **Agent** and **Ops** roles](/docs/agent/authentication)
- A running Postgres instance accessible from the machine where the agent runs
- `jq` installed on the agent machine

## 1. Create the service specification

Start by creating the service specification that defines the Postgres database attributes. This uses the [Create a service specification](/docs/api/service-specification-create) endpoint.

```bash
np service specification create --body '{
  "name": "Postgres Database",
  "slug": "postgres-db",
  "use_default_actions": false,
  "attributes": {
    "schema": {
      "type": "object",
      "properties": {
        "db_name": { "type": "string", "title": "Database name" },
        "owner": { "type": "string", "title": "Database owner", "readOnly": true }
      }
    }
  }
}'
```

Save the returned `id`. You'll need it in the next step as `$service_spec_id`.

## 2. Create the action specification with external fields

Create the action specification that declares what data to fetch and where to inject it. This uses the [Create a service specification action](/docs/api/service-specification-actions-create) endpoint.

```bash
np service specification action specification create \
  --serviceSpecificationId $service_spec_id \
  --body '{
    "name": "Create Postgres Database",
    "type": "create",
    "external": {
      "action": "list-postgres-users",
      "output_schema": {
        "type": "object",
        "properties": {
          "users": { "type": "array", "items": { "type": "string" } }
        },
        "required": ["users"]
      }
    },
    "parameters": {
      "schema": {
        "type": "object",
        "properties": {
          "db_name": { "type": "string", "title": "Database name" },
          "owner": {
            "type": "string",
            "title": "Database owner",
            "additionalKeywords": {
              "enum": ".external.users"
            }
          }
        },
        "required": ["db_name", "owner"]
      },
      "values": {}
    },
    "results": {
      "schema": { "type": "object", "properties": {} },
      "values": {}
    }
  }'
```

The key parts:

- **`external.action`**: The action name the agent will receive: `"list-postgres-users"`.
- **`external.output_schema`**: Validates the agent's response. If it doesn't match, the resolution fails gracefully.
- **`additionalKeywords.enum`**: The jq expression `.external.users` references the resolved data.

## 3. Create the notification channel

External field notifications use `source: "service"`, which is the same source as infrastructure notifications. Use an `action` filter to ensure this channel only receives external field requests:

```bash
np notification channel create --body '{
  "nrn": "organization=1:account=2:namespace=3:application=4",
  "source": ["service"],
  "description": "External fields for Postgres DB",
  "type": "agent",
  "configuration": {
    "api_key": "AAAA.1234567890abcdef1234567890abcdefPTs=",
    "command": {
      "type": "exec",
      "data": {
        "cmdline": "services/handle-external-context.sh",
        "environment": { "NP_ACTION_CONTEXT": "${NOTIFICATION_CONTEXT}" }
      }
    },
    "selector": { "environment": "local" }
  },
  "filters": {
    "action": { "$in": ["list-postgres-users"] }
  }
}'
```

:::info Why filter by action?
The `$in` filter creates an allowlist of external field actions this channel should handle. This is safer than a `$nin` denylist. If nullplatform adds new infrastructure action types in the future, they won't accidentally match your channel. See [External fields — channel separation](/docs/services/craft-a-service/external-context#set-up-the-notification-channel) for details.
:::

Replace the `nrn` and `api_key` values with your own. The `nrn` should match the application where you want external fields to resolve.

## 4. Deploy the handler script

On the machine where the agent runs, create `services/handle-external-context.sh`:

```bash
#!/bin/bash
CONTEXT="$NP_ACTION_CONTEXT"
ACTION=$(echo "$CONTEXT" | jq -r '.action')

echo "Resolving external field for action: $ACTION" >&2

case "$ACTION" in
  list-postgres-users)
    USERS=$(psql -h "$DB_HOST" -U "$DB_ADMIN" -t -c "SELECT usename FROM pg_user" | \
      jq -R -s 'split("\n") | map(select(length > 0) | ltrimstr(" ") | rtrimstr(" "))')
    echo "{\"users\": $USERS}"
    ;;
  *)
    echo "Unknown action: $ACTION" >&2
    exit 1
    ;;
esac
```

Make it executable:

```bash
chmod +x services/handle-external-context.sh
```

:::warning Only JSON in stdout
The agent captures **everything** written to stdout and parses it as JSON. Logging, debug prints, or progress indicators will break the response. **Always redirect logging to stderr** with `>&2`.
:::

## 5. Test it

Read the action specification with an `application_id` to verify external field resolution. This uses the [Read an action specification](/docs/api/action-specification-read) endpoint.

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

1. In the nullplatform dashboard, go to an application that has an agent configured.
2. In the Development view, go to **Service** and click **+ New service**.
3. In the service creation form, the `owner` field should render as a dropdown with the actual Postgres users from your database.

<img alt="external-context-owner-dropdown" src="/img/tutorials/external-context-owner-dropdown.png" width="100%" className="helper-image" />

</TabItem>
<TabItem value="test-cli">

```bash
np action specification read \
  --id $action_spec_id \
  --application-id $app_id
```

In the response, verify that:

- `parameters.schema.properties.owner.enum` contains the actual Postgres users
- `external_resolution.status` is `"success"`

</TabItem>
<TabItem value="test-curl">

```bash
curl -s "https://api.nullplatform.com/action_specification/$action_spec_id?application_id=$app_id" \
  -H "Authorization: Bearer <token>" | jq .
```

In the response, verify that:

- `parameters.schema.properties.owner.enum` contains the actual Postgres users
- `external_resolution.status` is `"success"`

</TabItem>
</Tabs>

If resolution fails, the response includes an `external_resolution` object with the error details. See [Troubleshooting](/docs/services/craft-a-service/external-context#troubleshooting) for common issues and fixes.

## Next steps

- [External fields reference](/docs/services/craft-a-service/external-context): full reference for configuration, resolution statuses, and troubleshooting
- [Additional keywords](/docs/json-ui-schema/json-schema#additional-keywords): learn about jq expressions and the full context available for dynamic schemas
- [Action specifications](/docs/services/craft-a-service/service-actions): learn about action specification design and lifecycle
