---
title: "Create agent plan"
method: POST
path: "/api/agent-plans"
tags: ["agent_plans"]
---

# Create agent plan

`POST /api/agent-plans`

## Request

- Base URL: `https://beta.getsales.io/unibox/v1`
- URL: `https://beta.getsales.io/unibox/v1/api/agent-plans`
- Auth: HTTP bearer and API key in header `Team-SID`

Commit the plan a strategize run produced for one thread: steps [{kind, params}] in order, 1..8, a message step's params carrying its text, each kind's payload in the params describe. ACCESS: the agent-extended toolset only; a user or public token is 403 forbidden (agent_toolset_only); a human approves or edits one with update_agent_plan.

It supersedes the plan the thread held and points the thread at this one, in one transaction; the agent's unsent messages there are withdrawn (the plan carries its own). Ids and step states are server-assigned. The requested state is a ceiling: under copilot the plan lands proposed, every step parked for approval. A run commits one plan: a second create by the same run answers 200 already_exists with the plan it made. A turn whose run no longer stands commits nothing (the lead wrote again, or the run was settled): 409 plan_turn_superseded, end the turn.

Steps are frozen here, texts included: each executes with no further LLM call, and a send_message step sends its text as written through the send door under the plan's approval (422 rule required without one). Write the reply; when a reply is expected end with a wait_for_reply, the follow-up with its own text, and a last wait_for_reply. A stage move sets the linked person to the outcome stage the step names; a people search starts a credit-billed search at that person's company; on a thread linked to no person both are skipped when due.

Contract:
- MCP tool `create_agent_plan`, registry package `mcp.unibox/agent_plans`, mount `unibox.agent`.
- Operation `create`, response envelope `create`.
- Flags: none.

## Request body

- CreateAgentPlanRequest: Request body of `create_agent_plan`.
  - `conversation_sid` string, required: The thread being planned for. Its previous live plan is superseded in the same transaction and conversations.active_plan_sid is re-pointed here.
  - `goal` string, required: The plan's intent in one human-readable line, 1..255 chars. It is what the thread and the context panel render.
  - `state` 'proposed' | 'active': Default proposed. The mode gate is server-side, so a plan asked for as active under copilot is still parked for approval, and a post-goal plan, or one that carries a reach_objective step, is floored to proposed whatever is sent.
  - `behavior` 'answer_interested' | 'meeting_request_book' | 'answer_info_request' | 'referral_capture' | 'not_now_defer' | 'ooo_delay_ping' | 'auto_reply_skip' | 'wrong_person_referral' | 'reschedule_meeting' | 'cancel_meeting' | 'return_to_orchestration' | 'close_not_interested' | 'handle_objection' | 'stoplist_do_not_contact' | 'vendor_pitch_skip' | 'acknowledge_low_signal' | 'followup_no_reply' | 'signal_followup' | 'followup_after_person', nullable: The built-in competence module that planned; null when a matched custom scenario drove the plan on its own.
  - `behavior_version` integer, nullable: Version of that module, taken from its manifest. Stamped for audit, never interpreted.
  - `custom_scenario_sid` string, nullable: The custom scenario (ub_sc_…) that matched this reply, when one did.
  - `run_sid` string, required: The strategize run committing this plan (ub_rn_…). It is the audit anchor: the plan is later explained through this run.
  - `steps` object[], required: The ordered steps, 1 to 8. Ids and step states are assigned server-side; a plan born proposed parks every step as awaiting_approval.
    - `kind` 'send_message' | 'wait_until' | 'wait_for_reply' | 'create_task' | 'move_stage' | 'find_people' | 'react' | 'like_post' | 'reach_objective' | 'resume_orchestration' | 'stoplist_person' | 'use_playbook', required: What this step should do. Validated against the behavior module's allowed kinds.
    - `params` object, nullable: The step payload, frozen here; omit or send {} where the kind takes nothing. send_message {intent, text, subject}: intent is the message's direction in a sentence; text is required and sent as written (422 on steps.N.params.text: rule required, max 8000, reveals_tracking for a text telling the lead what our tracking saw); subject is an email's, optional, max 998; the approval approves the text, no draft round. A message step passes the playbook's purpose, link and length through (each from its dictionary, 422 rule in); with link always the text must carry the goal's link (rule booking_link_missing; a thread with no link is 422 on params.link, rule no_booking_link). wait_until {until, reason}; wait_for_reply {timeout_days}. create_task {title, due_in_days}. move_stage {target_stage_key}: required, one of soft_interest, strong_interest, meeting_requested, meeting_scheduled, closed_won, wrong_decision_maker, not_now, unresponsive, not_interested, disqualified (422 rules required, in). find_people {prompt (1..2000), per_company_limit (1..25, default 5)}: company_sid, company_name and estimated_credits are the server's stamp; 422 on steps.N.kind (not_linked, no_company, no_company_anchor, person_not_found, playtables_unavailable) when the search cannot run or price. react {emoji (one emoji, rules required, max), message_sid? (a LinkedIn message of theirs here, rule message_not_reactable; absent = their newest; no_message when they have none)}. like_post {} (not_on_linkedin on a thread with no LinkedIn account, no_linkedin_profile when the lead has none). reach_objective {} (rules no_dialog, objective_already_reached). resume_orchestration {reason}; stoplist_person {}. use_playbook {from}: the stage the Playbook enters the dialog at, one of engage, discover, bridge, offer, after_yes (422 rule in; discover when none is named); once per plan (422 on steps.N.kind, rule playbook_twice); from it the Playbook's turns write the replies, the plan none.

