> ## 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 or close on one lead. It


> replaces the lead's open suggestion and withdraws any card that suggestion filed.

There is deliberately no MCP tool for this: a hold or close 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-09-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 or close on one lead. It
      description: >
        replaces the lead's open suggestion and withdraws any card that
        suggestion filed.


        There is deliberately no MCP tool for this: a hold or close 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) or close (stop
                    working it).

                    Required.
                  type: string
                note:
                  title: |
                    Note says why, in the desk's words. At most 2000 characters.
                  type: string
                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
                  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
                  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.LeadFacts:
      description: |
        means the lead has not told us.
      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 the evaluation understood about the lead. A nil field
      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 a handoff's delivery. Nothing else happens when a handoff
        is carried out — deliverHandoff records the literal string "accepted"
        and sends nothing — so a surface that wants to say what a handoff DID
        has to read this and not the row's execution ref. 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_...).

````