---
name: mindset-function
description: Use when building a function in Mindset (mindset.ai) through the mindset-admin MCP server, the fixed pipeline an agent calls as one tool. Covers choosing a function, an agent or a single operation, writing the steps (operation calls, model calls through an AI model connection, computed values, choices), test runs on real input, publishing past the go-live gate and giving it to an agent.
metadata:
  title: Build a function
  blurb: Builds a function, a fixed set of steps an agent calls as one tool.
  group: Mindset MCP
  order: 2
  docs: build-a-function, versions-and-going-live
  checked: d671041b6
  updated: 2026-10-19
---

# Build a Mindset function

When you finish, a published function does one job that has one right answer, its test parameters have run on real input and read as verified, and an agent holds it. If the work has to wait for a person or happen in a fixed order, use the `mindset-script` 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 first: function, agent or single operation

Ask what happens if it runs twice on the same input.

- **One right answer: a function.** Comparing an invoice to its purchase order, converting a currency, looking up a supplier's payment terms. Left to an agent, you pay a model to work out something that has a correct answer, and get a slightly different result each time.
- **It needs weighing up: leave it to the agent.** Whether a $340 overage is worth querying depends on the contract and the supplier. That belongs in the agent's prompt, not a function.
- **One call to one system: give the agent the operation.** A function is worth building when it's several calls plus the work between them, so the agent makes one call and gets one answer.
- **Most work is both.** The agent judges, and the function does the arithmetic it judges on. Have the function return the value the agent or its script decides on, such as whether there are any differences at all.

If the person's request is judgment, say so and don't build a function.

## Ask the person for

- The job in their own words, and one real input for each path through it, for example "an invoice that matches its purchase order, and one with a price difference". These become the test parameters.
- Which connection holds the data (a pure calculation needs none), and for a model step which **AI model** connection to use. The connection fixes the model, so never ask which model name to write.
- Which agent should hold it, and whether that agent already has an active version.
- If the function writes: where a test write can land safely, such as a test supplier record.

## Step 1: read the language and the parts

1. Call `get_information` with `topic: "dsl-reference"` and read it whole. It's the function language as the validator enforces it: every operator, the result shapes and the refusal codes. There is no separate `authoring:function` topic.
2. Call `find_examples` with `resource_type: "function"` and the intent. Start from the closest example. A result whose `grain` is `counter-example` is a refusal to relay to the person, not a body to copy.
3. List the operations: `list_resources` with `type: "connection_operation"` and `filter: { connection: "<connection id>" }`, then `get_resource` on each one you'll call. Check its input, its `tested` status, and that its `exposures` includes `functions` (**Available to functions** in the Studio). An operation must also be enabled, and a write must have consent.
4. See what an operation really returns before you bind to it: `test_resource` with `type: "connection"` and `input: { operation, input }`. It reads by default and refuses a write.

## Step 2: write the document

A function document has `handle`, `kind: "declarative"`, `input`, `steps` (each an `id` and exactly one operation in `do`) and `return`, plus an optional `output` schema. Each step does one of four things:

| Step | How | Invoice example |
|---|---|---|
| Calls a connection operation | `$op` with `connection`, `operation`, `input` | Fetch the purchase order for the invoice number |
| Asks a model | `$llm` with `prompt`, `connection` (an AI model connection), `schema` | Pull the quantity received out of a free-text delivery note |
| Works out a value | A pure expression: `$filter`, `$map`, `$sum`, `$sub` and the rest | List every line where quantity or price differs |
| Chooses between values | `$cond` | Return "no action" when the list of differences is empty |

The rules agents get wrong most often:

