Skill Goals
Skill goals are part of the Conversational Agents Beta.
A goal turns a skill into a guided task: the agent collects a defined set of answers, SendSeven validates each answer, saves the valid ones to the contact, and runs your completion actions. Typical uses: qualify a lead, take a callback request, register a return, collect feedback.
- The goal is stored on the skill version. Changing it creates a new version, and pinned agents keep their old goal.
- Validation is done by SendSeven, not by the model. The agent submits answers; only answers that pass your rules are accepted and saved. The model never writes contact fields directly.
- Goals work on every surface: channels, Live Chat, email, Flows and the test console.
Add a goal to a skill
Send goal when you create or update a skill (or in a skill import):
{
"slug": "qualify-lead",
"title": "Qualify a lead",
"description": "Use when a visitor asks about pricing, a demo or buying for a team.",
"default_mode": "model_selected",
"body": "Be friendly and brief. Ask one question at a time.",
"goal": {
"objective": "Find out whether the visitor is a good fit for a sales call.",
"completion_criteria": "We know the company size, the budget and the email address.",
"outputs": [
{"key": "company_size", "label": "Company size", "type": "select", "options": ["1-10", "11-50", "51-200", "200+"],
"bind": {"kind": "custom_field", "field_id": "CUSTOM_FIELD_ID"}},
{"key": "budget", "label": "Monthly budget (EUR)", "type": "number", "constraints": {"min": 0, "max": 1000000}},
{"key": "email", "bind": {"kind": "contact_standard", "field": "email"}},
{"key": "website", "type": "url", "required": false}
],
"on_max_attempts": "handoff",
"actions": {
"save_contact_fields": {"enabled": true, "overwrite_existing": false},
"assign": {"strategy": "round_robin_by_tag", "tag_id": "TAG_ID_SALES", "add_tag_ids": ["TAG_ID_LEAD"], "prefer_online": true},
"webhook": {"enabled": true},
"after": {"type": "none"}
}
}
}
Send "goal": null on PATCH to remove the goal; omit it to keep the current one.
Goal fields
| Field | Notes |
|---|---|
objective | Required. What the agent should achieve. 1-2,000 characters. |
completion_criteria | Optional. When the goal counts as done. Max 2,000. |
outputs | Up to 20 answers to collect. Can be empty for an objective-only goal. |
on_max_attempts | What happens when an answer is still invalid after max_attempts: handoff (default) hands over to a human, skip leaves it empty and moves on. |
actions | Completion actions. |
Outputs
| Field | Notes |
|---|---|
key | Required. ^[A-Za-z][A-Za-z0-9_]{0,63}$. Unique (case-insensitive). The key in webhooks and Flow variables. |
label | Max 100. |
description | Max 500. Help for the agent, for example what to ask. |
required | Default true. |
type | text, number, integer, boolean, date, datetime, email, phone, url, select, multiselect. Bound outputs take the type of their field; an unbound output without a type is text. |
options | For select and multiselect. Up to 100 non-empty strings, max 255 characters each. |
bind | Where the answer is saved. See Binding. |
constraints | Extra validation rules. See Constraints. |
max_attempts | 1..5, default 3. Invalid answers allowed before on_max_attempts applies. |
Binding to contact fields
bind | Saves to |
|---|---|
{"kind": "contact_standard", "field": "…"} | A standard contact field: first_name, last_name, name, language (text), birthday (date), email, phone |
{"kind": "custom_field", "field_id": "…"} | An active custom field in your workspace |
- A bound output uses the field's type. Declaring a different
typereturnstype_mismatch. - One field can be bound to only one output.
- A bound
select/multiselectoutput may restrictoptionsto a subset of the field's options. - Location and collection custom fields cannot be bound.
- The custom field's own validation rules always apply.
Constraints
| Key | Types |
|---|---|
min, max | number, integer |
min_length, max_length (0..10000) | text, email, phone, url |
pattern (1-200 characters, a safe regular expression) | text, email, phone, url |
date_min, date_max (ISO date or "today") | date, datetime |
boolean, select and multiselect take no constraints. For a bound output, constraints may only tighten the field's own rules: a wider range, a longer maximum or a different pattern returns constraint_loosens.
How the agent works with a goal
- The goal becomes active when its skill is loaded (by the classifier, by
load_skill, as a Flow skill, or as an always-on skill). - The agent asks for the answers and submits them with
submit_goal_fields. Each answer is normalised and validated; invalid answers come back with an error code (for exampletoo_smallorinvalid_email) so the agent can ask again. - The agent finishes with
complete_goal. This succeeds only when every required output was accepted or skipped. A goal without outputs can be completed after at least 1 contact message following activation, or at least 2 when the goal has completion actions. - The completing turn has the outcome
goal_completed(task_completedinside a Flow), and the completion actions run.
More rules:
- One active goal at a time. Loading another skill with a goal pauses the current one. Up to 3 paused goals are kept and resume when their skill is loaded again.
- The active goal's skill stays loaded on the following turns, unless the conversation moves to another topic.
- Goal tools are free and get 2 extra tool calls on top of
max_tool_calls. - Goal progress is kept per conversation. In the test console it travels in the
stateyou send back (key_agent_goal).
complete_goal errors the agent may receive: no_active_goal, goal_unavailable, required_fields_missing (with remaining_required), too_early.
Completion actions
Actions run once per goal activation, in live conversations only, in this order:
| Action | Fields | Effect |
|---|---|---|
save_contact_fields | enabled (default true), overwrite_existing (default false) | Saves the accepted answers to their bound fields. With overwrite_existing: false, a field that already has a value is left alone. |
assign | strategy, tag_id, add_tag_ids (max 10), prefer_online (default true) | strategy: none (default), round_robin_all, least_busy_all, round_robin_by_tag, least_busy_by_tag (tag_id required). Assigns an unassigned conversation; an assigned one is never reassigned. add_tag_ids are added to the conversation. |
webhook | enabled (default true) | Sends agent.goal_completed. |
after | type: none (default), handoff or start_flow; flow_id; variable_prefix (default goal, ^[A-Za-z][A-Za-z0-9_]{0,31}$) | handoff hands the conversation to a human (needs escalation on; with a *_by_tag strategy the assign.tag_id is the hand-off tag). start_flow starts a published, manually startable Flow for the conversation with the answers in {{vars.<variable_prefix>.<key>}}. |
Inside a Flow, actions run too, but after is skipped: the Flow continues on the Run AI Assistant node's done branch instead. See Agents in Flows.
In the test console nothing is executed. The engine_trace contains a goal_actions_dry_run step whose would_execute lists the fields that would be saved, the assignment, the webhook and the after action.
agent.goal_completed webhook
Subscribe your webhook endpoint to agent.goal_completed (see Webhook setup). The event uses the standard envelope:
{
"id": "evt_4f9c2a7b1d3e5f60",
"type": "agent.goal_completed",
"created_at": "2026-10-05T10:00:01Z",
"tenant_id": "…",
"event_id": "6f1c1d1e-2b3a-4c5d-8e9f-0a1b2c3d4e5f",
"data": {
"agent": {"id": "…", "name": "Sales Assistant"},
"skill": {"id": "…", "slug": "qualify-lead", "version": 3},
"conversation_id": "…",
"contact_id": "…",
"channel_type": "whatsapp",
"outputs": {"company_size": "11-50", "budget": 5000, "email": "[email protected]", "website": null},
"saved_fields": ["company_size", "email"],
"completed_at": "2026-10-05T10:00:00Z",
"flow_run_id": null
}
}
event_ididentifies the goal activation and is the same on every delivery attempt. Use it to de-duplicate.outputshas at most 20 keys. Skipped optional answers arenull. Text is cut at 4,000 characters and lists at 50 items.- Full reference: Webhook events — agent.goal_completed.
Statistics
GET /automation/skills/{skill_id}/goal-stats?days=30 returns started, completed and handed-off goals, the completion rate and a per-version breakdown. See Skills API — Goal statistics.
Errors
An invalid goal is rejected with 422:
{
"detail": {
"code": "invalid_goal",
"message": "The skill goal is invalid",
"errors": [
{"path": "outputs[1].constraints.max", "code": "constraint_loosens", "message": "…"}
]
}
}
Up to 50 errors are returned, each with the path of the problem.
code | Meaning |
|---|---|
invalid, invalid_type, invalid_value | Malformed goal, wrong type or value |
options_required, duplicate_option, options_not_applicable, options_not_subset | Problems with options |
constraint_not_applicable, min_greater_than_max, constraint_loosens | Problems with constraints |
unknown_standard_field, type_mismatch, duplicate_bind, unsupported_field_type | Problems with bind |
custom_field_not_found, custom_field_inactive | The bound custom field does not exist or is inactive |
tag_not_found | assign.tag_id or an add_tag_ids entry is not in your workspace |
flow_not_found, flow_not_published, flow_not_manually_startable | Problems with after.flow_id |
Answer validation codes the agent can receive: required, not_in_options, not_a_number, not_an_integer, too_small, too_large, too_short, too_long, pattern_mismatch, invalid_email, invalid_phone, invalid_url, invalid_date, date_too_early, date_too_late, invalid_boolean, invalid_language, unsupported.