---
title: "Update agent plan"
method: PATCH
path: "/api/agent-plans/{sid}"
tags: ["agent_plans"]
---

# Update agent plan

`PATCH /api/agent-plans/{sid}`

## Request

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

The human verb on a plan, four intents in one call: approve it (state active), stop it (state canceled), rewrite its steps (texts included), or end its counting wait now (end_wait). The agent's own token is 403 forbidden (human_verb_only). A step edit replaces only what has not run; done, skipped or failed steps are locked (422). 409 plan_not_editable once completed, superseded or canceled; 409 plan_rewrite_in_flight while steer_agent_plan's rewrite moves.

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

## Path parameters

- `sid` string, required: Agent plan sid (ub_pl_…).

## Request body

- UpdateAgentPlanRequest: Request body of `update_agent_plan`.
  - `state` 'active' | 'canceled': active approves a proposed plan (stamps approved_by_user_sid, releases the parked steps and arms the first one); canceled stops it and hands the thread back. Nothing else is accepted: completed and superseded are system-owned and rejected.
  - `steps` object[]: Replaces the NON-TERMINAL tail of the plan, 1 to 8 entries. Done, skipped or failed steps stay untouched and may not appear; waits re-arm on the new values. Allowed while proposed or active. Leaving out a follow-up and its reply wait skips that follow-up.
    - `id` integer, nullable: The id of an existing non-terminal step to keep; omit (or send null) for a new step, which gets a fresh id. Sending the id of a done, skipped or failed step is a 422: terminal steps are locked.
    - `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 does. Required on every entry whenever steps is sent.
    - `params` object, nullable: The per-kind shapes and rules of create, replaced wholesale: carry forward the keys to keep. A person may leave a message's text out (the agent writes it when due) or write it as they like, max 8000; a step sent back as stored is left as it is.
  - `end_wait` boolean: true: the wait the active plan is counting down ends now, so what follows goes at once, through the follow-up caps a wait that ran out meets. 409 no_wait_to_end when the next step is not a wait counting down. Runs after the other fields.

## Response `200`

`update` success envelope.

- UpdateAgentPlanResponse
  - `success` true, required
  - `operation` 'update', 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.
  - `updated_fields` string[], required
  - `previous_values` object, required
  - `kept_fields` string[]: The fields this write did not change because the person's own value stands on them (stamped person); read the item for what landed.
  - `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)