- **An effect is a whole step.** `$op` and `$llm` can't sit inside `$map` or any other expression (`effect_in_expression`). To call once per item, put `over` (a list) and `as` (an item name) on the step. The result is an ordered list of `{ index, status, value, error }`, and a failed item doesn't stop the others. The limit is 1,000 items, run 8 at a time.
- **There are no local variables.** To reuse an intermediate value, make it its own step and bind `steps.<id>.result`.
- **A wrong binding fails silently.** The evaluator never throws: a missing path yields nothing. An `$op` result is the raw response body (no `.body` wrapper). Under `over`, an `$llm` item's `value` is the completion text itself. A run that succeeds and returns nothing usually means a bad path or an input the caller never sent.
- **Declare every input** with an example value or a JSON Schema node. Never put a `$bind` in `input` (`input_schema_required`). Example values become the function's test parameters; with `input` as a JSON Schema, keep them in `testParameters`. A test run doesn't refuse a missing input, even a `required` one, and arithmetic on a missing value can come out as a plausible 0, so check inputs in a step and return a problem instead. A function has no description of its own: the agent sees its name and each input's `description`, so write those for the agent.
- **Declare `output`** when the return uses `$reduce`, `$parseJSON`, an `$llm` without `schema`, or a step's `error` (`output_schema_required`).
- **Every step runs.** You can't skip one on a condition: branch with `$cond`. A failed step stops the run unless it's marked `optional: true`, and then a later step can read `steps.<id>.error`.
- **Retries are off by default.** `retry` is an object, `{ "maxAttempts": 3, "retryOn": ["429", "5xx", "timeout", "network"] }`, and `maxAttempts` counts every attempt, up to 10.
- **No credentials, no raw HTTP.** Every outside call is an `$op` on a connection, even to a public host. A literal secret in a header is refused (`secret_literal`).
- **A function can't call another function** (`composition_unsupported`).
- **A write's destination comes from an input**, never a literal in the step, so the test inputs decide where a test write lands.

Before you save, call `render_function_doc` with the `doc`. It describes what each step does and what happens when it fails. Fix anything that surprises you.

## Step 3: create it and change it

- `create_resource` with `type: "function"` and `input: { name, doc, idempotencyKey }`. Always send `idempotencyKey` (any string of 8 or more characters for this one intent). If the call fails, retry with the **same** key, because the first may have committed. A key means one intent: re-sending it with different content returns the original function and drops your changes.
- Creating makes version 1, a draft. Nothing can call it yet.
- To change it, `get_resource` first for its `versionToken` and `acceptedEditOps`, then `edit_resource` with `ops` (`add_step`, `set_step`, `move_step`, `remove_step`, `set_return`, `set_input`, `path_set`, `path_remove`, `full_replace`). Each edit adds a draft version. A `path_set` on an object patches it: delete a key with `path_remove`.
- A save that breaks a rule is refused with a code and a message naming the step or field. Fix that and save again.

## Step 4: test it on real input

1. Run it: `test_resource` with `type: "function"`, `ref` (its id or handle) and `input`. It runs the newest version, draft or published, and returns a per-step trace and a `runId`. For the full record, call `get_run` with that `runId`. While you iterate on a function that writes, `dryRun: true` runs the reads for real and skips each write.
2. Keep a test parameter set (resource type `flow_test_parameters`) with one scenario per path. Read `get_information` with `topic: "authoring:flow_test_parameters"` for the shape. Include one awkward case. The first set, `contract example`, is made for you from the function's example input.
3. Run each set with `test_resource` and `parameterSet`. `verified` is true only when every step was exercised. `notExercised` names each step that wasn't, and why.
4. **A test that writes needs a go-ahead.** The first call is refused with `write_go_ahead_required`, a write list and its `listHash`. Tell the person what the run will read and what it will write, and where. When they say go, call again with `goAhead: { listHash, instruction }` beside `input`, quoting their words. Never invent a go-ahead. The writes are real.

## Step 5: publish it

