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

# Prompt Variables

> Fill the placeholders an agent's prompt blocks declare, and discover which ones exist.

| Function                            | Description                                                                            |
| ----------------------------------- | -------------------------------------------------------------------------------------- |
| `getSidenetVariables()`             | Get the `{{placeholders}}` this copilot's agents declare, as a fillable object (async) |
| `flattenSidenetVariables(declared)` | Turn that object into one `{ path, default }` entry per placeholder (sync)             |

It tells you what you may put in `variables` without reading anyone's prompt or calling `GET /v1/copilots/{id}` yourself.

## Context vs. variables

Both feed data from your app into the agent's system prompt, but they arrive there in completely different ways — and they are not interchangeable.

|                              | `context`                                                                   | `variables`                                                              |
| ---------------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| Shape                        | `string[]`                                                                  | `Record<string, string \| number \| boolean \| null \| array \| object>` |
| Where it lands               | Appended verbatim as a **Session context** section at the top of the prompt | **Substituted into `{{placeholders}}`** inside the agent's prompt blocks |
| Prompt has to know about it? | No — works with any agent                                                   | Yes — the block must contain `{{userName}}`, or the value is never read  |
| Typical use                  | Ambient page/app state: what the user is looking at right now               | Identity and account facts the prompt is written around                  |

```typescript theme={null}
initSidenet({
  copilotId: 'copilot-456',
  auth: { ...tokens },

  // Injected as-is, no prompt authoring needed
  context: [
    'User is viewing invoice #4021 (status: overdue, amount: €2,340).',
    'Selected rows: 3, 7, 12.',
  ],

  // Substituted into the prompt's {{placeholders}}
  variables: {
    userName: 'Maya',
    plan: 'pro',
    seats: 12,
    user: { language: 'FR', country: 'MX' },  // fills {{user.language}}, {{user.country}}
  },
});
```

With a prompt block that reads:

```
You are Acme support. The customer is on the {{plan || 'free'}} plan with {{seats || 0}} seats.
Address them as "{{userName || 'there'}}" and reply in {{user.language || 'EN'}}.
```

the agent receives `…on the pro plan with 12 seats. Address them as "Maya" and reply in FR.` — the placeholders are gone by the time the model sees the prompt. `context`, by contrast, is never templated: whatever strings you pass show up as their own section, unchanged.

## Nested objects and dot paths

A prompt block can read a dot path — `{{user.language}}`, `{{account.plan.tier}}` — and you can fill it **either way**. Pass the whole object as it already exists in your app, or a dotted key; the API expands a dotted key into the nested form, so both spellings land in the same place:

```javascript theme={null}
// Nested — usually what your app already has
updateSidenetConfig({
  variables: {
    userName: 'Maya',
    user: { language: 'FR', country: 'MX', tier: 'pro' },
    order: { id: 'A-4021', status: 'shipped' },
  },
});

// Dotted keys — same result
updateSidenetConfig({
  variables: {
    userName: 'Maya',
    'user.language': 'FR',
    'user.country': 'MX',
  },
});

// Mixing is fine — these merge into one `user` namespace
updateSidenetConfig({
  variables: { user: { language: 'FR' }, 'user.country': 'MX' },
});
```

You can hand the whole object straight through — no flattening step:

```javascript theme={null}
// currentUser = { id: 'u_1', language: 'FR', plan: { tier: 'pro', seats: 12 } }
updateSidenetConfig({ variables: { user: currentUser } });
// fills {{user.language}}, {{user.plan.tier}}, {{user.plan.seats}}
```

<Warning>
  The one case to avoid is a genuine conflict — the same path given as both a scalar and a namespace (`{ user: 'Maya', 'user.language': 'FR' }`). `user` can't be a string *and* an object, so the first one in the payload wins and the other is dropped.
</Warning>

### Rules for `variables` (enforced by the API)

* Every dot-separated segment must be a plain identifier — letters, digits, underscores, starting with a letter or underscore.
* Values are a string, number, boolean, `null`, an array of those, or an object of the same.
* Paths go up to **3 segments** deep — `{{a.b.c}}` yes, `{{a.b.c.d}}` no — however you spell them.
* An array is kept or dropped whole and renders as JSON if interpolated directly; its real use is `contains` / `in` display conditions.
* Up to 64 values (each leaf counts, however deeply nested), 2 KB per value, 16 KB total. A `null` leaf is "no value" — it's not stored and doesn't count, so an unfilled leaf costs you nothing.
* Anything breaking those rules is **dropped, not rejected** — a display-only field must never fail a chat turn. So a typo'd key fails silently; check what your agent actually renders.
* A placeholder with no value and no default is left visible (`{{userName}}`) rather than blanked, which makes a missing value obvious in the answer.
* The `now.*` namespace (`{{now.datetime}}`, `{{now.date}}`, `{{now.iso}}`, `{{now.timezone}}`) is filled in server-side on every turn and **cannot be overridden** — a key whose first segment is `now` is dropped either way it's spelled, so don't send your own clock.

