Actors
The Actors module represents entities — people, bots, or other participants — that interact within a project. A common use case is storing external contacts such as WhatsApp numbers, where external_id holds the phone number and correlates the actor with a record in the external system.
Overview
An Actor belongs to a project and has a display name, an optional external_id, and optional links to an Agent or Chat. Actors are identified by a public id prefixed with actor_. The internal database primary key is never returned.
See the Permissions Reference for the IAM action strings for this module.
The module covers:
- Identity — display name and external correlation via
external_id - Idempotent creation —
POST /actorswithexternal_iduses find-or-create semantics - Agent/Chat linking — an Actor can be bound to an Agent or a Chat for AI interactions
- Instructions — per-actor system prompt overrides composed into generate calls
- Tags — key-value metadata enabling attribute-based access control via IAM conditions
Related Tutorials
- Cap Spend Per End User - Step 4 (Create an actor per end user)
- Cap Spend Per End User - Step 5 (Run a turn through a session bound to the actor)
Data Model
| Field | Type | Required | Description |
|---|---|---|---|
id | string | — | Public identifier prefixed with actor_ |
project_id | string | — | Public ID of the owning project (proj_ prefix) |
name | string | Yes | Display name of the actor |
external_id | string | No | External identifier (e.g. WhatsApp phone number). Unique per project; null is never unique |
instructions | string | null | No | Persona-specific instructions composed into the effective system prompt for generate calls |
agent_id | string | null | No | Public ID of the linked Agent (agent_ prefix). Mutually exclusive with chat_id |
chat_id | string | null | No | Public ID of the linked Chat (chat_ prefix). Mutually exclusive with agent_id |
memory_id | string | null | No | Public ID of the linked Memory container (mem_ prefix). Stores persistent facts for this actor |
tags | object | No | Key-value string pairs used for ABAC conditions (see Tags) |
created_at | string | — | ISO 8601 creation timestamp |
updated_at | string | — | ISO 8601 last-updated timestamp |
Key Concepts
external_id and Idempotent Creation
external_id is a free-form string for correlating an Actor with a record in an external system (e.g. a WhatsApp phone number, a CRM contact ID). It is enforced unique per project at the database level — two actors in the same project cannot share the same external_id. Across different projects the same value is allowed.
null / absent external_id is never considered a duplicate — PostgreSQL NULL semantics are preserved.
:::warning Choose this value knowing it egresses
external_id is not internal-only. Whenever a generation runs in a session bound to this actor, the value is auto-populated into tool_context and transmitted as the X-Soat-Context-ActorExternalId request header to every http and mcp tool the agent calls — including endpoints you do not control.
If the tool set includes third-party endpoints, prefer an opaque internal identifier here (and correlate to the phone number or email on your own side) rather than storing the PII directly. See the Tool Context reference.
:::
When external_id is supplied to POST /actors, the endpoint uses find-or-create semantics:
- If no actor with that
external_idexists in the project, a new actor is created and201 Createdis returned. - If an actor with that
external_idalready exists, the existing actor is returned as-is with200 OK. None of the other request fields (name, instructions, etc.) are applied to the existing actor.
This makes actor creation safe to call repeatedly from event-driven pipelines (e.g. a new inbound WhatsApp message) without risk of duplicate actors or errors.
POST /api/v1/actors
Content-Type: application/json
{
"project_id": "proj_V1StGXR8Z5jdHi6B",
"name": "Alice",
"external_id": "+15551234567"
}
- First call →
201 Createdwith the new actor. - Subsequent calls with the same
external_id→200 OKwith the existing actor.
When external_id is not supplied, POST /actors always creates a new actor and returns 201 Created.
Agent and Chat Linking
An Actor can be linked to either an Agent or a Chat — not both simultaneously. These links control which AI backend handles generate calls initiated by or for the actor.
- Set
agent_idto link the actor to a specific Agent. - Set
chat_idto link the actor to a specific Chat. - Pass
nullin aPATCH /actors/:idrequest to unlink either field. - Supplying both
agent_idandchat_idin the same request returns400 Bad Request.
Memory Linking and Auto-Creation
An Actor can be linked to a Memory container via memory_id. The memory container stores persistent facts about the actor (e.g. preferences, conversation history summaries) that can be injected into AI generation calls.
Manual linking — supply memory_id in the POST /actors or PATCH /actors/:id request body:
{ "memory_id": "mem_V1StGXR8Z5jdHi6B" }
Auto-creation — set auto_create_memory: true in the POST /actors body to automatically create a new memory container named after the actor and link it:
{
"project_id": "proj_ABC",
"name": "Alice",
"external_id": "+15551234567",
"auto_create_memory": true
}
The response will include memory_id pointing to the newly created memory.
auto_create_memory is create-if-missing, not create-always: it only creates a memory when no memory_id is supplied in the same request. If you pass an explicit memory_id, that memory is linked and auto_create_memory has no effect. When combined with external_id (idempotent creation), the memory is created at most once — repeat calls return the existing actor with its existing memory_id unchanged.
Deleting an actor does not delete its linked memory. Memory data outlives the actor record and may contain valuable information.
Once you have the actor's memory_id, use it in generation calls:
{
"knowledge_config": {
"memory_ids": ["mem_V1StGXR8Z5jdHi6B"],
"write_memory_id": "mem_V1StGXR8Z5jdHi6B"
}
}
To unlink a memory from an actor without deleting it, PATCH the actor with "memory_id": null.
Instructions
instructions is a free-form string injected into the system prompt when an AI generation is scoped to this actor. Use it to encode persona-specific context (tone, name, constraints) that should be consistent across all interactions with the actor.
Pass null to PATCH /actors/:id to clear the instructions.
Filtering
GET /actors supports the following query parameters for filtering:
| Parameter | Description |
|---|---|
project_id | Limit results to a specific project (required for JWT callers in most cases) |
external_id | Exact match — use to resolve an external identifier to an actor_ ID |
name | Partial, case-insensitive match against the actor's display name |
limit | Maximum number of results to return (default: 50) |
offset | Number of results to skip for pagination (default: 0) |
The response envelope is:
{
"data": [
/* ActorRecord[] */
],
"total": 42,
"limit": 50,
"offset": 0
}
Project Scope
Project-scoped API keys make project_id optional: omit it and the request defaults to the key's project, supply a matching one and it is accepted, and supply a different project's id and the request is rejected with 403. JWT callers must supply project_id explicitly for write operations. See Implicit project id for the full rules.
Tags
Tags are key-value string pairs attached to an actor. They enable attribute-based access control (ABAC) via IAM condition keys (see IAM).
{
"tags": {
"channel": "whatsapp",
"tier": "premium"
}
}
Tags can be managed via the dedicated tag sub-endpoints:
| Method | Endpoint | Description |
|---|---|---|
GET | /api/v1/actors/:id/tags | Return the actor's current tags |
PUT | /api/v1/actors/:id/tags | Replace all tags (any tags not in the body are removed) |
PATCH | /api/v1/actors/:id/tags | Merge tags (existing tags not in the body are kept) |
All tag endpoints require actors:UpdateActor permission.
SOAT Resource Names
Actors use the actor resource type in SRNs:
soat:<project_id>:actor:<actor_id>
Example: soat:proj_ABC:actor:actor_123
Use SRN patterns in policy resource fields to scope permissions to specific actors or all actors in a project:
{
"effect": "Allow",
"action": ["actors:GetActor", "actors:ListActors"],
"resource": ["soat:proj_ABC:actor:*"]
}
Examples
Create an actor
- CLI
- SDK
- curl
soat create-actor \
--project-id proj_ABC \
--name Alice \
--external-id +15551234567
// SDK
import { SoatClient } from '@soat/sdk';
const soat = new SoatClient({
baseUrl: 'https://api.example.com',
token: 'sk_...',
});
const { data, error } = await soat.actors.createActor({
body: {
project_id: 'proj_ABC',
name: 'Alice',
external_id: '+15551234567',
},
});
if (error) throw new Error(JSON.stringify(error));
curl -X POST https://api.example.com/api/v1/actors \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"project_id": "proj_ABC",
"name": "Alice",
"external_id": "+15551234567"
}'
Idempotent actor upsert
Safe to call on every inbound message — creates the actor on first contact, returns the existing record thereafter:
- CLI
- SDK
- curl
soat create-actor --name Bob --external-id +15559876543
// SDK — identical call; 201 on create, 200 when the actor already exists
const { data, error } = await soat.actors.createActor({
body: { name: 'Bob', external_id: '+15559876543' },
});
if (error) throw new Error(JSON.stringify(error));
curl -X POST https://api.example.com/api/v1/actors \
-H "Authorization: Bearer <project-key>" \
-H "Content-Type: application/json" \
-d '{
"name": "Bob",
"external_id": "+15559876543"
}'
Allow a user to manage all actors in a project
{
"statement": [
{
"effect": "Allow",
"action": ["actors:*"],
"resource": ["soat:proj_ABC:actor:*"]
}
]
}
Restrict access to actors tagged with a specific channel
{
"statement": [
{
"effect": "Allow",
"action": ["actors:GetActor", "actors:ListActors"],
"resource": ["soat:proj_ABC:actor:*"],
"condition": {
"StringEquals": {
"soat:ResourceTag/channel": "whatsapp"
}
}
}
]
}