- `publish_resource` with `type: "function"` and the id. It validates the document and refuses one that won't run. Once published, that version is the only active one.
- **The go-live gate.** Publishing is refused with `operations_untested` while a step calls an operation with no successful call on record. A test run that reaches the operation fixes it. `overrideUntested` publishes anyway, and is recorded under the person's name: pass it only when the person chooses it by name ("go live untested"), never on a general "go ahead". A step that calls an operation still waiting for a person's approval is refused with `operation_awaiting_approval`. No override covers that: a person approves it in the Studio, then a test run makes it tested.
- **Publishing reaches every agent that holds the function on its next call.** Nothing on the agents is activated. Before publishing a new version of a function in use, find out which agents hold it (in the Studio: **Govern → Monitor → Topology**).
- To roll back: `list_resource_versions`, then `activate_resource_version` with the version number. It's gated the same way. `unpublish_resource` takes the function offline.

## Step 6: give it to an agent

1. `agent_config_admin` with verb `set_assignments`, `assignments: { functions: [<every function the agent should hold>] }` and `activation`. A type you name is replaced whole, so include the functions it already has.
2. Choose `activation` by one question. If the agent already has an active version, it's in service: pass `"stage"`, then tell the person the version number and that they activate it on the agent's **Versions & Availability** tab. If no version has ever been active, pass `"activate"`.
3. Call `agent_config_admin` with verb `assignment_health`. `unusable` names a function the agent won't get (an unpublished one, for example), and `configNotLive` says the change is staged but not live.
4. If the agent's script calls the function, the phase that calls it must also list it. See the `mindset-script` skill.

## Rules

- Never put a credential in a function. Credentials live on connections.
- Never pass `overrideUntested` or a write go-ahead the person didn't give in their own words.
- Never activate a change on an agent that's already live on your own initiative. Stage it and say so.
- Deleting a function can't be undone. Confirm with the person first, or `unpublish_resource` instead.

## Check it works

- `render_function_doc` describes what the person asked for.
- Every test parameter set reads `verified: true`, including the awkward case.
- `get_resource` shows the version you published is the active one.
- `assignment_health` reports nothing `unusable` and no `configNotLive` once the version is active.
- The person asks the agent something that needs the function, on its **Chat** tab, and the run record shows the call.

## When it fails

| What you see | Cause and fix |
|---|---|
| A step returns nothing and everything after it is empty | A bound path doesn't match the real shape. Read the trace in `get_run`, or test the operation on its own, and fix the path |
| `status: "ok"` with an empty result | The caller didn't send an input the steps bind. Check every `input.<name>` arrived |
| `effect_in_expression` | An `$op` or `$llm` is nested in an expression. Make it its own step, with `over` for a list |
| `input_schema_required` or `output_schema_required` | An input or the output can't be typed. Declare it |
| `output_mismatch` | The return doesn't produce what `output` declares. `context.differences` names each path |
| A step calling an operation fails | The operation isn't enabled, isn't offered to functions, or a write lacks consent. Check it on the connection |
| `write_go_ahead_required` | Expected for a test that writes. Get the person's go-ahead, then call again with `goAhead` |
| `operations_untested` on publish | Run the test parameters until each named operation has a successful call |
| `operation_awaiting_approval` on publish | A step calls a write still waiting for approval. A person approves it in the Studio, then run the test parameters |
| It runs on its own but the agent never calls it | Not published, not assigned, or the agent version holding it isn't active |
| It runs out of time on a big list | Use `over` on one step instead of the agent calling the function many times, or give each run fewer items |

## 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/build-a-function.md: read for the running invoice example and what each Studio tab shows the person.
- https://docs.mindset.ai/docs/versions-and-going-live.md: read for test parameters, write go-aheads, tested status and what makes it lapse.
- https://docs.mindset.ai/docs/versions-and-going-live.md: read when you need to know when a change reaches people and how to roll it back.
- https://docs.mindset.ai/docs/runs.md: read to size a function against its time and call limits.
- https://docs.mindset.ai/docs/approve-what-an-agent-can-do.md: read when an operation the function writes through is waiting for consent.
