Assistants
Build task flows
Build ordered task flows, link them to assistants, and override task instructions per assistant.
A task group is a named, ordered sequence of tasks: a structured flow an assistant can follow, such as a booking flow that collects party size, then date, then a contact number. Task groups belong to your account rather than to a single assistant: you link a group to as many assistants as need it, and any one of those assistants can override an individual task's instructions without forking the group.
Sixteen endpoints cover the whole surface. They fall into four sets: the groups themselves, the tasks inside a group, the link between an assistant and a group, and the per-assistant instruction overrides. Every field and status code is also listed in the Task Groups API reference.
| Method | Path | Purpose |
|---|---|---|
| GET | /taskgroups | List task groups |
| POST | /taskgroups | Create a task group |
| GET | /taskgroups/{id} | Retrieve one task group with its tasks |
| PATCH | /taskgroups/{id} | Update a group's name or description |
| DELETE | /taskgroups/{id} | Delete a task group |
| GET | /taskgroups/{id}/tasks | List the tasks in a group |
| POST | /taskgroups/{id}/tasks | Add a task to a group |
| PATCH | /taskgroups/{id}/tasks/{taskId} | Update a task |
| POST | /taskgroups/{id}/tasks/reorder | Reorder the tasks in a group |
| DELETE | /taskgroups/{id}/tasks/{taskId} | Delete a task |
| GET | /assistants/{id}/taskgroups | List the groups linked to an assistant |
| POST | /assistants/{id}/taskgroups | Link a group to an assistant |
| DELETE | /assistants/{id}/taskgroups/{taskgroupId} | Unlink a group from an assistant |
| GET | /assistants/{id}/tasks/overrides | List an assistant's task overrides |
| POST | /assistants/{id}/tasks/{taskId}/override | Set an instruction override |
| DELETE | /assistants/{id}/tasks/{taskId}/override | Remove an instruction override |
Overview#
Four objects, in the order you create them:
task group name + description, owned by your account
└── task order + name + description + instructions
assistant ──link──► task group one link row per assistant/group pair
assistant ──override──► task replacement instructions for one task, one assistantA typical build is five calls:
POST /taskgroups: create the group.POST /taskgroups/{id}/tasks: add each task, giving every one anorder.POST /assistants/{id}/taskgroups: link the group to an assistant.PATCH /assistants/{id}: settaskDrivenModetotrueon that assistant.POST /assistants/{id}/tasks/{taskId}/override: only where one assistant needs a different instruction for a shared task.
Task-driven mode is the switch
Linking a group to an assistant does not by itself change how the assistant talks. Assistants carry a taskDrivenMode boolean, set through POST /assistants or PATCH /assistants/{id}, which defaults to false. When it is true, the assistant's linked task groups and their tasks are supplied to the conversation as structured data; when it is false, the standard prompt pipeline runs unchanged. See Assistants.
Every response on this page uses the standard envelope (success, data, metadata) described in API conventions. The shapes below are what you get in data. Errors use the same envelope with error in place of data; the codes are in Errors.
The task group and task shapes#
Both objects come back from several endpoints, so they are described once here and referred to below.
Task group
| Field | Type | Description |
|---|---|---|
id | string | The group's UUID |
tenantId | string | The account that owns the group |
name | string | The group's name |
description | string | What the flow is for |
tasks | array | The group's tasks (present on the single-group, create, update and delete responses) |
isDeleted | boolean | Deletion flag |
createdAt | string | null | ISO 8601 timestamp |
updatedAt | string | null | ISO 8601 timestamp |
deletedAt | string | null | ISO 8601 timestamp, null while the group is live |
Task
| Field | Type | Description |
|---|---|---|
id | string | The task's UUID |
tenantId | string | The account that owns the task |
taskgroupId | string | The group the task belongs to |
order | number | The task's position in the flow |
name | string | The task's name |
description | string | What the task achieves |
instructions | string | How the assistant should carry the task out |
outputs | string[] | null | Names of the values the task is expected to produce |
toolNames | string[] | null | Names of the tools the task may use |
type | string | null | A label for the kind of task |
isDeleted | boolean | Deletion flag |
createdAt | string | null | ISO 8601 timestamp |
updatedAt | string | null | ISO 8601 timestamp |
deletedAt | string | null | ISO 8601 timestamp, null while the task is live |
Placeholders in task instructions#
A task's instructions can reference an output produced by an earlier task in the same
group, using a single-brace placeholder:
{ "order": 1, "name": "collect_party_size", "outputs": ["party_size"] }
{ "order": 2, "name": "confirm_booking",
"instructions": "Read the booking back: a table for {party_size}." }A placeholder name must start with a letter or underscore. {1st_choice} is rejected as
malformed rather than treated as literal text. That check exists to catch typos that
would otherwise reach a caller verbatim.
References run forward only#
A task may only reference outputs from tasks at an earlier order. Referencing its own
outputs, or those of any task at the same order or later, is rejected when you save it:
Task at order 2 references forward/own outputs: booking_ref.
Placeholders may only reference outputs from tasks at earlier orders.This is why reordering can fail: moving a task above one whose output it depends on breaks the rule.
A placeholder that matches no earlier output is not an error: it passes validation and may resolve from the call's own context at run time. So a typo in an output name fails silently rather than loudly; check the name against the producing task.
{{double braces}} are not an escape
Doubling the braces does not produce a literal. {{party_size}} contains {party_size},
which is matched and validated exactly as the single-brace form. There is no way to write
a literal {name} in a task instruction. Note this differs from an assistant's
developerPrompt, which uses {{placeholder}} for its parameters; the two syntaxes are
not interchangeable.
Create a task group#
POST /taskgroups
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Must match ^[a-z0-9_]+$ (lowercase letters, digits and underscores only) |
description | string | Yes | What the flow is for |
curl -X POST https://core-api.heysadie.ai/taskgroups \
-H "Authorization: ApiKey YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "table_booking",
"description": "Collects everything needed to book a table."
}'data is the new task group, with a tasks array you have not filled in yet:
{
"success": true,
"data": {
"id": "0195f1e2-5555-7000-8000-000000000000",
"tenantId": "0195f1e2-1111-7000-8000-000000000000",
"name": "table_booking",
"description": "Collects everything needed to book a table.",
"isDeleted": false,
"createdAt": "2026-07-30T12:00:00.000Z",
"updatedAt": "2026-07-30T12:00:00.000Z",
"deletedAt": null,
"tasks": []
},
"metadata": {
"timestamp": "2026-07-30T12:00:00.000Z",
"version": "1.0",
"path": "/taskgroups"
}
}Keep the id: every task, link and override call below needs it.
List task groups#
GET /taskgroups
| Query param | Type | Required | Description |
|---|---|---|---|
limit | number | No | Page size. Default 50, maximum 500. |
offset | number | No | Items to skip. Default 0. |
curl "https://core-api.heysadie.ai/taskgroups?limit=50&offset=0" \
-H "Authorization: ApiKey YOUR_API_KEY"data has two keys: taskgroups, an array of task group objects, and total, the number of groups available. Page through it by advancing offset until you have seen total groups.
{
"success": true,
"data": {
"taskgroups": [
{
"id": "0195f1e2-5555-7000-8000-000000000000",
"tenantId": "0195f1e2-1111-7000-8000-000000000000",
"name": "table_booking",
"description": "Collects everything needed to book a table.",
"isDeleted": false,
"createdAt": "2026-07-30T12:00:00.000Z",
"updatedAt": "2026-07-30T12:00:00.000Z",
"deletedAt": null
}
],
"total": 1
},
"metadata": {
"timestamp": "2026-07-30T12:05:00.000Z",
"version": "1.0",
"path": "/taskgroups"
}
}Retrieve a task group#
GET /taskgroups/{id} returns one group with its tasks nested under tasks: the whole flow in a single call, which is the endpoint to use when you want to render or diff a complete procedure.
curl https://core-api.heysadie.ai/taskgroups/0195f1e2-5555-7000-8000-000000000000 \
-H "Authorization: ApiKey YOUR_API_KEY"data is a single task group object, including tasks. A group that does not exist (or belongs to another account) returns 404 with RESOURCE_NOT_FOUND.
Update a task group#
PATCH /taskgroups/{id} changes the group's own metadata. Both fields are optional; send only what you are changing. To edit what the assistant actually does, edit the tasks, not the group.
| Field | Type | Required | Description |
|---|---|---|---|
name | string | No | Must match ^[a-z0-9_]+$ |
description | string | No | What the flow is for |
curl -X PATCH https://core-api.heysadie.ai/taskgroups/0195f1e2-5555-7000-8000-000000000000 \
-H "Authorization: ApiKey YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "description": "Books a table and captures dietary requirements." }'data is the updated task group object with its tasks array, so you can use the response to refresh your local copy without a follow-up GET.
Delete a task group#
DELETE /taskgroups/{id} returns the task group object it removed: id, name, description, the tasks array, and the isDeleted / deletedAt fields that record the deletion.
curl -X DELETE https://core-api.heysadie.ai/taskgroups/0195f1e2-5555-7000-8000-000000000000 \
-H "Authorization: ApiKey YOUR_API_KEY"Deleting is a soft delete, and it cascades
DELETE /taskgroups/{id} does three things in one operation:
- marks the group deleted,
- marks every task in it deleted,
- removes the group's links to every assistant that had it attached.
Deleted groups and tasks stop appearing in list results (there is no
includeDeleted-style filter), so treat the operation as irreversible from the API's point
of view even though the records are retained rather than erased.
You do not need to unlink the group from assistants first; the delete does it for you. But
any assistant that was using it loses that flow immediately, so check
GET /assistants/{id}/taskgroups across your assistants before deleting a shared group.
Add a task#
POST /taskgroups/{id}/tasks adds one task to the group. Tasks are what the assistant actually follows: description says what the step is for, instructions says how to carry it out.
| Field | Type | Required | Description |
|---|---|---|---|
order | number | Yes | Position in the flow |
name | string | Yes | The task's name |
description | string | Yes | What the task achieves |
instructions | string | Yes | How the assistant should carry the task out |
outputs | string[] | null | No | Names of the values the task should produce |
toolNames | string[] | null | No | Names of the tools this task may use |
type | string | null | No | A label for the kind of task |
curl -X POST https://core-api.heysadie.ai/taskgroups/0195f1e2-5555-7000-8000-000000000000/tasks \
-H "Authorization: ApiKey YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"order": 0,
"name": "Collect party size",
"description": "Find out how many people the booking is for.",
"instructions": "Ask the caller how many guests will be dining. Accept 1 to 20; if they say more, tell them large parties are booked by the events team.",
"outputs": ["party_size"]
}'data is the created task:
{
"success": true,
"data": {
"id": "0195f1e2-6666-7000-8000-000000000000",
"tenantId": "0195f1e2-1111-7000-8000-000000000000",
"taskgroupId": "0195f1e2-5555-7000-8000-000000000000",
"order": 0,
"name": "Collect party size",
"description": "Find out how many people the booking is for.",
"instructions": "Ask the caller how many guests will be dining. Accept 1 to 20; if they say more, tell them large parties are booked by the events team.",
"outputs": ["party_size"],
"toolNames": null,
"type": null,
"isDeleted": false,
"createdAt": "2026-07-30T12:10:00.000Z",
"updatedAt": "2026-07-30T12:10:00.000Z",
"deletedAt": null
},
"metadata": {
"timestamp": "2026-07-30T12:10:00.000Z",
"version": "1.0",
"path": "/taskgroups/0195f1e2-5555-7000-8000-000000000000/tasks"
}
}Repeat the call for each step, incrementing order. Keep the returned id values: a task's id is what an override is set against.
List tasks#
GET /taskgroups/{id}/tasks returns the group's tasks. data is an array of task objects: the same shape as the create response, not wrapped in an object and with no total.
curl https://core-api.heysadie.ai/taskgroups/0195f1e2-5555-7000-8000-000000000000/tasks \
-H "Authorization: ApiKey YOUR_API_KEY"order is the field that defines the sequence. The specification does not state the array's sort order, so sort by order client-side (as the snippet above does) rather than relying on array position.
Update a task#
PATCH /taskgroups/{id}/tasks/{taskId} takes the same fields as create, all optional. This is where you change a shared step for every assistant using the group; to change it for one assistant only, set an override instead.
| Field | Type | Required | Description |
|---|---|---|---|
order | number | No | Position in the flow |
name | string | No | The task's name |
description | string | No | What the task achieves |
instructions | string | No | How the assistant should carry the task out |
outputs | string[] | null | No | Names of the values the task should produce |
toolNames | string[] | null | No | Names of the tools this task may use |
type | string | null | No | A label for the kind of task |
curl -X PATCH https://core-api.heysadie.ai/taskgroups/0195f1e2-5555-7000-8000-000000000000/tasks/0195f1e2-6666-7000-8000-000000000000 \
-H "Authorization: ApiKey YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"instructions": "Ask how many guests will be dining, including children."
}'data is the updated task object, including the fields you did not send.
Reorder tasks#
POST /taskgroups/{id}/tasks/reorder sets the position of tasks in one call, which is what you want after a drag-and-drop edit: send the whole flow rather than issuing a PATCH per task and leaving the group briefly inconsistent.
| Field | Type | Required | Description |
|---|---|---|---|
order | array | Yes | Objects pairing a task with its new position |
order[].taskId | string (UUID) | Yes | The task to move |
order[].order | number | Yes | Its new position |
curl -X POST https://core-api.heysadie.ai/taskgroups/0195f1e2-5555-7000-8000-000000000000/tasks/reorder \
-H "Authorization: ApiKey YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"order": [
{ "taskId": "0195f1e2-7777-7000-8000-000000000000", "order": 0 },
{ "taskId": "0195f1e2-6666-7000-8000-000000000000", "order": 1 }
]
}'data is an array of task objects carrying their new order values: the same shape GET /taskgroups/{id}/tasks returns, so you can render the result straight from the response.
A reorder must list every task, exactly once
The order array is a complete new layout, not a patch. Four rules are enforced:
- every
taskIdmust be a live task in this group: unknown IDs are rejected and named; - the array must contain every task in the group, exactly once: omitting one or
repeating one is rejected with
Reorder payload must include every task in the taskgroup exactly once; - no two tasks may share an
ordervalue; - the resulting layout is re-checked against the forward-only placeholder rule.
That last one is the surprising failure: moving a task above one whose output it references is rejected, because the reference would then point forward. Read the group, reorder the whole list locally, and send it back complete.
Delete a task#
DELETE /taskgroups/{id}/tasks/{taskId} removes one task from the group. data is the task object that was removed, carrying isDeleted and deletedAt.
curl -X DELETE https://core-api.heysadie.ai/taskgroups/0195f1e2-5555-7000-8000-000000000000/tasks/0195f1e2-6666-7000-8000-000000000000 \
-H "Authorization: ApiKey YOUR_API_KEY"Deleting a task leaves gaps in the remaining order values. Follow up with a reorder call if your flow depends on contiguous positions.
Link a task group to an assistant#
POST /assistants/{id}/taskgroups links an existing group to an assistant. The group is unchanged by this; linking is a relationship, so the same group can be linked to as many assistants as you like.
| Field | Type | Required | Description |
|---|---|---|---|
taskgroupId | string (UUID) | Yes | The group to link |
curl -X POST https://core-api.heysadie.ai/assistants/0195f1e2-2222-7000-8000-000000000000/taskgroups \
-H "Authorization: ApiKey YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "taskgroupId": "0195f1e2-5555-7000-8000-000000000000" }'The response is the standard envelope with success: true and metadata; the specification does not define a payload for data on this endpoint, so branch on success and read the resulting state from GET /assistants/{id}/taskgroups rather than from the create response.
Remember that a link alone changes nothing until taskDrivenMode is true on the assistant.
List an assistant's task groups#
GET /assistants/{id}/taskgroups returns everything linked to one assistant, each group with its tasks nested. That is enough to render the assistant's full set of flows in a single call.
| Field | Type | Description |
|---|---|---|
linkId | string | The link between this assistant and this group |
assistantId | string | The assistant |
taskgroupId | string | The linked group |
name | string | The group's name |
description | string | The group's description |
tasks | array | The group's tasks, in the task shape described above |
curl https://core-api.heysadie.ai/assistants/0195f1e2-2222-7000-8000-000000000000/taskgroups \
-H "Authorization: ApiKey YOUR_API_KEY"{
"success": true,
"data": [
{
"linkId": "0195f1e2-8888-7000-8000-000000000000",
"assistantId": "0195f1e2-2222-7000-8000-000000000000",
"taskgroupId": "0195f1e2-5555-7000-8000-000000000000",
"name": "table_booking",
"description": "Collects everything needed to book a table.",
"tasks": [
{
"id": "0195f1e2-6666-7000-8000-000000000000",
"tenantId": "0195f1e2-1111-7000-8000-000000000000",
"taskgroupId": "0195f1e2-5555-7000-8000-000000000000",
"order": 0,
"name": "Collect party size",
"description": "Find out how many people the booking is for.",
"instructions": "Ask the caller how many guests will be dining.",
"outputs": ["party_size"],
"toolNames": null,
"type": null,
"isDeleted": false,
"createdAt": "2026-07-30T12:10:00.000Z",
"updatedAt": "2026-07-30T12:10:00.000Z",
"deletedAt": null
}
]
}
],
"metadata": {
"timestamp": "2026-07-30T12:20:00.000Z",
"version": "1.0",
"path": "/assistants/0195f1e2-2222-7000-8000-000000000000/taskgroups"
}
}The nested tasks carry the group's shared instructions. Overrides are not merged into this response. Read them from GET /assistants/{id}/tasks/overrides and apply them against taskId if you are showing an operator what one assistant will actually say.
Unlink a task group#
DELETE /assistants/{id}/taskgroups/{taskgroupId} removes the link between that assistant and that group. It addresses the link, not the group: the group itself is a separate resource with its own DELETE /taskgroups/{id}, and every other assistant linked to it is untouched.
curl -X DELETE https://core-api.heysadie.ai/assistants/0195f1e2-2222-7000-8000-000000000000/taskgroups/0195f1e2-5555-7000-8000-000000000000 \
-H "Authorization: ApiKey YOUR_API_KEY"The response is the standard envelope with success: true; the specification defines no payload for data.
Override a task for one assistant#
This is the mechanism that keeps one flow usable across many assistants, so it is worth being precise about what it does.
A task group is shared. Its tasks (including their instructions) belong to the group, and editing a task with PATCH /taskgroups/{id}/tasks/{taskId} changes it for every assistant linked to that group. That is usually what you want. What you do not want is to duplicate an eight-step booking flow because one restaurant needs to ask about outdoor seating.
An override solves exactly that. POST /assistants/{id}/tasks/{taskId}/override records replacement instruction text for one task, against one assistant. It writes nothing to the task group, so:
- the shared task keeps its original
instructions; - every other assistant linked to the group is unaffected;
- there is no second copy of the flow to keep in sync: structural edits still happen once, on the group.
Scope it correctly: an override replaces instruction text for a single task. It cannot add a task, remove one, or change the order for one assistant. Those are properties of the group, and an assistant that needs a genuinely different sequence needs a different group.
| Field | Type | Required | Description |
|---|---|---|---|
instructionOverride | string | Yes | The instruction text to use for this task on this assistant |
curl -X POST https://core-api.heysadie.ai/assistants/0195f1e2-2222-7000-8000-000000000000/tasks/0195f1e2-6666-7000-8000-000000000000/override \
-H "Authorization: ApiKey YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"instructionOverride": "Ask how many guests will be dining, then ask whether they would prefer indoor or outdoor seating."
}'The response is the standard envelope with success: true; data contains the stored override record. You can also read every active override with GET /assistants/{id}/tasks/overrides.
Re-posting an override updates it
Four behaviours are part of the current contract:
POSTis idempotent in effect. If an override already exists for that assistant-and-task pair, a secondPOSTupdates the stored text rather than failing with a conflict, so you can treat it as an upsert.- The group must be actively linked. You can only override a task whose task group is currently attached to that assistant. Unlink the group and the override has nothing to apply to.
- The override replaces the base instructions completely. It is not appended or merged. Structural fields such as task name, order, tools, and outputs still come from the shared task.
nullis not a write operation. The request requires a non-empty string. UseDELETE /assistants/{id}/tasks/{taskId}/overrideto restore the shared task instructions. A nullable value in a list response is not an instruction to suppress the task; only a non-null active override is applied.
List an assistant's overrides#
GET /assistants/{id}/tasks/overrides returns every override recorded against one assistant. data is an array:
| Field | Type | Description |
|---|---|---|
overrideId | string | The override record |
assistantId | string | The assistant the override applies to |
taskId | string | The task whose instructions are overridden |
taskgroupId | string | The group that task belongs to |
instructionOverride | string | null | The replacement instruction text |
curl https://core-api.heysadie.ai/assistants/0195f1e2-2222-7000-8000-000000000000/tasks/overrides \
-H "Authorization: ApiKey YOUR_API_KEY"{
"success": true,
"data": [
{
"overrideId": "0195f1e2-9999-7000-8000-000000000000",
"assistantId": "0195f1e2-2222-7000-8000-000000000000",
"taskId": "0195f1e2-6666-7000-8000-000000000000",
"taskgroupId": "0195f1e2-5555-7000-8000-000000000000",
"instructionOverride": "Ask how many guests will be dining, then ask whether they would prefer indoor or outdoor seating."
}
],
"metadata": {
"timestamp": "2026-07-30T12:30:00.000Z",
"version": "1.0",
"path": "/assistants/0195f1e2-2222-7000-8000-000000000000/tasks/overrides"
}
}taskId is the join key. Pair this response with GET /assistants/{id}/taskgroups (as the Node.js snippet's byTaskId map does) to show, task by task, which steps this assistant runs as shared and which it runs its own way.
Remove an override#
DELETE /assistants/{id}/tasks/{taskId}/override deletes the override record for that assistant and task, leaving the assistant with the group's shared task as it stands.
curl -X DELETE https://core-api.heysadie.ai/assistants/0195f1e2-2222-7000-8000-000000000000/tasks/0195f1e2-6666-7000-8000-000000000000/override \
-H "Authorization: ApiKey YOUR_API_KEY"The response is the standard envelope with success: true; the specification defines no payload for data. Confirm the removal with GET /assistants/{id}/tasks/overrides: the entry for that taskId is gone.
Next steps#
- Task groups: what a flow is for and when to reach for one, without the API detail.
- Create and configure an assistant: where
taskDrivenModeis set, alongside the rest of the assistant's configuration. - Prompts: the standing instructions that run alongside a flow.
- Task Groups API reference: every field, parameter and status code, generated from the specification.