---
name: mindset-script
description: Use when adding a script to a Mindset (mindset.ai) agent through the mindset-admin MCP server, for the part of its work that must wait or happen in order. Covers deciding what is a phase, gates (checks and model judgments), per-phase resources, waiting for a person (Slack approvals with $ask, human-in-the-loop widget questions), wait limits, test parameters, write go-aheads and activating the agent version.
metadata:
  title: Add a script to an agent
  blurb: Adds a script to an agent, for work that must wait or happen in order.
  group: Mindset MCP
  order: 3
  docs: write-a-script, runs, approve-what-an-agent-can-do, versions-and-going-live
  checked: d671041b6
  updated: 2026-10-19
---

# Add a script to a Mindset agent

When you finish, an agent has a script for the part of its work that must wait or happen in order. Each phase is checked before the agent moves on, any wait for a person has a time limit, the script's test parameters have run, and an agent version carrying it is active or staged for the person to activate. For a fixed job with one right answer, use the `mindset-function` skill instead.

## Mindset in brief

Mindset is a hosted platform for building, running and governing AI agents. An **agent** is configuration, not code: a prompt, a model, and the resources it may use. Those resources are **scripts** (steps that must wait or happen in order), **functions** (fixed steps with one right answer), **widgets** and **connections** (the only way data enters or leaves). People manage all of it in the Studio, a web app, or through Mindset's MCP servers.

## Where you are building: host, org, environment

Every Mindset call names three things. Get all three from the person before you write code or call a tool. Never guess or invent them.

| Value | What it is | Where the person finds it |
|---|---|---|
| **Host** | The Mindset deployment their org lives on, for example `https://eu.mindset.ai` | The address bar when they're signed in to the Studio, or an agent's **Embed** tab |
| **Org slug** | Their company's account. The org is the isolation boundary: members, model keys, settings and audit sit here, and one org can never see into another | An agent's **Embed** tab, or the address under **Settings → Systems → Connect an AI client** |
| **Environment slug** | A named partition inside the org, such as Sandbox, Test or Production | An agent's **Embed** tab, or the same address. The **Environment** switcher in the Studio shows which one is current |

How environments behave:

- **Agents, scripts, functions, connections, triggers, schedules, conversations, runs and API keys each belong to exactly one environment** and can't see anything in another.
- **Every org starts with one environment, Sandbox, whose slug is `original`.** A slug never changes, even when the environment is renamed. Use the slug, never the display name.
- **Nothing copies between environments.** To promote a change from Test to Production, make the same change again in Production.
- **Don't create an environment to hold your work.** Environments can't be deleted, and they're for stages such as Test and Production, not for projects. Work in the environment the person names. Create one only if the person asks you to.
- **Members, model keys and org settings are shared by every environment.** Changing an org setting changes it everywhere.

## Credentials for building in Mindset: sign in, never a key

You build inside Mindset through the **`mindset-admin` MCP server**. The person signs in with their own Mindset account in a browser, and they must be an org admin. There is no key or token, so never ask the person for one.

- **The server address names one environment**: `<host>/api/v1/orgs/<org-slug>/envs/<environment-slug>/mcp`. Every call goes to that environment unless the tool call passes `envSlug` to point that one call at another environment. Org-level tools refuse an `envSlug`.
- **No tool takes a secret value.** When a connection needs a credential, call `request_capture`. It returns a reference and a capture URL. Give the person the URL to open and paste the value into themselves, then use the reference. Capture links expire after 15 minutes. For a system that signs in with OAuth, `authorize_connection` returns a sign-in link the person opens the same way, and it expires after 10 minutes. If a link has expired, request a new one. Never ask the person to paste a credential into the conversation, even if they offer.
- **Errors come back structured.** Read `code` and `context` and act on them. Don't retry blind.

## Before you start

This skill works through the `mindset-admin` MCP server. Check that its tools (for example `get_information`) are available to you. If they aren't, set the server up first, following the `mindset-mcp` skill or https://docs.mindset.ai/docs/work-from-claude-code.md, then come back here.

Call `get_information` with no arguments for the platform's own topic index. Its guidance is current for the platform you're connected to. Where it and this skill disagree, follow `get_information`.

## Decide whether it needs a script

Most agents need none. The prompt carries what the agent is and how it behaves. Add a script only for an instruction the agent must not be able to skip:

- It must wait for an answer from a person.
- It must not move on until something is true.
- It must not reach a tool before an earlier step is done.

A **phase boundary** is a place where the run must wait. "Collect the supplier's name, the invoice number and the amount" is one phase with three values to collect. "Confirm the total before you continue" is a boundary. A phase that only says, shows or looks something up and carries straight on isn't a phase: fold it into the one before, and use a step's `when` for something that appears only sometimes.