## Response `200`

`create` success envelope.

- CreateAgentPlanResponse
  - `success` true, required
  - `operation` 'create', required
  - `item` object, required
    - `sid` string, required: Agent plan sid (ub_pl_…).
    - `team_sid` string, required: Owning team (ts_tm_…, external - gs.service.id).
    - `agent_sid` string, nullable, required: The AI SDR the plan was made by (ub_ag_…, block 10, 2026-10-30); null on a row from before.
    - `conversation_sid` string, required: The thread this plan belongs to (ub_cv_…). At most one plan per thread is live (proposed or active).
    - `state` 'proposed' | 'active' | 'completed' | 'superseded' | 'canceled', required: proposed = waiting for one-click approval, no step runs; active = executing or waiting on a timer; completed / superseded / canceled are terminal.
    - `goal` string, required: Human-readable intent of the plan, 1..255 chars, e.g. "Re-engage after OOO on Aug 5".
    - `steps` object[], required: Ordered steps, 1..8. They run in order; a failed one stops the plan.
      - `id` integer, required: Plan-local step id, stable across edits. Assigned by the server, counting from 1.
      - `kind` 'send_message' | 'wait_until' | 'wait_for_reply' | 'create_task' | 'move_stage' | 'find_people' | 'react' | 'like_post' | 'reach_objective' | 'resume_orchestration' | 'stoplist_person' | 'use_playbook', required: What the step does.
      - `params` union, required: The step payload, frozen at planning time. Per kind: send_message { intent, text, subject }, the message's direction and, since 2026-10-05, its text exactly as it goes out (an email's subject when it needs one): approving the plan approves the text and the step sends it as written, while a step with no text has its message written when it comes due; wait_until { until, reason }; wait_for_reply { timeout_days, until }, until stamped by the server when the wait begins; a message step also carries the playbook's three product parameters since 2026-10-12, purpose (reply, follow_up, nudge, closing, holding), link (none, when_natural, always: the text carries the effective booking link, the campaign's or the table's AI SDR setup over the team's) and length (short, normal, full_picture), and the booking link is a send_message with link always, the booking-link kind retired and its stored plans rewritten; create_task { title, due_in_days }; move_stage { target_stage_key }, one of soft_interest, strong_interest, meeting_requested, meeting_scheduled, closed_won, wrong_decision_maker, not_now, unresponsive, not_interested, disqualified; find_people { prompt, per_company_limit, company_sid, company_name, estimated_credits }, where the last three are stamped by the server (the company the search runs on and the credits it is expected to spend; a step the CRM could not price is refused, never stored); react { emoji, message_sid }, the sender profile's reaction on the LinkedIn message of theirs the step names (their newest when the writer named none): the agent plans it on the lead's first reply and when the stage move takes them forward in the pipeline, and on a later reply only when the lead made an effort in it (the heart on a warm reply, a thumbs-up on any other; never on an automatic reply or a do-not-contact); like_post {}, the sender profile's Like on the lead's own latest LinkedIn post, picked when the step runs (no plain repost, at most 90 days old, never one the account liked already); reach_objective {}, the mark of the live dialog's objective, run once the plan is approved; resume_orchestration { reason }; stoplist_person {}; use_playbook { from, outcome }, the stage the Playbook starts the dialog at (engage, discover, bridge, offer, after_yes; 2026-10-19) and, once the step is done, how the Playbook's lead ended (said_yes, booked, signed_up, registered, stalled_out, handed, left, cap).
        - object
        - unknown[]
          - unknown
      - `state` 'pending' | 'awaiting_approval' | 'waiting' | 'done' | 'skipped' | 'failed', required: Where this step stands.
      - `executed_at` string, nullable, required: ISO 8601 UTC, stamped when the step reached a terminal state; null before that.
      - `cause` string, nullable, required: Why the step was skipped or failed, or what a pending step waits for, as a reason token (snake_case, 64 chars at most, never a sentence: say it in your own words); null on a step in any other state, and on one that ended before 2026-10-02. A pending step carries one only while the turn that writes its message is held because the message could not leave now: sender_unavailable (no sender profile carries the account, or the account is signed out, disconnected, stopped or paused; nothing is lost, the step runs by itself once the account can send, so point the user at the sender profile; after seven days of waiting the step fails under the same token), subscription_required (the workspace is on the billing hold). A skipped step: not_linked (the thread is linked to no person: move_stage, find_people, resume_orchestration); already_beyond, stage_unknown, person_gone, playtables_refused, target_stage_key_unknown (move_stage: the person already stands further along the pipeline than the stage and the agent never moves a person back, the team has no stage under the key, the person is gone from the CRM, the CRM refused, the step names no outcome stage); no_company, no_company_anchor, person_not_found, prompt_missing (find_people); sender_unavailable, message_not_reactable, linkedin_refused, gtm_unavailable, gtm_bridge_unavailable, gtm_busy, step_job_failed (react: the account cannot act, the message takes no reaction, LinkedIn said no, gtm-api did not answer or stayed busy, the job that runs the reaction died; a reaction never fails, whatever keeps it from landing is a skip and the plan goes on); no_recent_post, already_liked, not_on_linkedin, no_linkedin_profile, post_not_likeable, gtm_scope_missing, gtm_access_lost, and, as for a reaction, sender_unavailable, linkedin_refused, gtm_unavailable, gtm_busy, step_job_failed (like_post: the lead shows the account no recent post of their own (a plain repost or a post older than 90 days does not count), the account liked their latest post already, the thread runs on no LinkedIn account, the lead has no profile the account can open, LinkedIn takes no Like on the post, the workspace connection lacks the engagements permission or no longer shows the account; a Like never fails either, it is skipped and the plan goes on); no_booking_link (send_booking_link: the team lost its link after planning); no_dialog, objective_already_reached (reach_objective); no_step_transport (stoplist_person: the block itself was applied when the reply was read). A failed step carries the error code of the run that died on it: playtables_unreachable (the CRM did not answer through three tries), no_message_sent (the turn wrote no message), turn_timeout, budget_exceeded, runner_error; or the reason the send door refused the step's message with: stoplisted, agent_disabled_on_thread (refused for good, the turn is not tried again), send_refused (the door named no reason, usually the thread has no account on the channel), sender_unavailable or subscription_required (the step waited for its sender for seven days, or the account dropped on each of three tries). unknown stands for a cause that was not a token. The list is open: read a token you do not know as "it could not run".
    - `created_by_run_sid` string, required: The strategize run that produced this plan (ub_rn_…). Its reason, confidence and LLM log ref answer "why this plan".
    - `behavior` 'answer_interested' | 'meeting_request_book' | 'answer_info_request' | 'referral_capture' | 'not_now_defer' | 'ooo_delay_ping' | 'auto_reply_skip' | 'wrong_person_referral' | 'reschedule_meeting' | 'cancel_meeting' | 'return_to_orchestration' | 'close_not_interested' | 'handle_objection' | 'stoplist_do_not_contact' | 'vendor_pitch_skip' | 'acknowledge_low_signal' | 'followup_no_reply' | 'signal_followup' | 'followup_after_person', nullable, required: The built-in competence module that planned; null when a matched custom scenario drove the plan on its own.
    - `behavior_version` integer, nullable, required: Version of that module at planning time (the audit stamp, rendered as "answer_interested v3"); null when behavior is null.
    - `custom_scenario_sid` string, nullable, required: The custom scenario that matched (ub_sc_…), which takes precedence over the built-in behavior; null when none did. The sid survives the scenario being deleted.
    - `approved_by_user_sid` string, nullable, required: User that approved a proposed plan; null while unapproved or when the plan was born active under autopilot.
    - `created_at` string, required: ISO 8601 UTC.
    - `updated_at` string, required: ISO 8601 UTC.
    - `deleted_at` string, nullable, required: ISO 8601 UTC. Always null in practice: plans have no delete path, the terminal states cover removal.
  - `already_exists` boolean, required
  - `meta` object, required
    - `trace_id` string, required: UUID v7; same 128-bit value as the X-Trace-Id header.
    - `span_id` string, required: 16 hex chars, root span of this request.
    - `timestamp` string, required: ISO 8601 UTC (Y-m-dTH:i:sZ), response time.
    - `duration_ms` integer, required: Server-side wall clock.
    - `team_sid` string, nullable: The team this call ran in (the token team, or the team_sid override). Null when unauthenticated; absent from pre-2026-08-20 backends.
    - `actor_type` string, nullable: user | agent | api_key | system. Null when unauthenticated; absent from pre-2026-08-20 backends.

## Other responses

- `4XX`: MCP error envelope. `error.code` is one of validation_failed, nothing_to_update, not_found, relation_not_found, invalid_transition, limit_exceeded, payment_required, duplicate_rejected, conflict, delete_blocked, unauthorized, forbidden, rate_limited, not_implemented.
- `5XX`: MCP error envelope with `error.code` internal_error or service_unavailable.

---

[API](https://skmtc.dev/getsales/apis/getsales-public-contract-gs-service-unibox.md) - [All operations](https://skmtc.dev/getsales/apis/getsales-public-contract-gs-service-unibox/llms.txt) - [OpenAPI document](https://skmtc.dev/getsales/apis/getsales-public-contract-gs-service-unibox/revisions/8f1bc317897f?raw)