Variables also drive **display conditions**: a prompt block can carry rules (e.g. `plan in ['pro','enterprise']`, `user.language exists`) that decide whether the block is included at all, evaluated against the same values.

## Discovering which variables to send

You don't have to read the prompt to know what an agent expects — `getSidenetVariables()` returns the `{{placeholders}}` the copilot's agents actually declare, **in the same nested shape you send back**:

```typescript theme={null}
import { getSidenetVariables, flattenSidenetVariables, updateSidenetConfig } from 'sidenetai-sdk';

const declared = await getSidenetVariables();
// {
//   instance: { currency: 'EUR', store_count: null, today_local: null },
//   userName: null,
// }
```

Each leaf is what that path renders when you omit it (the block's `{{plan || 'free'}}` → `'free'`), or `null` when it has none — in which case a literal `{{plan}}` ends up in the prompt. **The nulls are exactly what you have to fill in.** Keys come back alphabetical at every level.

Because it's the same shape, the object round-trips: fill in the leaves you have and send the whole thing back, no reshaping and no need to strip what you couldn't fill.

```javascript theme={null}
updateSidenetConfig({
  variables: {
    ...declared,
    instance: { ...declared.instance, store_count: 42 },
  },
});
```

To list the paths instead of walking the tree, use `flattenSidenetVariables()`:

```typescript theme={null}
flattenSidenetVariables(declared);
// [
//   { path: 'instance.currency',    default: 'EUR' },
//   { path: 'instance.store_count', default: null  },
//   { path: 'instance.today_local', default: null  },
//   { path: 'userName',             default: null  },
// ]

// The ones the agent renders as a literal {{name}} if you skip them
const required = flattenSidenetVariables(declared)
  .filter((v) => v.default === null)
  .map((v) => v.path);
```

A `path` is itself a valid key to send (`{ 'instance.currency': 'EUR' }`), per the dot-path section above.

### Return types

`getSidenetVariables()` returns `SidenetDeclaredVariables` — an object whose leaves are `string | null` and whose branches are more of the same:

| Leaf     | Meaning                                                                                       |
| -------- | --------------------------------------------------------------------------------------------- |
| `string` | The default this path renders when you omit it (from the block's `{{name \|\| 'fallback'}}`). |
| `null`   | No default — a literal `{{name}}` is left in the prompt. **These are the ones to fill in.**   |

`flattenSidenetVariables(declared)` returns `SidenetVariablePath[]`:

| Field     | Type             | Description                                                                                                   |
| --------- | ---------------- | ------------------------------------------------------------------------------------------------------------- |
| `path`    | `string`         | The placeholder path — `userName`, or a dot path like `instance.currency`. Valid as a key to send back as-is. |
| `default` | `string \| null` | The leaf's value, as above.                                                                                   |

Notes:

* Served from the object cached during `initSidenet()`, so it's free after boot. It fetches once only if that init fetch failed. Call `refreshSidenetConfig()` to pick up prompt edits made since.
* Keys are alphabetical at every level.
* Throws if the SDK isn't initialized, or if you initialized with only an `agentId` / `agentVersionId` — with no `copilotId` there's no network to enumerate.
* An empty object is a real answer: the agents use no placeholders, or they still run the legacy single prompt string, which isn't templated.
* Sending a `null` leaf back is a no-op — not stored, not counted against the 64-value limit, not reported as dropped. A namespace whose leaves are all `null` is dropped whole, so a wholesale `{{instance}}` stays visible rather than rendering `{}`.
* A path that a prompt reads both wholesale (`{{user}}`) and by sub-path (`{{user.name}}`) comes back only as the namespace — that combination isn't fillable both ways, since `{ user: 'x', 'user.name': 'y' }` is rejected as a conflict.
* Server builtins (`now.*`) are excluded — you can't supply those.

<Warning>
  **Breaking change.** `getSidenetVariables()` previously returned `SidenetVariable[]` — `[{ name, default, usedBy }]`. It now returns the nested object above, and `usedBy` is gone from the API. Code that read `variables[].name` should switch to `flattenSidenetVariables(declared)` and read `.path`. The `SidenetVariable` type is no longer exported.
</Warning>

## Both are updatable at runtime

Neither `context` nor `variables` is init-only. `updateSidenetConfig()` applies both to the **next** send — a response already streaming keeps the values its turn started with, and the conversation is not reset:

```javascript theme={null}
// Page navigation → new ambient context
updateSidenetConfig({ context: ['User is viewing invoice #4022 (status: paid).'] });

// Plan upgraded mid-session → new template values
updateSidenetConfig({ variables: { userName: 'Maya', plan: 'enterprise', seats: 40 } });
```

Each call **replaces** the whole value rather than merging into it — pass the full set you want the next send to use, and `{}` (or `[]`) to clear.

<Warning>
  `variables` are **display-only and untrusted**. They're rendered into the system prompt and read by nothing else — no authorization, identity, billing, or routing decision uses them (identity comes from your session token). A value that originated with an end user lands verbatim in a system message, so have the prompt block delimit it — `User's stated name: "{{userName}}"` rather than dropping it mid-sentence where it reads as an instruction.
</Warning>
