# Property Actions — fire a side-effect when a property reaches a value

> A **custom property** is inert: an external evaluator fills its value, and by itself nothing
> happens (`knowledge/custom-properties.md`). A **property action** is one of the things that
> *consumes* that value — it runs a **side-effect the moment the property satisfies a
> condition**: hit a webhook, or act on a CRM (move a stage, update a field, open a task, push
> the contact). This is distinct from contact limits / notifications / workflows (the other
> consumers). Pairs with `knowledge/mcp-tool-reference.md` and `knowledge/messaging-channels.md`.

## The shape

A property action = **a condition + one side-effect**:

- **Condition** — `custom_property_id` + `operator` (EQUALS, GREATER_THAN, IS_NOT_EMPTY, …) +
  the compared value (`value_boolean` / `value_int` / `value_string` / `value_date_time` /
  `value_option_property_id`, matched to the property's type). The action fires when the
  property's value satisfies this.
- **`action_type`** — picks the side-effect, and which extra fields matter:

| `action_type` | Does | Needs |
|---|---|---|
| `HTTP_REQUEST` | Calls a webhook | `http_url` (+ optional `http_method`, `http_headers`, `http_body_template`) |
| `CRM_STAGE_TRANSITION` | Moves an opportunity to a stage | `crm_sync_connection_id`, `crm_target_stage_id` (+ `crm_opportunity_lookup`, `crm_create_if_not_exists`) |
| `CRM_FIELD_UPDATE` | Writes a CRM field | `crm_sync_connection_id` (+ `crm_target_entity`, `crm_external_field_id`, `crm_value_source`, `crm_static_value`) |
| `CRM_PUSH_CONTACT` | Pushes the contact into the CRM | `crm_sync_connection_id` (+ `crm_create_opportunity`, `crm_stage_id`) |

You don't need any of the CRM ids by hand — **discover them** (next section).

## Discovering the CRM ids — the discovery tools

A CRM action needs ids from the CRM (a connection, a stage, a field). Resolve them by MCP,
no copy-paste from Ninjo:

```text
list_crm_connections()                         → sync_connection_id (+ provider)  ← start here
list_crm_pipelines(sync_connection_id?)        → pipeline_id
list_crm_stages(pipeline_id)                   → crm_target_stage_id / crm_stage_id
list_crm_fields(sync_connection_id, entity)    → crm_external_field_id   (entity: contact|opportunity)
```

- `list_crm_connections` is the **root** — the influencer's CRM connections (superset of
  `list_agent_channels`, which only shows connections routed to a given agent).
- Connections / pipelines / stages come from the **local synced mirror** (fast; empty if a
  connection never synced). `list_crm_fields` is **live** from the CRM (via mk1-chat).
- `entity` on fields: CRM custom fields hang off an object, so ask for `contact` **or**
  `opportunity` fields — matches the action's `crm_target_entity` (default `contact`).

So the end-to-end flow, with nothing known in advance:
> `list_crm_connections` → `list_crm_pipelines` → `list_crm_stages` (or `list_crm_fields`)
> → `upsert_property_action` with the ids you got back.

## Reading — `list_property_actions`

```text
list_property_actions(influencer_id?) → { propertyActions, count }
```

Each item carries its `condition` block, its `action_type`, and **exactly one** populated
subtype block (`http` / `crm_stage_transition` / `crm_field_update` / `crm_push_contact`) —
the rest are `null`. Use it to see how a property is wired to downstream automation and to
CRM channels. Read-only, metadata only.

## Writing — `upsert_property_action` / `delete_property_action`

```text
upsert_property_action({ name, enabled, custom_property_id, operator, action_type, <value_*>, <subtype fields> })
  → { success, id, action_type }          # omit `id` to CREATE, pass `id` to UPDATE
delete_property_action({ id }) → { success }   # the subtype row cascades away too
```

- **Create vs update**: no `id` creates a new action; passing an existing `id` updates it.
- For a **booking/conversion webhook**: `action_type: HTTP_REQUEST`, point `http_url` at the
  endpoint, and set the condition on the terminal boolean property (e.g. `call_booked` EQUALS
  true). Prefer a **boolean/number** property for the gate — string "true"/"false" compares
  unreliably (same rule as automation conditions, `knowledge/custom-properties.md`).
- For a **CRM action**: set the matching `crm_*` fields; the `crm_sync_connection_id` (and, for
  a stage transition, `crm_target_stage_id`) must belong to the influencer.
- Validation (operator↔type compatibility, that the property / sync connection / target stage
  all belong to you) runs in **mk1-chat** — the gateway forwards writes there so the domain
  logic lives in one place. A bad combination comes back as a plain error to fix and retry.

## Where this fits

Defining a property is half the job; **wiring a consumer off it is the other half**
(`cortex-gateway` MCP server instructions). Property actions are the *immediate side-effect*
consumer — for stop-messaging / notify / re-engage, use contact limits, custom notifications,
and follow-up workflows instead (`knowledge/workflows.md`, `knowledge/custom-properties.md`).