The script can't loop. "Do this for each item" with a call per item is a function. Counting or filtering a list is an expression in a gate. Ten items that each need judgment belong in one phase that judges all ten, not ten phases.

If the whole request is behavior with nothing to wait for, say so and change the prompt instead.

## Ask the person for

- Which agent, and whether it already has an active version. If there's no agent yet, create it first with the `mindset-build-agent` skill.
- The phases, written as outcomes: not "search the contracts" but "we know whether the contract allows this difference, and which clause says so".
- What each phase may use: which functions, operations and other agents.
- For each wait on a person: who answers, where (a Slack channel, or a link), how long it can wait, and where the run goes if nobody answers.
- One test input for each path through the script, and a safe place for test writes to land.

## Step 1: read the platform's guidance

1. Call `get_information` with `topic: "authoring:script"`, then its parts `authoring:script:gates`, `authoring:script:steps`, `authoring:script:resources` and `authoring:script:refusals`. Together they're the script language as the validator enforces it. Gates use the same expression operators as functions, listed in `dsl-reference`.
2. Call `find_examples` with `resource_type: "script"` and the intent, and start from the closest one.
3. Find what the script will call: `list_resources` with `type: "function"`, `type: "connection"` (for each connection's `id`) and `type: "connection_operation"`.
4. Assign every function, widget and operation the script uses to the agent first (`agent_config_admin`, verb `set_assignments`). A script narrows what its agent holds. It never grants anything. A `$call` step calls with the agent's grants, so every operation it calls must be offered to agents (`exposures` includes `agents`) and held by the agent, as the operation or its whole connection. To keep the model off an operation only a step calls, declare `resources` on each phase.

## Step 2: write the phases

A script document has `handle`, `kind: "script"`, optional `params` and `returns`, and `phases` in order. A phase has an `id`, `name`, `description`, `onEnter` steps, a `prompt`, `resources`, `vars` (values the model collects), `writes`, a gate (`advanceWhen`, or `advance` routes), an optional `wait`, and on the last phase `terminal: true` with `onComplete`.

The invoice exceptions script:

| Phase | Collects | Can use | Advances |
|---|---|---|---|
| Gather | The invoice, its purchase order, the comparison | Finance reads, the comparison function | A check: all three collected. No differences: skip to Close |
| Assess | Whether the contract allows it, and which clause | The contracts knowledge base | A judgment: the answer names a clause |
| Prepare | A supplier query, registered | `register_supplier_query` | A check: the query is registered |
| Close | Nothing | Nothing | Ends |

The rules agents get wrong most often:

- **Routes only go forward.** Each route targets a later phase (`backward_route`), the first route whose `when` holds wins, and a phase has `advanceWhen` or `advance`, never both. Every phase but the last needs a gate (`missing_route`). To "go back", add a later phase that repeats the work.
- **Use a check wherever the answer is yes or no.** A check reads collected data, costs nothing and can't be argued past. Use `$askLLM` (a judgment) only for quality you'd have to read yourself. It's strict, it costs a model call on every turn the phase is active, and inside an `$and` the checks should come first. A gate can't call anything: put the `$call` in `onEnter` and have the gate read `steps.<id>.result`.
- **`resources` replaces, it doesn't add.** Leave it out and the phase offers everything the agent holds. Declare it, even as `{}`, and the phase offers only what it lists. `tools` lists operations the model may call. `connections` only authorizes the phase's own `$call` steps and gives the model nothing.
- **The phase that asks a question also handles the reply.** A phase advances only after a turn ends, so the person's answer arrives in the same phase. If answering needs a lookup, that phase needs the tool.
- **`onEnter` runs before the model.** An entry step can't read a value the same phase's model collects (`entry_reads_own_phase_var`). The model working something out and the script calling with it are two phases.
- **Name a function by its handle and a connection by its `id`**, never its name, in both `$call` and the phase's `resources`. The phase that calls must declare the target (`invalid_call_target`). `$op` isn't a script step: use `$call`.
- **To force a function**, list only that function on the phase, call it with `$call` in `onEnter` or gate on its result. A prompt saying "always call it" can be skipped.
- **Keep writes late.** Only the phase that writes should hold the write, and the write's destination should come from a script param, not a literal, so test inputs decide where test writes land.
- **Give every `$call` a `timeout`**, plus `retry: { max }` for blips and `onError: { to }` (a later phase) for outages. Entry steps share one time budget, and running out is `entry_budget_exhausted`.
- **Every key in `returns` is required** unless marked `optional: true`. A run that never fills one ends with `returns_unsatisfied` instead of completing.
- **Write a `description` for every phase and a `heading` for every step**. The person reads them in the Studio.
- **An ending says only what the run did.** If the person's wording promises an effect no step performs ("your refund has been approved"), tell them, and ask for the step that would make it true.

Before you save, call `render_script_doc` with the `doc` and read every phase, gate and route.

## Step 3: waiting for a person

Nobody answering is the normal case, so every phase that waits declares `wait: { "seconds": N, "to": "<later phase id>" }`. The floor is 300 seconds, because waits expire on a 5 minute sweep. Without a `wait` the run waits indefinitely, and a phase that asks a person by link must declare one (`link_wait_required`). A waiting run is parked: it spends nothing, resumes with a fresh turn budget, and is closed as abandoned after 31 days.

Design the decision: put it late, ask once for the whole batch, and put everything needed to decide in the question.

**Slack approvals (`$ask`).** An `onEnter` step that posts one Slack message with a row of buttons per item. Read `get_information` with `topic: "slack-approvals-authoring"` before you write one, and `slack-approvals-setup` if the Slack connection doesn't exist yet. What goes wrong:

- `connection` is the Slack connection's `id`, and the same id must be in the phase's `resources.connections`.
- The phase must declare `writes: { "item:*": { "from": "host" } }`, or every click is refused.
- Each item needs `id` and `text`, and the field must be called `text`. Any other name posts raw JSON with no error.
- `options` (1 to 10) are shared by every item. "Pick one of three" is one item with three options. "Approve each of twelve" is twelve items with two options. A `choice` is a token with no spaces, and the `label` is the words.
- Guard the step with `"when": { "$gt": [{ "$count": { "$bind": "data.exceptions" } }, 0] }`. An empty list fails the phase, while a skipped ask passes straight through.
- Gate on `{ "$eq": [{ "$count": { "$bind": "decisions.remaining" } }, 0] }`. The `decisions` lists exist only in that phase's gates. A later phase reads each answer at `data["item:<id>"]`.
- `answerers` takes Slack member IDs (`U…`). Leave it out for anyone in the channel, and never send `[]`.
- Add an option for someone who can't decide, such as "Needs someone else", and route it.

**A widget question (`$widget`).** It puts a widget in front of a person, in the chat or by a link, and branches on the widget's declared `outputs`. It's switched on per deployment: use it only if `get_information` with no arguments lists `human-in-the-loop-blocks`, and read that topic first. The widget must declare `outputs`, be in the phase's `resources.widgets` and be assigned to the agent. Offer a "can't decide" choice, and never collect a decision as a `vars` value the model records from the chat. Whoever holds a link can answer it, so send a link only to the person meant to answer.

## Step 4: create, bind and test

1. `create_resource` with `type: "script"` and `input: { name, handle, doc }`. A script takes no `idempotencyKey`. Give it an explicit `handle`, so a retried create is refused as `handle_taken` instead of making two.
2. To change it, `get_resource` first for its `versionToken`, then `edit_resource` with `path_set`, `path_remove` or `full_replace`. A `path_set` on an object patches it: delete a key with `path_remove`.
3. **Bind it with `update_agent`**, passing the agent's `handle`, `scriptId` and `activation: "stage"`. That save validates the script and refuses a broken one, naming the rule. `publish_resource` refuses a script: which text runs is pinned by the agent's version. Binding is also refused while a step calls an operation waiting for a person's approval (`operation_awaiting_approval`). Saving such a script only warns, unless a live agent already runs it. Once a person approves the operation, bind again. No new script version is needed. Until the version is active, `get_agent` shows `scriptId: null` and the script shows no bound agent: read the staged version with `agent_config_versions` verb `read`.
4. Store test parameters as `create_resource` with `type: "flow_test_parameters"` and `input.script` naming the script. Read `get_information` with `topic: "authoring:flow_test_parameters"` for the shape. Keep one scenario per path, such as an invoice that matches and one with a difference the contract doesn't allow. A scenario's `record` steps stand in for what the model collects, so also rehearse a real customer message with `test_resource` and `input.task`.
5. Run each set: `test_resource` with `type: "script"`, `ref`, `parameterSet`, and `input.configVersionId` naming the staged version. It starts a real conversation. `verified` is true only when every step was exercised. **Branch not taken** means you need another case for that branch. When the run asks a person by link, the link is live in the test run, even on an agent never activated. When the person answers, the run carries on by itself on the version under test, still under the go-ahead given at the start, and the operations it reaches become tested.
6. A test that writes is first refused with `write_go_ahead_required`, a write list and a `listHash`. Tell the person what it will read and write, and where. When they say go, call again with `goAhead: { listHash, instruction }`, quoting them. `input.posture: "read-only"` refuses every write instead.
7. Read a run back with `get_run`. For a script it returns the phase reached, the collected data, each step's result and any decisions.

## Step 5: make it live

- **An agent with an active version is in service.** Leave your change staged. Tell the person the version number and that they activate it on the agent's **Versions & Availability** tab. On an agent that has never had an active version, activate the tested version yourself with `agent_config_versions` verb `activate`.
- **The go-live gate.** Activating a version whose script calls an operation with no successful call on record is refused with `operations_untested`. Run the test parameters. `overrideUntested` is recorded under the person's name: pass it only when they choose it by name ("go live untested").
- A conversation in progress moves to the new version on its next turn. A script run already under way finishes on the script version it started with.
- To stop the agent running its script, `update_agent` with `scriptId: null` and `activation`. The script and its versions are kept. A script a live agent runs can't be deleted (`has_dependents`). Detach it and activate that version first.

## Limits that shape a script

- A phase moves on only when a turn ends. A run started by a schedule or webhook keeps taking turns while it advances. In a chat, the person's next message is the next turn.
- A turn stops at about 99 model rounds, 240 seconds (streamed or delegated) or 400,000 tokens. A turn that stops at a limit stays in its phase even if the gate was met: the outcome is recorded as `bound`, not `held`. A phase's `loopBudget` holds the phase when it runs out.
- A scheduled firing gets 15 minutes and 12 turns in all.
- A run that stops part way may already have written. Put what you can't undo last, and make steps safe to repeat.

## Rules

- Never pass a write go-ahead or `overrideUntested` the person didn't give in their own words.
- Never activate a change on an agent that's already live on your own initiative.
- Agents can't approve a write operation. One waiting for consent stays refused until an admin approves it in the Studio.
- Never ask for a Slack token or signing secret in the conversation. The Slack connection takes them through `request_capture`.

## Check it works

- `render_script_doc` shows every phase, gate and route as the person described them.
- Every test parameter set reads `verified: true`, and together they take every branch.
- `agent_config_admin` with verb `assignment_health` returns an empty `scriptResources.phases`, so no phase declares a resource the agent doesn't hold. It reads the active version only: a `scriptHandle` of null means nothing was checked.
- On the agent's **Chat** tab, a conversation stops at each phase it should and advances when the gate holds.
- For a wait on a person: someone answered a real question, and a run where nobody answered took the `wait` route.

## When it fails

| What you see | Cause and fix |
|---|---|
| The agent says it has nothing to call | The phase's `resources` leaves the tool out, or lists the connection without the operation in `tools`. Declare it on the phase where the reply arrives |
| `invalid_call_target` | A `$call` names something its phase doesn't declare, or a connection by name. Use the id, and declare it on the same phase |
| `backward_route`, `missing_route` or `route_conflict` | Routes must go forward, every non-terminal phase needs one gate, and never both forms |
| A phase never advances | A gate that can't hold: `$in` against an object instead of a list, or a value nobody collects. Read the gate in `render_script_doc` |
| Slack buttons do nothing, or the click is refused | The phase lacks `writes: { "item:*": { "from": "host" } }`, or the Slack app's interactivity isn't set up |
| Slack posts raw JSON | An item's text field isn't called `text` |
| The phase fails on entry when there's nothing to decide | The `$ask` step has no `when` guard for an empty list |
| `entry_budget_exhausted` | The phase does too much before it hands back. Move work to a later phase or tighten timeouts |
| `operations_untested` on activation | Run the test parameters until each named operation has a successful call |
| `resource_not_granted` naming an operation a step calls | It isn't offered to agents, or the agent doesn't hold it. Offer it to agents, and assign it or its connection |
| `link_wait_required` | A phase asks a person by link with no `wait`. Give it `wait: { seconds, to }` |
| `operation_awaiting_approval` on binding or activation | A step calls a write still waiting for approval. A person approves it in the Studio, then bind or activate again |
| `write_not_agreed` in a test or on the Chat tab | Writes on a version that isn't active need a go-ahead for its write list |

## Read more

Fetch these as Markdown when you need more than this skill gives you. If one disagrees with this skill, the page is newer: follow it.

- https://docs.mindset.ai/docs/write-a-script.md: read for the full invoice exceptions script and forcing a function.
- https://docs.mindset.ai/docs/runs.md: read to size a run, and for what happens when it waits, stops or resumes.
- https://docs.mindset.ai/docs/approve-what-an-agent-can-do.md: read for write consent and setting up Slack approvals.
- https://docs.mindset.ai/docs/versions-and-going-live.md: read for test parameters, write go-aheads and the go-live gate.
- https://docs.mindset.ai/docs/change-something-thats-already-live.md: read for staging, comparing and activating agent versions.
