---
title: "create_next_action: create the next action with an expected signal and a review date. Pass parentActionId to make it the child of a needs_decision action — the parent is NOT resolved automatically, close it explicitly with close-loop. Defaults verb to \"send\" when omitted. When the created action has no due_at the 201 carries warnings[] with code no_due_date: the action IS created, but with no clock it can never become overdue and the attention layer (and therefore the Telegram nudge clock) will never surface it."
method: POST
path: "/actions"
---

# create_next_action: create the next action with an expected signal and a review date. Pass parentActionId to make it the child of a needs_decision action — the parent is NOT resolved automatically, close it explicitly with close-loop. Defaults verb to "send" when omitted. When the created action has no due_at the 201 carries warnings\[\] with code no_due_date: the action IS created, but with no clock it can never become overdue and the attention layer (and therefore the Telegram nudge clock) will never surface it.

`POST /actions`

## Request

- Base URL: `https://forbiz.io/api/public/agent`
- URL: `https://forbiz.io/api/public/agent/actions`
- Auth: HTTP bearer

## Headers

- `Idempotency-Key` string

## Request body

- object
  - `description` string, required
  - `verb` 'send' | 'call' | 'ask' | 'write' | 'draft' | 'review' | 'decide' | 'schedule' | 'research' | 'create' | 'connect' | 'update' | 'prepare' | 'validate' | 'check'
  - `whyThisMove` string
  - `expectedSignal` string
  - `expectedSignalSource` union: BORN WITH A SIGNAL SOURCE (1.43.0). WHERE the expected signal will appear, as { kind, detail }: kind is one of email_thread, chat, phone_call, site_order, bank, calendar, in_person, system, other, and detail is free text (max 200 chars) identifying the place to look. OPTIONAL: when absent the 201 carries missing: ["expected_signal_source"] with one sentence saying that without it Forbiz cannot tell "nobody looked" from "looked and found nothing". NEVER inferred from the description. The legacy strings "human" and "external" are still accepted and keep their exact attention/Telegram semantics. An invalid value is refused 400 invalid_expected_signal_source carrying allowed_kinds, the full kind enum (1.60.0).
    - 'human' | 'external'
    - object
      - `kind` 'email_thread' | 'chat' | 'phone_call' | 'site_order' | 'bank' | 'calendar' | 'in_person' | 'system' | 'other', required
      - `detail` string
  - `expectedSignalAt` string, date-time
  - `dueAt` string, date-time, nullable: When this action is due. Without it the action can never become overdue and the attention layer will never surface it; the response then carries warnings[] with code no_due_date. Alias: due_at.
  - `actionType` string
  - `humanInterventionKind` 'decide' | 'execute', nullable: Declared kind of human intervention. 'decide' + actionType='strategy_checkpoint' PROPOSES a strategy checkpoint for a Mission (resolved through the action's item → mission relation); a human then approves or asks for an adjustment in Forbiz or from Telegram — this API can never approve. 'execute' means a human must perform the step. Omitted/null preserves existing behavior exactly.
  - `parentActionId` string: Required when this action is the decision taken on a parent action. root_action_id is NOT accepted as input — the server derives it as parent.root_action_id ?? parent.id and returns it in the response.
  - `personId` string
  - `organizationId` string
  - `outreachId` string
  - `campaignTargetId` string
  - `projectId` string, uuid, nullable: Optional. Anchor this action to an existing project in the credential's workspace. Must be a valid UUID for a project in the same workspace, otherwise 400/404. Omitted or null preserves the current unanchored behavior.
  - `commitmentKey` string, nullable: RECOMMENDED. A short stable id for the objective this commitment serves, e.g. "unit-13/quote-followup" (max 120 chars; alias commitment_key). ONE COMMITMENT PER OBJECTIVE has three layers: (1) with a key, a second OPEN action for the same key in the workspace is refused 409 COMMITMENT_ALREADY_OPEN, database-enforced so it holds under concurrency; (2) with no key but a declared object (campaignTargetId / personId / organizationId / projectId), the server derives and stores auto:<kind>:<id>:<action_type> and applies the same rule — with no object no key is invented; (3) otherwise a look-alike OPEN action found by the existing full-text index is refused 409 POSSIBLE_DUPLICATE, which is never a block: resend with confirm_new: true. Open means lifecycle planned/executed/waiting_for_signal and status <> skipped — resolved, closed_without_signal and skipped never refuse.
  - `roi` object, nullable: BORN WITH A VALUE (1.43.0). What this commitment is worth, each dimension an integer 0-5: cash, access, proof, narrative, relationship, learning, compliance. OPTIONAL and never inferred. When absent the response carries missing: ["roi"] and priority: { value, defaulted: true } — nothing pretends to be ranked when its inputs were never given. The existing write-time priority trigger is unchanged; ORDERING happens at READ time in GET /next and GET /state with three named terms (value, days_late, blocking) and their weights published in rank_reason on every ranked item.
    - `cash` integer
    - `access` integer
    - `proof` integer
    - `narrative` integer
    - `relationship` integer
    - `learning` integer
    - `compliance` integer
  - `confirm_new` boolean: Send true to override a POSSIBLE_DUPLICATE refusal and create anyway. Nothing is ever merged, edited or closed. It does NOT override COMMITMENT_ALREADY_OPEN.
  - `purpose` string, nullable: ACCEPTED AND IGNORED (1.56.0). `purpose` is a PROJECT field, not an action field: it is not stored on the action and it hides nothing. The action is created exactly as if the field had not been sent and it appears in the normal queues (/next, /state, /prompts, /decisions/recommendation, nudges, digest). When present the response carries purpose_ignored: true and purpose_ignored_note { code: "purpose_ignored", message }. To practise, POST /projects with purpose 'test' and create the actions inside that project.

## Response `201`

Created action

- object
  - `ok` boolean
  - `owner` Owner: Ownership of the record. Never means the performer of an operation — use /actions/{id}/history for actor semantics.
    - `type` 'human'
    - `user_id` string: Owning Forbiz user. Action payloads never repeat raw ownership UUIDs.
    - `workspace_id` string, nullable
    - `note` string
  - `action` Action: An action. Internal ownership UUIDs are never included; ownership is reported once in `owner`.
    - `id` string
    - `description` string
    - `verb` string
    - `action_type` string
    - `lifecycle_status` 'planned' | 'executed' | 'waiting_for_signal' | 'signal_recorded' | 'resolved' | 'closed_without_signal' | 'cancelled': READ-ONLY as reported. 'signal_recorded' is DERIVED (1.55.0): an action that has actual_signal is never reported as waiting_for_signal, because it is waiting for interpretation, not for a signal. lifecycle_status_stored carries the stored value.
    - `lifecycle_status_stored` 'planned' | 'executed' | 'waiting_for_signal' | 'resolved' | 'closed_without_signal' | 'cancelled', nullable: The stored column, unchanged. Write paths and filters use these values.
    - `status` string
    - `why_this_move` string, nullable
    - `expected_signal` string, nullable
    - `expected_signal_at` string, date-time, nullable
    - `expected_signal_source` 'human' | 'external' | 'null', nullable: LEGACY: AUDIENCE ONLY ('human' | 'external'). It is NOT the declared signal source and is null unless an audience was sent. What was declared as expectedSignalSource {kind, detail} is echoed as expected_signal_source_declared {kind, detail, audience} and stored in expected_signal_source_kind / expected_signal_source_detail.
    - `expected_signal_source_declared` object, nullable: What the creator declared about WHERE the signal will appear (1.55.0). kind/detail come from the stored declaration columns; audience is the legacy expected_signal_source value.
      - `kind` string, nullable
      - `detail` string, nullable
      - `audience` 'human' | 'external' | 'null', nullable
    - `human_intervention_kind` 'decide' | 'execute' | 'null', nullable: Caller-declared kind of human intervention this action needs. 'decide' = a human decision; 'execute' = a human must perform it. Never inferred from action_type. A STRATEGY CHECKPOINT is exactly human_intervention_kind='decide' AND action_type='strategy_checkpoint'; a generic 'decide' action gates nothing. Strategy approval can NEVER be written through this API: only the authenticated human UI or an explicit correlated Telegram Approve button records it.
    - `actual_signal` string, nullable: What happened in reality. Written once; never overwritten.
    - `actual_signal_at` string, date-time, nullable
    - `outcome_status` 'expected' | 'partial' | 'unexpected' | 'deferred' | 'null', nullable
    - `learning_note` string, nullable
    - `executed_at` string, date-time, nullable: When the action actually happened in the real world (may be backdated by the caller).
    - `execution_recorded_at` string, date-time, nullable: When Forbiz recorded the execution. Server-owned; a caller can never supply it.
    - `parent_action_id` string, nullable
    - `root_action_id` string, nullable
    - `outreach_id` string, nullable
    - `person_id` string, nullable
    - `organization_id` string, nullable
    - `campaign_target_id` string, nullable
    - `timing` Timing: ONE canonical derivation, computed at request time and never stored. Overdue is a single concept with two governing clocks: before execution the COMMITMENT clock (due_at) governs, after execution the REVIEW clock (expected_signal_at) governs. There is no grace period, and is_commitment_overdue can never be true while is_overdue is false. timing.due_at is the commitment clock ONLY — expected_signal_at never populates it (use timing.review_due_at).
      - `temporal_state` 'planned' | 'needs_timing' | 'waiting' | 'overdue' | 'needs_decision' | 'resolved' | 'closed_without_signal' | 'cancelled'
      - `due_at` string, date-time, nullable: The COMMITMENT clock (actions.due_at).
      - `review_due_at` string, date-time, nullable: The REVIEW clock (expected_signal_at).
      - `governing_clock` 'commitment' | 'review' | 'null', nullable
      - `governing_due_at` string, date-time, nullable: The instant of the governing clock — sort attention by this.
      - `executed_at` string, date-time, nullable: Real-world occurrence time. The ONLY timestamp temporal math uses.
      - `recorded_at` string, date-time, nullable: When Forbiz recorded the execution. Informational; never used for timing math.
      - `is_backdated_execution` boolean: True when the execution was recorded more than 5 minutes after it happened (reconciled history).
      - `evaluated_at` string, date-time
      - `seconds_until_due` integer, nullable
      - `overdue_by_seconds` integer, nullable
      - `is_overdue` boolean
      - `requires_feedback` boolean
      - `requires_decision` boolean
      - `available_operations` string[]
      - `available_commands` OperationCommand[]
        - `operation` 'mark_executed' | 'record_signal' | 'postpone' | 'set_review_date' | 'interpret_signal' | 'create_next_action' | 'close_loop' | 'close_without_signal'
        - `method` 'POST'
        - `path` string: Path relative to the API server, with {id} substituted by the action id.
        - `body` string[]: Required request body fields.
        - `resulting_state` string
        - `description` string
  - `action_id` string
  - `parent_action_id` string, nullable
  - `root_action_id` string, nullable: Server-derived chain root. Null for a root action; otherwise parent.root_action_id ?? parent.id.
  - `warnings` object[]: Non-blocking advisories. Present only when something was created that the attention layer cannot see.
    - `code` 'no_due_date' | 'no_review_date'
    - `message` string
  - `note` string

## Other responses

- `208`: Idempotent replay of a previous request with the same Idempotency-Key. Nothing was applied again.
- `409`: Refused, nothing created. code=COMMITMENT_ALREADY_OPEN carries commitment_key, existing {id, description, lifecycle_status, due_at, expected_signal, expected_signal_at} and commands (that action's timing.available_commands, executable as-is) — continue that commitment. code=POSSIBLE_DUPLICATE carries up to 3 candidates {id, description, lifecycle_status, due_at, commands}; resend with confirm_new: true to create anyway.

---

[API](https://skmtc.dev/forbiz/apis/forbiz-external-agent-api.md) · [All operations](https://skmtc.dev/forbiz/apis/forbiz-external-agent-api/llms.txt) · [OpenAPI document](https://skmtc.dev/forbiz/apis/forbiz-external-agent-api/revisions/e5b10d59dc5f?raw)
