---
title: "Sense"
description: "How a pack announces that the world changed and how the runtime delivers it, under Policy and a separate grant, with no power to mint or act."
canonical: https://harnesscontextprotocol.io/docs/concepts/sense
---

# Sense

> How a pack announces that the world changed and how the runtime delivers it, under Policy and a separate grant, with no power to mint or act.

Sense is the outbound loop of HCP: Execute says "do this", and Sense says "this
happened". A pack declares triggers, the runtime delivers them into a sink while
the `sense.enable` grant is on, and an Invoker decides what happens next, which
may include running Execute under its own grants. Pack authors start at
[Triggers](#triggers), runtime builders at [Sense delivery](#sense-delivery).

```text
world changes ──► pack announces `new_item` ──► runtime sink records
                                                     │
                                               (optional wake)
                                                     ▼
                                       Invoker runs Execute: list → get → gated write
```

## Isolation

Sense is emit-only. An event that could execute could also spend, so the path
from observation to action passes through every gate a typed command would.

| Sense must | Sense must not |
|------------|----------------|
| Share Policy, Binding, and land with Execute on the same pack | Mint credentials or bypass Binding |
| Fail closed on an unknown ref or when `sense.enable` is off | Treat polling as Sense |
| Be optional on most packs | Replace pull commands (`status`, `follow`) |
| Wake Execute only after the sink has recorded | Become a second pack catalog |

## Triggers

A trigger is a world change your pack may announce. Declare one only when the
upstream system really pushes, because what an Invoker has to pull is Execute.

| Ask | If yes |
|-----|--------|
| Does the upstream system push this to you (webhook, event stream, vendor notification)? | Trigger |
| Do you have to ask for it with `status`, `follow`, `show` or `list`? | Read command. Not a trigger |
| Is it your runtime's uptime check or a UI refresh timer? | Not a pack trigger at all |
| Did the pack's own credential go stale? | Trigger: `binding.degraded` |

Agents may poll observation commands, and that polling is never Sense. If the
source cannot push yet, ship pull commands and add the trigger once it can,
without overloading a command such as `plan` to fake one.

### Declare

Declare each trigger in two places that agree:

```json title="surface.json (fragment)"
"stimulusTriggers": [
  { "ref": "new_item" },
  { "ref": "binding.degraded" }
]
```

```json title="policy.json (fragment)"
"stimuli": {
  "new_item":         { "operation": "read", "privacy": { "land": "personal" } },
  "binding.degraded": { "operation": "read", "privacy": { "land": "personal" } }
}
```

Lint fails on a ref without a row or a row without a ref. Refs are lower case,
dot-separated and name the fact: `new_item`, never `please_list`. Land follows
the data, so a personal mailbox event is `personal`, a shared project stream
`brain` and a watched workspace directory `local`. The sink records each event
under its Principal, Binding and land, so a `brain` trigger still needs project
membership to be surfaced.

### Kinds

Sense has one loop and four kinds. Mail, calendar, chat and tickets are all
channel events on that loop, and only the Binding family and the payload differ.

| Kind | Refs | Then |
|------|------|------|
| Channel event | `new_item` (a new item on the linked stream), `item.updated` (an existing item changed) | Execute `list` / `get`; optional gated write |
| Binding health | `binding.degraded`, with a [state](/docs/concepts/binding#health) | A human runs `connect`. `doctor` reports; it does not mint |
| Domain observation | `run.completed` (an upstream job finished), a settlement posted | `show` / `follow`, or a gated write |
| Emit-only | Any ref on a `sense_only` pack | No user command required |

Design each trigger together with its reaction, which is Execute under its own
Policy row and grant:

```text
new_item        ──►  sink  ──►  Invoker: mail.get <id>  ──►  (maybe) mail.send --confirm
binding.degraded ──► sink  ──►  human: mail connect
run.completed   ──►  sink  ──►  Invoker: show <run>
```

### Composition

A linked channel carries Execute (`list`, `get`, `send`) and Sense (`new_item`,
`binding.degraded`) on one pack id, and a public read pack is Execute only until
a real push exists. An emit-only pack is a pure ingress source that declares
`kind: "sense_only"`, at least one trigger and no `verbs`, with a `connect` only
if the upstream needs a registration call. Control is Execute on a second pack,
because operator health is not a user inbox.

A pack that declares Sense adds two lines to its
[proof script](/docs/packs/check#the-proof-script):

```text
one announce with sense.enable on   → sink records
one announce with sense.enable off  → dropped, Execute unchanged
```

## Sense delivery

The runtime activates Sense per pack, independently of Execute, and turning it
off never touches the CLI wrap.

| Activation | Route | Effect on Execute |
|------------|-------|-------------------|
| on | `inbox` (surface to the user) or `silent` (record only) | None |
| off | `drop`: acknowledge the producer, do not deliver | None |

Route selection is runtime code, and the reference shape is:

```ts
function resolveSenseRoute(activation: 'on' | 'off', ref: string): 'inbox' | 'silent' | 'drop' {
  if (activation === 'off') return 'drop'
  return isUserFacing(ref) ? 'inbox' : 'silent'
}
```

Delivery requires `sense.enable` for the Principal and pack. Without the grant
the runtime drops the trigger, an unknown ref is dropped and reported, and with
both checks passed the runtime records under `Principal + Binding + land`:

```text
ingress → validate ref is declared → check sense.enable → resolve route → sink
```

A sink is wherever recorded triggers go: an inbox table, a queue, a webhook
fan-out or a UI badge counter. Sinks are runtime machinery with no HCP type. The
protocol requires only that each record is attributable (Principal, Binding,
land, pack, ref), that recording never mints, spends or runs a command, and that
an Invoker reading the sink later runs Execute under Execute's own grants. A
runtime must not shortcut that last step, since Sense recording and Execute
authorisation are separate code paths with separate grants.

How a trigger reaches the runtime has no HCP type either. Choose a mechanism per
pack family and document it in your runtime's own docs, because the pack does
not carry it.

| Mechanism | Example |
|-----------|---------|
| Signed ingress | HMAC-verified webhook from the upstream provider |
| Vendor push mapping | Provider change notification → pack ref |
| Queue | Internal event bus |
| Pack process | A `sense_only` pack emitting on stdout |

The [runtime checklist](/docs/runtimes/conformance#sense-if-implemented) lists
what conformance needs from delivery, including `binding.degraded` from the hub.

## What Sense is not

- A product ledger, database schema, or inbox UI
- A requirement that every pack emit events
- A way around Binding
- Polling with a nicer name, such as an agent polling `status`
- Uptime monitors, UI refresh timers or control-plane ticks, which are runtime
  machinery and never go through the sink as pack triggers

## Related

- [Specification: Sense](/docs/specification/sense)

## Sitemap

- [Documentation](https://harnesscontextprotocol.io/docs)
- [Packs](https://harnesscontextprotocol.io/packs)
- [Glossary](https://harnesscontextprotocol.io/docs/reference/glossary)
- [llms.txt](https://harnesscontextprotocol.io/llms.txt)
- [llms-full.txt](https://harnesscontextprotocol.io/llms-full.txt)
- [OpenAPI](https://harnesscontextprotocol.io/openapi.json)
- [MCP](https://harnesscontextprotocol.io/mcp)
