---
title: Chatwoot AIP composite operations
description: >-
  Use three strict connector-owned composites with explicit message, status,
  handoff, idempotency, and partial-effect boundaries
kind: capability-reference
audience: application-developer
appliesTo: "1.x"
writingStandard: "aip-docs/1.0"
lastReviewedRevision: "97be86e9efedf07ecf1783b03800f683f107fb04"
connector: chatwoot
capabilityIds:
  - cap:chatwoot:message:create
  - cap:chatwoot:conversation:status
  - cap:chatwoot:conversation:handoff
aliases:
  - Chatwoot composite capabilities
  - Chatwoot strict message and handoff operations
---

# Chatwoot AIP composite operations

Use these three connector-owned capabilities when the strict message, status,
or handoff contract matches the task. Their colon-named IDs, inputs, outputs,
and provider behavior differ from the generic dot-named catalogue.

Read the pinned implementation caveats before calling them. Two published
schemas permit inputs that do not produce the intuitive runtime behavior unless
the application adds a stricter check.

## Choose composite or catalogue

| Composite on this page | Related catalogue capability | Choose the composite when |
|---|---|---|
| `cap:chatwoot:message:create` | `cap:chatwoot:message.create` | Text and public/private mode fit the strict connector input |
| `cap:chatwoot:conversation:status` | `cap:chatwoot:conversation.toggle_status` | The desired status is exactly `open`, `resolved`, or `pending` |
| `cap:chatwoot:conversation:handoff` | `cap:chatwoot:conversation.assignment` plus optional message operation | One governed intent assigns and optionally adds a private note |

Colons and dots are significant. Do not normalize either ID or reuse approval,
Action, or idempotency evidence across them.

## Composite summary

| Capability | Kind | Risk | Approval | Retry | Provider calls |
|---|---|---|---|---|---:|
| `message:create` | Tool | Medium | Required | Unsafe | 1 |
| `conversation:status` | Tool | Medium | Required | Safe | 1 |
| `conversation:handoff` | Tool | High | Required | Unsafe | 0–2 |

All three execute synchronously. None advertises async, streaming,
cancellation, transaction, reconciliation, or rollback support.

## Discovery and operation allowlist

The connector builds discovery by adding these three composites first and then
the admitted dot-named catalogue operations. The Chatwoot operations file
filters the 150-operation catalogue; it does not remove these composites from
the connector-generated manifest.

Tenant catalogue admission, routing, policy, and authorization still apply. A
connector-generated capability is not permission for every actor or tenant to
invoke it.

## Send a message

`cap:chatwoot:message:create` publishes this input:

| Field | Type | Published requirement | Meaning |
|---|---|---|---|
| `conversation_id` | String | Required | Conversation in the configured account |
| `content` | String | Required | Message or private-note text |
| `private` | Boolean | Optional; schema default `false` | `false` for public reply, `true` for private note |

Additional properties are rejected by the published schema. The caller cannot
supply the internal AIP action marker.

### Supply private explicitly

The published schema marks `private` optional, but the pinned invocation path
deserializes into a Boolean field without a serde default. A direct input that
omits `private` can fail as `connector.chatwoot.invalid_action`.

Send the field explicitly until source and schema agree:

```json
{
  "conversation_id": "314",
  "content": "Your request is now with the operations team.",
  "private": false
}
```

This guidance avoids relying on a JSON Schema default as if it were applied by
the connector.

### Provider request

The connector posts to:

```text
POST /api/v1/accounts/{configured_account_id}/conversations/314/messages
```

It constructs a provider body with:

- the supplied `content` and `private` values;
- `content_attributes.aip.connector_id` set to `chatwoot`;
- `content_attributes.aip.loop_prevention` set to `true`;
- `content_attributes.aip.action_id` set from the real AIP Action ID.

Those attributes let inbound mapping suppress the connector's own message
echo. They do not prove external delivery or provider deduplication.

### Constructed result

After a successful provider response, the connector discards the provider body
and returns:

```json
{
  "conversation_id": "314",
  "sent": true
}
```

`sent: true` means the provider request returned a successful HTTP status. It
does not prove that a customer channel delivered or displayed the message.

## Update conversation status

`cap:chatwoot:conversation:status` requires exactly:

| Field | Type | Accepted values |
|---|---|---|
| `conversation_id` | Non-blank string | Conversation in the configured account |
| `status` | String enum | `open`, `resolved`, `pending` |

Additional properties are rejected by the published schema.

Example:

```json
{
  "conversation_id": "314",
  "status": "pending"
}
```

The connector posts:

```text
POST /api/v1/accounts/{configured_account_id}/conversations/314/toggle_status
```

with:

```json
{
  "status": "pending"
}
```

After a successful provider response, it constructs:

```json
{
  "conversation_id": "314",
  "status": "pending"
}
```

This is the only Chatwoot mutation whose connector contract advertises safe
retry. The provider call still receives no AIP idempotency header. Read current
conversation state before a repeat and preserve the same governed intent.

## Handoff a conversation

`cap:chatwoot:conversation:handoff` publishes:

| Field | Type | Published requirement | Effect |
|---|---|---|---|
| `conversation_id` | String | Required | Selects the conversation |
| `assignee_id` | String | Optional | Requests provider assignment |
| `note` | String | Optional | Sends a private handoff note |

Additional properties are rejected by the schema.

### Require an intent field

