> ## Documentation Index
> Fetch the complete documentation index at: https://docs.erdo.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# RecordLeadNextActionAPI records a person's hold, close or start on one lead.


> A hold or close replaces the lead's open suggestion and withdraws any card that suggestion filed. A start has the follow-up engine work the lead now — one captured before the engine was switched on, or test traffic — and with reset starts the lead over.

There is deliberately no MCP tool for this: a hold, close or start is the sales desk's call about a person it is talking to. A scoped API key is refused.




## OpenAPI

````yaml /api/openapi.json post /v1/datasets/{datasetSlug}/leads/{leadRef}/next-actions
openapi: 3.0.0
info:
  title: Erdo API
  description: >-
    Erdo's REST API: query and write datasets, run agents, manage threads,
    integrations, pages, evals, workstreams, experiments, and bounded outreach.
    Authenticate with a Bearer API key (erdo_api_...) or scoped token
    (erdo_token_...).
  version: '2026-10-22'
servers:
  - url: https://api.erdo.ai
    description: Production
security:
  - bearerAuth: []
paths:
  /v1/datasets/{datasetSlug}/leads/{leadRef}/next-actions:
    post:
      summary: >
        RecordLeadNextActionAPI records a person's hold, close or start on one
        lead.
      description: >
        A hold or close replaces the lead's open suggestion and withdraws any
        card that suggestion filed. A start has the follow-up engine work the
        lead now — one captured before the engine was switched on, or test
        traffic — and with reset starts the lead over.


        There is deliberately no MCP tool for this: a hold, close or start is
        the sales desk's call about a person it is talking to. A scoped API key
        is refused.
      operationId: POST:mcp.RecordLeadNextActionAPI
      parameters:
        - allowEmptyValue: true
          explode: false
          in: path
          name: datasetSlug
          required: true
          schema:
            type: string
          style: simple
        - allowEmptyValue: true
          explode: false
          in: path
          name: leadRef
          required: true
          schema:
            type: string
          style: simple
      requestBody:
        content:
          application/json:
            schema:
              properties:
                decided_by_external_ref:
                  title: >
                    DecidedByExternalRef is YOUR OWN handle for the person who
                    held or closed

                    this lead — a member id, a slug, whatever your records use.
                    Erdo stores it

                    verbatim beside the account your API key belongs to, and
                    never interprets it.

                    It exists because one organization API key usually serves a
                    whole sales desk,

                    so without it every hold and close is attributed to the
                    key's owner.

                    ATTRIBUTION, NEVER AUTHENTICATION: anything holding the key
                    can send any value,

                    Erdo verifies nothing, and it must never be presented as
                    proof of who acted.

                    At most 200 characters.
                  type: string
                kind:
                  title: >
                    Kind is hold (the desk is handling this lead), close (stop
                    working it) or

                    start (have the follow-up engine work this lead). The engine
                    only reaches leads

                    captured after it was switched on, and never test traffic,
                    by itself; start is

                    how a person brings one of those in. Start answers
                    FailedPrecondition when the

                    engine is switched off, or when it already has the lead and
                    reset is not set.

                    Required.
                  type: string
                note:
                  title: |
                    Note says why, in the desk's words. At most 2000 characters.
                  type: string
                reset:
                  title: >
                    Reset, with kind start, starts the lead over: open decisions
                    are withdrawn and

                    the engine reads the lead afresh from now — its earlier
                    decisions, the desk's

                    refusals, the timeline and anything the lead said in a
                    conversation before now

                    are not read, and the lead is treated as just captured.
                    Nothing is deleted,

                    and opt-outs (STOP, do-not-call) still apply.
                  type: boolean
                until:
                  format: date-time
                  title: >
                    Until ends a hold, RFC 3339. Defaults to seven days; at most
                    180 days. The lead

                    is also reopened when it writes back.
                  type: string
              type: object
              required:
                - kind
        required: true
      responses:
        '200':
          content:
            application/json:
              schema:
                properties:
                  accompanying_email:
                    $ref: '#/components/schemas/leadaction.AccompanyingEmailState'
                  action_input:
                    type: object
                  action_kind:
                    type: string
                  approval_request_id:
                    type: string
                  call_outcome:
                    $ref: '#/components/schemas/leadaction.CallOutcome'
                  canonical_lead_id:
                    type: string
                  created_by:
                    type: string
                  dataset_id:
                    type: string
                  decided_at:
                    format: date-time
                    type: string
                  decided_by:
                    type: string
                  decided_by_external_ref:
                    description: >
                      handles for the person who decided this action and the
                      person who took that decision back — opaque strings Erdo
                      stores verbatim and never interprets.


                      They exist because an application built on \`/v1\`
                      authenticates a whole team with ONE organization API key,
                      so DecidedBy — the Erdo account that key belongs to — is
                      the same person for every decision anybody there makes.
                      The caller is the only party that can say which of its
                      users acted, so it says, and it resolves these back to
                      names on its own side.


                      ATTRIBUTION, NEVER AUTHENTICATION. Anything holding the
                      organization's key can write any value here; Erdo does not
                      verify it, must never branch on it, authorize by it, or
                      resolve it against an Erdo identity. It is fine for
                      "Approved by Ana" on a card and must never be presented as
                      proof of who acted. Empty means the caller offered no
                      name, which is every decision made before this existed.
                    title: >
                      DecidedByExternalRef and UndoneByExternalRef are the
                      CALLING PRODUCT's own
                    type: string
                  due_at:
                    format: date-time
                    type: string
                  error:
                    type: string
                  evaluated_at:
                    format: date-time
                    type: string
                  executed_at:
                    format: date-time
                    type: string
                  execution_ref:
                    type: string
                  facts:
                    $ref: '#/components/schemas/leadaction.LeadFacts'
                  handoff_alert:
                    $ref: '#/components/schemas/leadaction.HandoffAlertState'
                  id:
                    type: string
                  lead_reference:
                    type: string
                  mode:
                    type: string
                  model:
                    type: string
                  playbook_revision:
                    format: int64
                    type: integer
                  policy_digest:
                    description: >
                      is what says whether the decision is stale — not the
                      revision number. The revision bumps on every save, so a
                      decision at revision 9 may have been decided under exactly
                      the rules revision 10 carries; comparing it with
                      LeadPlaybook.PolicyDigest answers the question the
                      revision only hints at. Empty on every decision written
                      before digests existed and on one a person recorded by
                      hand. See policy.go for the two halves it is made of.
                    title: >
                      PolicyDigest identifies the playbook this decision was
                      made under, and it
                    type: string
                  priority:
                    type: string
                  priority_reason:
                    type: string
                  rationale:
                    type: string
                  resumed_at:
                    description: >
                      handed the lead back to the engine (POST
                      .../next-actions/resume). The revisit time is set to the
                      same moment, so the engine decides for the lead on its
                      next pass. Absent on every decision that was never resumed
                      — a hold whose time simply ran out has none.
                    format: date-time
                    title: >
                      ResumedAt is when somebody at the desk ended this hold or
                      close early and
                    type: string
                  resumed_by:
                    title: |
                      ResumedBy is the Erdo account that resumed it.
                    type: string
                  resumed_by_external_ref:
                    description: >
                      who resumed it. Opaque, unverified, stored verbatim —
                      attribution, never authority, exactly as
                      DecidedByExternalRef.
                    title: >
                      ResumedByExternalRef is the calling product's own handle
                      for the person
                    type: string
                  revisit_at:
                    format: date-time
                    type: string
                  rule:
                    type: string
                  source:
                    type: string
                  stage:
                    type: string
                  status:
                    type: string
                  undone_by_external_ref:
                    type: string
                type: object
          description: Success response
        default:
          $ref: '#/components/responses/APIError'
components:
  schemas:
    leadaction.AccompanyingEmailState:
      description: >
        out beside a gated decision: whether it went, when, in what words, and
        why it did not if it did not.


        It joins the row's own columns (the authoritative state) with the draft
        stored in action\_input, so a caller does not have to read two things
        and work out which one is true.
      properties:
        body_markdown:
          type: string
        error:
          title: |
            Error is why it has not gone, in the words a person reads.
          type: string
        execution_ref:
          title: |
            ExecutionRef is the mailer's id for the email that went.
          type: string
        sent_at:
          format: date-time
          type: string
        status:
          title: >
            Status is pending, sending, sent, failed or skipped. See the
            constants.
          type: string
        subject:
          type: string
        to:
          type: string
      title: >
        AccompanyingEmailState is what a surface needs to render the email that
        went
      type: object
    leadaction.CallOutcome:
      properties:
        dial_outcome:
          description: >
            it: answered | voicemail | no\_answer | busy | failed | canceled. It
            is the field to read for "did they pick up": Status reads
            "completed" for a call a voicemail box answered exactly as for one a
            person did. Empty on a call still in flight and on calls recorded
            before agent calls carried one.
          title: >
            DialOutcome is what became of the lead's leg, when the call record
            states
          type: string
        duration_seconds:
          format: int64
          type: integer
        status:
          description: >
            connected — to a person or to a voicemail box, see DialOutcome —
            "no-answer", "busy", "failed" and so on otherwise, and a ringing or
            in-progress status while the call has not ended.
          title: |
            Status is the call record's status: "completed" for a call that
          type: string
      title: >
        CallOutcome is how one placed call ended, in the call record's own
        words.
      type: object
    leadaction.LeadFacts:
      description: >
        read nothing.


        A decision now carries only Language, the language the lead wrote in,
        which is the engine's own reading. What the lead stated is read from the
        lead's details instead. The other fields are filled only on decisions
        made before the engine read details.
      properties:
        bedrooms:
          $ref: '#/components/schemas/leadaction.LeadFact'
        broker:
          $ref: '#/components/schemas/leadaction.LeadFact'
        budget:
          $ref: '#/components/schemas/leadaction.LeadFact'
        financing:
          $ref: '#/components/schemas/leadaction.LeadFact'
        language:
          $ref: '#/components/schemas/leadaction.LeadFact'
        purpose:
          $ref: '#/components/schemas/leadaction.LeadFact'
        timeline:
          $ref: '#/components/schemas/leadaction.LeadFact'
      title: >
        LeadFacts is what an evaluation read about the lead. A nil field means
        it
      type: object
    leadaction.HandoffAlertState:
      description: >
        desk a handoff is waiting: whether it went, when, the message to open,
        and why it did not go if it did not.


        The alert is the only MESSAGE a handoff produces. Carrying a handoff out
        is a person marking it done, which records the literal string "accepted"
        and sends nothing — so a surface that wants to show what a handoff SENT
        has to read this and not the row's execution ref, and one that wants to
        know whether the desk has dealt with the lead reads the row's status. It
        is also why these facts are exposed at all: a sent alert is a real,
        openable message, and the timeline used to show a handoff as an
        unopenable line that mentioned no email whatever.


        It reads only the row's own columns, unlike AccompanyingEmailState,
        because the alert's words are not drafted onto the decision — they are
        composed at send time from the card by buildHandoffAlert. The message
        itself is read back from the email service by ExecutionRef, which is the
        only copy of it there is.
      properties:
        error:
          description: |
            a person reads.
          title: >
            Error is why it has not gone, or which recipients it missed, in the
            words
          type: string
        execution_ref:
          description: >
            it. One alert is one message per recipient and this is the first
            copy that sent; every copy carries the same words, so it opens what
            the desk read. Empty on an alert that has not gone, and on one sent
            before the id was recorded (migration 013) — in which case there is
            nothing to open and a surface must not pretend otherwise.
          title: >
            ExecutionRef is the email service's id for the alert, which is what
            opens
          type: string
        sent_at:
          format: date-time
          type: string
        status:
          description: |
            constants in handoff\_alert.go.
          title: |
            Status is pending, sending, sent or failed. See the HandoffAlert*
          type: string
      title: >
        HandoffAlertState is what a surface needs to render the alert that told
        the
      type: object
    leadaction.LeadFact:
      description: |
        read it from, so a person can check the reading against the row.
      properties:
        source_column:
          type: string
        value:
          type: string
      title: >
        LeadFact is one thing the evaluation read about the lead and the column
        it
      type: object
  responses:
    APIError:
      content:
        application/json:
          schema:
            externalDocs:
              url: https://pkg.go.dev/encore.dev/beta/errs#Error
            properties:
              code:
                description: Error code
                example: not_found
                externalDocs:
                  url: https://pkg.go.dev/encore.dev/beta/errs#ErrCode
                type: string
              details:
                description: Error details
                type: object
              message:
                description: Error message
                type: string
            title: APIError
            type: object
      description: Error response
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: An Erdo API key (erdo_api_...) or scoped token (erdo_token_...).

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.