> ## Documentation Index
> Fetch the complete documentation index at: https://help.statisfy.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Portal Runtime Config

> Configure SDK components from portal.runtime.json — literal values, per-customer $field references, and the useResolvedFields hook.

Inside a Statisfy-hosted [Portal](/portals/overview), SDK components can read their settings from the portal's **runtime config** instead of hardcoded props. That's what lets a non-developer repoint a chat module at a different Digital Worker from **Settings** without touching code — and it's what makes a component's values differ per signed-in customer.

<Note>
  This page only applies to portals. If you're embedding the SDK in an app you host yourself, pass props directly and skip it.
</Note>

## The config file

Every portal repository has a `portal.runtime.json` at its root. Config rides the commit, so the file that ships with a build **is** that build's config — there's no separate promote step.

```json theme={null}
{
  "shared": {},
  "components": {
    "digital_worker_chat": {
      "support": { "worker_id": "your-digital-worker-uuid" }
    },
    "onboarding_module": {
      "onboarding": { "worker_id": "another-worker-uuid" }
    }
  }
}
```

The shape is `{ shared: { name: value }, components: { type: { configId: { prop: value } } } }`:

| Level                          | Meaning                                                                                                                |
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------- |
| `shared`                       | Portal-wide properties consumed by more than one component.                                                            |
| `components.<type>`            | The component type, e.g. `digital_worker_chat`, `onboarding_module`.                                                   |
| `components.<type>.<configId>` | One instance's slot, keyed by the component's `configId` prop. Two instances with the same `configId` share this slot. |

A component opts into config mode by taking a **literal** `configId` — see [Config mode](/sdk/v0/digital_worker_chat#config-mode-inside-a-statisfy-portal). The portal save gate extracts that id into the component manifest and rejects a missing or non-literal one.

## Two kinds of value

```json theme={null}
{ "worker_id": "6f1c…" }                       // LITERAL — the value is the answer
{ "greeting":  { "$field": "welcome_note__c" } } // FIELD — per-customer lookup
```

* **Literal** — a plain string, available at load with no network round trip (it's bundled in the commit).
* **Field reference** — `{"$field": "<account field key>"}`. Resolved per visitor to that field's value on the **signed-in customer's account**, so one build renders differently for each customer.

Blank collapses to unset: `""` and a missing field both resolve to `null`.

## Reading resolved values: `useResolvedFields`

Components in config mode resolve their own values, so you rarely need this hook — reach for it when you're writing a custom component that wants the same per-customer data.

```tsx theme={null}
import { useResolvedFields, isFieldRef } from '@statisfy/digital-workers-react'
import runtime from '../portal.runtime.json'

function WelcomeBanner() {
  const { resolve, loading, error, refetch } = useResolvedFields()
  const configured = runtime.components?.hero?.main?.greeting

  if (error) return <button onClick={refetch}>Retry ({error.message})</button>

  // Literals return immediately; $field refs are null until the fetch lands.
  const greeting = resolve(configured)
  if (loading && isFieldRef(configured)) return <Skeleton />

  return <h1>{greeting ?? 'Welcome'}</h1>
}
```

### `UseResolvedFieldsResult`

| Field     | Type                             | Description                                                                                                                                                  |
| --------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `fields`  | `Record<string, string \| null>` | Account-field key → the signed-in customer's value. Empty until the fetch lands, and on error.                                                               |
| `locked`  | `Record<string, string \| null>` | Schema-locked property name → resolved value (e.g. `onboarding_project_id`).                                                                                 |
| `resolve` | `(value) => string \| null`      | Substitute one config value. Literals return immediately; `$field` refs read the fetched map — `null` while loading, for unknown keys, and for unset values. |
| `loading` | `boolean`                        | True while a fetch is in flight, including refetches. Only field-dependent values need to wait on it.                                                        |
| `error`   | `Error \| null`                  | The last fetch failure; cleared when a refetch starts.                                                                                                       |
| `refetch` | `() => void`                     | Re-run the fetch.                                                                                                                                            |

`isFieldRef(value)` narrows a config value to `{ $field: string }` — use it to decide whether a value needs to wait on `loading`.

<Warning>
  `resolve` returns `null` for an unknown or unset field, so always supply a fallback. A `$field` value is **not** guaranteed to exist: the account field may be blank for this customer.
</Warning>

## Outside React: `fetchResolvedFields`

The same payload, without the hook:

```ts theme={null}
import { fetchResolvedFields } from '@statisfy/digital-workers-react'

const { field_values, locked_values } = await fetchResolvedFields(
  { baseUrl: 'https://api.statisfy.app', getToken, publishableKey: 'pk_live_…' },
  { signal: controller.signal }
)
```

It resolves against `GET /sdk/v1/config/portal` for the session's customer — see the [API Reference](/sdk/v0/api_reference).

## Editing config

| Surface              | How                                                                                                             |
| -------------------- | --------------------------------------------------------------------------------------------------------------- |
| **Settings UI**      | The portal's Settings screen renders controls from the component schema — the intended path for non-developers. |
| **Editing the file** | Treat `portal.runtime.json` like any other portal file. The server re-formats it canonically on save.           |

Locked properties are pinned platform-wide: they're absent from the file by definition, resolve server-side under a stable schema name, and can't be overridden per portal.

<Warning>
  Unknown or inactive values fail the build — a `worker_id` that doesn't exist, or points at an inactive worker, won't ship. Validate before saving.
</Warning>
