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

# Goals & Goal Checks

> Track whether your agent's goals are actually met — with automatic per-turn evaluation and owner-written verification code that gates goal completion.

<Info>
  Goals are defined on the [Purpose page](/sidebar-menu/purpose#3-success-criteria) (Success
  Criteria and per-stage criteria). This page covers how goal *completion* is verified:
  the per-turn **Auto-check** evaluator and per-goal **Goal Checks** (owner code).
</Info>

## Why verification exists

Without verification, goal completion is self-reported: the agent decides it is done and
calls the `mark_goal_complete` tool with a value for every goal. The platform validates
that required values are *present* — but not that they are *true*. An agent could claim
`appointment_booked: true` without any appointment existing.

Two optional layers close that gap:

| Layer                                           | What it does                                                                                                                                                                  |
| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Auto-check** (`auto_eval` toggle, Goals card) | After every reply, assess all active goals, persist per-subscriber met / partial / unmet state, and show the agent its verified progress next turn.                           |
| **Goal Check** (`evaluator_code` per goal)      | Your own JS/Python, run in a secure sandbox, decides whether the goal is met — against real data, not the model's say-so. An `unmet` verdict **blocks** `mark_goal_complete`. |

The two compose: with Auto-check on, conversational goals are assessed by a small LLM
pass while code-backed goals are verified by your Goal Check — and the code's verdict
always wins.

## Where to edit Goal Checks

The same code is editable in **two places** (it is stored once, on the goal itself in
the `purpose` section):

1. **Purpose page** — each goal card has an **Add Code Check** button that opens an
   inline editor with a **Test** button.
2. **Events page** — under the **Goals & Handoff** channel, each goal with code appears
   as a **Goal Check** row; create new ones by picking *Channel → Goal → Event*.
   Unlike ordinary [event actions](/sidebar-menu/events), a Goal Check is **not**
   fire-and-forget: its return value is the verdict.

## The handler contract

Write the **body** of `handle(goal, ctx)` (JavaScript or Python — a pasted full
`handle` definition also works) and **return a verdict**:

```ts theme={null}
{ status: 'met' | 'partial' | 'unmet', value?: string | number | boolean, note?: string }
```

* `status` — the verdict. `partial` means real progress that isn't complete yet.
* `value` — the confirmed value (persisted, shown to the agent, fed back as
  `goal.candidate_value` next time).
* `note` — one short line of evidence, or what is still missing. Shown to the agent
  and in rejection messages, so make it actionable.

Returning a bare `true`/`false` or a bare status string also works. Anything
unrecognizable fails closed to `unmet`.

### When your code runs

| Moment                                            | Trigger                                        | On error                                                                                         |
| ------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| **After each reply** (end-pass)                   | Auto-check on, subscriber conversations        | Fails open — the state update is skipped; a sandbox outage can never flip a previously-met goal. |
| **At `mark_goal_complete`** (hard gate)           | The agent tries to complete a code-backed goal | Fails closed — unless the last persisted code verdict says `met`, the call is rejected.          |
| **Test button** / `POST /api/test-goal-evaluator` | You, from the dashboard                        | Error returned to you directly.                                                                  |

Goals already verified `met` by code are skipped on subsequent end-passes (the gate
always re-checks live). Sandbox time is billed per second; the Auto-check LLM pass is
billed to the owner like other auxiliary calls.

## The `goal` object

Your handler's first argument:

| Field                  | Type             | Meaning                                                                                                                                                                 |
| ---------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `goal.key`             | `string`         | The goal's key (its `mark_goal_complete` argument name).                                                                                                                |
| `goal.label`           | `string`         | Human-friendly name.                                                                                                                                                    |
| `goal.description`     | `string`         | The goal's description.                                                                                                                                                 |
| `goal.type`            | `string`         | `text` \| `enum` \| `number` \| `boolean`.                                                                                                                              |
| `goal.required`        | `boolean`        | Whether the goal is required.                                                                                                                                           |
| `goal.candidate_value` | `unknown`        | The value being claimed — the agent's `mark_goal_complete` input at the gate, or the extracted/last-known value on the end-pass. May be absent early in a conversation. |
| `goal.stage_context`   | `object`         | Cross-stage scalars collected via `mark_goal_complete` on earlier stages ([Stages](/sidebar-menu/purpose#stages)).                                                      |
| `goal.prior`           | `object \| null` | This goal's previously persisted verdict: `{ status, value?, note? }`.                                                                                                  |
| `goal.goals`           | `object`         | **The goal status map** — every goal in the active checklist. See below.                                                                                                |

### The goal status map (`goal.goals`)

Each subscriber's verified progress is persisted per goal (the same state that renders
the `[x] / [~] / [ ]` checkboxes in the agent's context). Your code receives all of it,
keyed by goal key:

```json theme={null}
{
  "contact_captured":  { "tag": "GOAL-1", "label": "Contact captured",  "required": true,  "status": "met", "value": "jane@x.com", "checked_at": "2026-07-20T09:12:00Z" },
  "service_identified":{ "tag": "GOAL-2", "label": "Service identified","required": true,  "status": "partial", "note": "service named, duration unconfirmed" },
  "appointment_booked":{ "tag": "GOAL-3", "label": "Appointment booked","required": true,  "status": "unchecked" }
}
```

* `status` is `met`, `partial`, `unmet`, or `unchecked` (never assessed yet — also the
  case for anonymous visitors, whose state has nowhere to persist).
* `tag` is the goal's positional **citation tag** — the same `GOAL-1`, `GOAL-2`, …
  labels shown on the Purpose page and in the agent's own context. Tags are numbered
  per active checklist (top-level goals, or the active stage's criteria).

Select a goal **by name** or **by tag**:

```js theme={null}
// by name (preferred — stable across reordering)
const contact = goal.goals.contact_captured;

// by tag
const byTag = (tag) => Object.values(goal.goals).find((g) => g.tag === tag);
if (byTag('GOAL-1')?.status !== 'met') {
  return { status: 'unmet', note: 'contact must be verified first' };
}
```

<Warning>
  Tags are positional: inserting or reordering goals renumbers them. Prefer selecting
  by key in code you intend to keep; use tags for quick cross-references that mirror
  the on-screen labels.
</Warning>

## The `ctx` object

The second argument is the same tool bridge custom code tools get — see the full
reference at [Custom tool ctx](/custom-tools-ctx). In short:

* `await ctx.tools.<name>(input)` — call any enabled built-in tool. Goal Checks get a
  curated, read-oriented bundle: subscriber memory + structured memory, knowledge-graph
  query, web search/fetch, calculate.
* `await ctx.audit(event, data)` — record a structured audit note.
* `ctx` identity fields (`agentId`, `agentUserId`, …) and plain `fetch` from inside the
  sandbox are also available.

The sandbox is fully isolated — no environment variables, no filesystem, 30-second
timeout, and a cross-tool call cap.

***

## Example 1 — simple: validate the claimed value

No I/O at all: the goal is met only when the claimed contact detail actually looks like
an email address or phone number. Pure validation of `goal.candidate_value`.

**Goal definition** (Purpose → Success Criteria):

```json theme={null}
{
  "label": "Contact captured",
  "key": "contact_captured",
  "type": "text",
  "description": "The customer's email address or phone number, exactly as given.",
  "required": true
}
```

**Goal Check** (body of `handle(goal, ctx)`):

```js theme={null}
const v = String(goal.candidate_value ?? '').trim();
if (!v) {
  return { status: 'unmet', note: 'no contact captured yet' };
}
const isEmail = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(v);
const isPhone = /^\+?[\d\s().-]{7,}$/.test(v);
if (!isEmail && !isPhone) {
  return { status: 'unmet', note: `"${v}" is not a valid email or phone number` };
}
return { status: 'met', value: v };
```

The agent can no longer satisfy this goal with `"will provide later"` — the gate
rejects the call and relays your `note`, so the agent knows exactly what to fix.

***

## Example 2 — medium: verify against stored data

The goal claims a booking was recorded. The check doesn't trust the claim — it reads
the subscriber's structured memory through `ctx.tools` and looks for the actual record,
returning `partial` when a booking exists but is missing its confirmation.

**Goal definition**:

```json theme={null}
{
  "label": "Appointment booked",
  "key": "appointment_booked",
  "type": "text",
  "description": "The booked slot, e.g. 2026-07-22 10:00. Must exist in the booking record.",
  "required": true
}
```

**Goal Check**:

```js theme={null}
// Read the structured memory record the agent maintains for this subscriber.
const memory = await ctx.tools.get_user_memory({});
if (memory?.error) {
  return { status: 'unmet', note: `could not read booking record: ${memory.error}` };
}

const booking = memory?.data?.booking ?? memory?.booking;
if (!booking || !booking.slot) {
  return { status: 'unmet', note: 'no booking record found in memory' };
}

// The record exists — but is it the slot being claimed, and is it confirmed?
const claimed = String(goal.candidate_value ?? '').trim();
if (claimed && !String(booking.slot).startsWith(claimed.slice(0, 10))) {
  return {
    status: 'unmet',
    note: `claimed slot "${claimed}" does not match recorded slot "${booking.slot}"`,
  };
}
if (!booking.confirmed) {
  return {
    status: 'partial',
    value: booking.slot,
    note: 'booking recorded but not yet confirmed with the customer',
  };
}
await ctx.audit('booking_verified', { slot: booking.slot });
return { status: 'met', value: booking.slot };
```

With Auto-check on, the agent's context shows `[~] appointment_booked (partially met;
booking recorded but not yet confirmed…)` until the confirmation lands — the agent
knows to close the loop instead of declaring victory.

***

## Example 3 — high complexity: external system + cross-goal ordering

A payment-collection goal that (a) refuses to pass until the *other* goals are
verified, using the **goal status map**, (b) confirms the payment against an external
API with plain `fetch`, and (c) uses `stage_context` from an earlier stage. This is the
kind of check that makes a multi-stage funnel trustworthy end-to-end.

**Goal definition** (on a `checkout` stage):

```json theme={null}
{
  "label": "Payment collected",
  "key": "payment_collected",
  "type": "text",
  "description": "The payment reference id returned by the payment provider.",
  "required": true
}
```

**Goal Check**:

```js theme={null}
// 1. Ordering: everything else in this stage must be verified first.
//    Select by key for stability; the tag lookup shows the GOAL-n alternative.
const unmetPrereqs = Object.entries(goal.goals)
  .filter(([key]) => key !== goal.key)
  .filter(([, g]) => g.required && g.status !== 'met')
  .map(([, g]) => `${g.tag} ${g.label} (${g.status})`);
if (unmetPrereqs.length > 0) {
  return {
    status: 'unmet',
    note: `verify these goals before collecting payment: ${unmetPrereqs.join(', ')}`,
  };
}

// 2. A payment reference must actually be claimed.
const ref = String(goal.candidate_value ?? '').trim();
if (!/^pay_[A-Za-z0-9]{8,}$/.test(ref)) {
  return { status: 'unmet', note: 'no valid payment reference (pay_…) supplied' };
}

// 3. Confirm against the payment provider. The order id was collected on an
//    earlier stage and rides along in stage_context.
const orderId = goal.stage_context.order_id;
if (!orderId) {
  return { status: 'unmet', note: 'order_id missing from stage context — was the intake stage completed?' };
}
let payment;
try {
  const res = await fetch(`https://api.example-payments.com/v1/payments/${encodeURIComponent(ref)}`, {
    headers: { Accept: 'application/json' },
  });
  if (!res.ok) {
    return { status: 'unmet', note: `payment lookup failed (HTTP ${res.status})` };
  }
  payment = await res.json();
} catch (e) {
  return { status: 'unmet', note: `payment provider unreachable: ${e.message}` };
}

// 4. The reference must belong to THIS order and be settled.
if (String(payment.order_id) !== String(orderId)) {
  return { status: 'unmet', note: `payment ${ref} belongs to a different order` };
}
if (payment.status === 'pending') {
  return { status: 'partial', value: ref, note: 'payment initiated, settlement pending' };
}
if (payment.status !== 'succeeded') {
  return { status: 'unmet', note: `payment status is "${payment.status}"` };
}

await ctx.audit('payment_verified', { ref, order_id: orderId, amount: payment.amount });
return { status: 'met', value: ref, note: `settled ${payment.amount} ${payment.currency}` };
```

Because the check gates `mark_goal_complete`, the stage cannot auto-advance past
checkout until the payment provider itself confirms settlement — regardless of what
the model believes.

<Note>
  Python works everywhere JS does — write the body of `async def handle(goal, ctx)`
  with the same return shape: `return { "status": "met", "value": ... }`.
</Note>

***

## Testing a check

* **Purpose page** — the **Test** button under the editor runs your (unsaved) code in
  the real sandbox against the goal's metadata and shows the verdict, note, runtime
  and provider. No subscriber state is touched; every entry in `goal.goals` reads
  `unchecked`.
* **API** — `POST /api/test-goal-evaluator` with
  `{ agent_name, goal_key, code?, sample_value? }` (owner session required).

## Semantics reference

* **One check per goal.** Empty code = no check; the goal stays LLM-assessed (or
  presence-only when Auto-check is off).
* **Hard gate**: a required code-backed goal must verify `met` for
  `mark_goal_complete` to succeed; a failing *optional* goal is subtracted from the
  optional-satisfied count. Verdicts are persisted even when the call is rejected.
* **Stage scoping**: state is kept per stage (and `_flat` for stage-less agents), so
  reused keys across stages never collide. Tags number within the active checklist.
* **Anonymous visitors**: no per-goal state is persisted (nothing to key it on); the
  gate still runs your code live at completion time.
* **Fail-open vs fail-closed**: see the table under *When your code runs*.

## Ask the AI Designer

The [AI Designer](/ai-helper) can author all of this for you — goal definitions,
`evaluator_code` Goal Checks, the `auto_eval` toggle, and fire-and-forget
[event actions](/sidebar-menu/events). Try: *"Add a goal 'deposit paid' with a code
check that verifies the deposit against our structured memory record, and turn on
Auto-check."*

## Manage via the Management MCP

Goal Checks live on the `purpose` section — each criterion's `evaluator_code` field
(see the [`purpose` field reference](/reference/purpose)). Event actions live in the
`events` section. Both are readable and writable through the
[Management MCP](/manage-mcp).