The published schema requires only `conversation_id`. The pinned handler does
not reject an input with neither `assignee_id` nor `note`. It makes no provider
request and still returns `handoff: true`.

Applications should require at least one non-empty intent field before
approval:

```json
{
  "conversation_id": "314",
  "assignee_id": "42",
  "note": "Escalated with the approved incident context."
}
```

This application check is stricter than the pinned capability schema. It
prevents a successful no-op from being mistaken for a handoff.

### Provider sequence

When `assignee_id` is present, the connector first posts:

```text
POST /api/v1/accounts/{configured_account_id}/conversations/314/assignments
```

with `{"assignee_id":"42"}`.

When `note` is present, the connector then posts a private message through the
same message helper used by `message:create`. The note includes the AIP
loop-prevention, connector, and Action markers.

After all requested calls succeed, the connector constructs:

```json
{
  "conversation_id": "314",
  "handoff": true
}
```

### Partial-effect boundary

| Requested fields | Provider sequence | Important failure boundary |
|---|---|---|
| Assignee only | Assignment | A lost response can hide a completed assignment |
| Note only | Private message | A lost response can hide a created note |
| Assignee and note | Assignment, then private message | Note failure can leave assignment completed |
| Neither | No provider request | Connector returns success without a handoff effect |

The two-call path is not atomic. It has no transaction, reconcile method, or
compensating capability. Recovery must inspect assignment and message state
separately.

## Shared approval and idempotency contract

All three composites declare:

| Contract field | Value |
|---|---|
| Approval | Required; tenant-policy selector |
| Approval validity | 900,000 ms |
| Approval evidence | Reason, input snapshot, policy decision |
| Idempotency | Required; external-account scope; 86,400,000 ms published TTL |
| Data | Restricted; contains PII; redaction required |
| Execution | Synchronous; no async, streaming, or cancellation support |
| Compensation | Rollback not supported |
| Expected latency hint | 3,000 ms |
| Provider timeout hint | 30,000 ms |
| Maximum queue-delay hint | 5,000 ms |

The AIP idempotency key remains part of governed execution, but these composite
provider requests do not add an `Idempotency-Key` header. They also do not add
the catalogue's `X-AIP-Action-ID` header.

The pinned composite invoke branches do not call the catalogue's
`required_idempotency_key` check. Use the governed AIP execution path, which
owns contract enforcement; do not treat direct connector invocation as an
idempotency gate.

Message and handoff notes embed the Action ID inside provider content
attributes. Status and assignment requests contain no equivalent Action field
in their constructed bodies.

Do not treat the AIP key as proof of Chatwoot deduplication. Preserve the Action
and key for AIP settlement, then reconcile provider state before another call.

## Side effects

| Composite | Declared side effects |
|---|---|
| `message:create` | `send_message`, `write`, `external_network` |
| `conversation:status` | `write`, `external_network` |
| `conversation:handoff` | `write`, `send_message`, `identity`, `external_network` |

Handoff declares the broadest policy signal because it can change ownership
and add a private message. Approval should match the actual optional fields,
not only the capability ID.

## Failures and recovery

| Failure code | Typical cause | Safe response |
|---|---|---|
| `connector.chatwoot.invalid_action` | Schema/runtime mismatch, invalid status, or malformed strict input | Correct input before any provider dispatch |
| `connector.chatwoot.authentication` | Provider returned `401` or `403` | Verify account route and credential revision |
| `connector.chatwoot.remote_rejected` | Provider rejected input or current state | Correct authoritative state before a new Action |
| `connector.chatwoot.remote_temporary` | Provider returned `429` or a server error | Read provider state; use status retry only within its contract |
| `connector.chatwoot.transport` | Provider exchange failed | Treat the mutation outcome as uncertain |
| `connector.chatwoot.response_too_large` | Provider response crossed the active bound | Reconcile state; do not enlarge the bound to force a repeat |

Every failure sets `retryable: false` and omits a provider request ID. The
status capability's safe-retry contract is a separate policy signal.

For uncertain message creation, inspect the conversation for the Action marker
before another call. For uncertain status, read the conversation and compare
the intended state. For uncertain handoff, inspect assignment first and then
the private note.

If assignment completed but note failed, do not repeat the entire handoff
blindly. Decide whether a separately approved note-only Action is appropriate
for current state.

## Verify an integration

Require all of the following:

- discovery returns the exact colon-named capability;
- the operations allowlist is not mistaken for composite authorization;
- `private` is present explicitly for `message:create`;
- handoff contains at least one reviewed intent field;
- the governed AIP path enforces the required idempotency contract;
- approval binds every optional field and the intended external effect;
- one Action ID, canonical input, and idempotency key are retained;
- constructed output is not mistaken for raw provider output;
- message delivery, status, assignment, and note state are verified through
  authoritative Chatwoot reads;
- loop-prevention attributes are retained for connector-originated messages;
- partial or uncertain outcomes are reconciled instead of repeated.

These checks validate application handling. They do not establish customer
delivery, provider deduplication, atomic handoff, or live qualification.

## Related documentation

- [Chatwoot capability index](README.md)
- [Conversations and messages](conversations-and-messages.md)
- [Run the Chatwoot connector quickstart](../getting-started/quickstart.md)
- [Approvals and policy](../../../concepts/approvals-and-policy.md)
- [Error reference](../../../reference/errors.md)
