> ## 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.

# Renders the email a declaration would produce for one sample row and delivers


> it to one nominated address, so somebody drafting a lead welcome message sees the real thing in their own inbox before it is switched on for real rows.

The message is rendered by the same template engine the live automation uses, never a second renderer. Two properties bound what this can do: the steps must be zero or more script steps followed by exactly one erdo/send\_email step, so no paid, provider-facing or approval-gated action is ever invoked on demand; and deliver\_to is the only address it can reach, replacing whatever the declaration's own recipient renders to.




## OpenAPI

````yaml /api/openapi.json post /v1/dataset-row-actions/test-send
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-09'
servers:
  - url: https://api.erdo.ai
    description: Production
security:
  - bearerAuth: []
paths:
  /v1/dataset-row-actions/test-send:
    post:
      summary: >
        Renders the email a declaration would produce for one sample row and
        delivers
      description: >
        it to one nominated address, so somebody drafting a lead welcome message
        sees the real thing in their own inbox before it is switched on for real
        rows.


        The message is rendered by the same template engine the live automation
        uses, never a second renderer. Two properties bound what this can do:
        the steps must be zero or more script steps followed by exactly one
        erdo/send\_email step, so no paid, provider-facing or approval-gated
        action is ever invoked on demand; and deliver\_to is the only address it
        can reach, replacing whatever the declaration's own recipient renders
        to.
      operationId: POST:mcp.TestSendDatasetRowActionAPI
      requestBody:
        content:
          application/json:
            schema:
              properties:
                dataset:
                  description: >
                    and authorised exactly as a save would resolve it: testing
                    what you are about to declare is the same privilege as
                    declaring it.
                  title: >
                    dataset is the slug the declaration would be made against.
                    It is resolved
                  type: string
                deliver_to:
                  description: >
                    address a test send can ever reach. Whatever the
                    declaration's own \`to\` renders to is replaced with it.
                  title: >
                    deliver_to is the single address this message is delivered
                    to — the only
                  type: string
                row:
                  items:
                    $ref: '#/components/schemas/mcp.RowActionTestField'
                  title: >
                    row is the sample row to render against, one entry per
                    column.
                  type: array
                steps:
                  description: >
                    takes them. A test send accepts one shape only: zero or more
                    script steps followed by exactly one erdo/send\_email step.
                    Any other action is refused, so nothing paid or
                    approval-gated is ever invoked to answer this call.
                  items:
                    $ref: '#/components/schemas/mcp.RowActionStep'
                  title: >
                    steps are the declaration's steps, exactly as PUT
                    /v1/dataset-row-actions
                  type: array
              type: object
      responses:
        '200':
          content:
            application/json:
              schema:
                properties:
                  sent:
                    title: |
                      sent is whether a message was delivered.
                    type: boolean
                  skipped:
                    description: >
                      the data a required placeholder needs is skipped rather
                      than sent, which is exactly what would happen to a real
                      row — and it is the most useful thing a test can report.
                    title: >
                      skipped says why nothing was sent, in plain words. A row
                      that is missing
                    type: string
                  subject:
                    description: |
                      went out: it is what a real row would have produced.
                    title: >
                      subject is the rendered subject line, reported whether or
                      not the message
                    type: string
                  to:
                    description: |
                      Empty when nothing was sent.
                    title: >
                      to is the address it went to — deliver_to, as reported by
                      the send itself.
                    type: string
                type: object
          description: Success response
        default:
          $ref: '#/components/responses/APIError'
components:
  schemas:
    mcp.RowActionTestField:
      description: >
        The row is a LIST rather than an object because a map of arbitrary
        values cannot appear in an API schema at all — the same constraint that
        keeps a step's \`input\` as raw JSON. Every value is text, which is what
        a template resolves anyway: a numeric column reads as its digits.
      properties:
        column:
          description: |
            resolve {{column}} and {{row.column}} against these.
          title: >
            column is the column name, exactly as the dataset spells it.
            Templates
          type: string
        value:
          title: |
            value is what that column holds for this sample row.
          type: string
      title: |
        One column of the sample row a test send renders against.
      type: object
    mcp.RowActionStep:
      description: >
        OR a script to run, never both. Steps are taken in order: an action is
        skipped once an earlier action has answered, which is how a fallback is
        expressed, while a script always runs, because judging or shaping what
        the steps before it found costs nothing.
      properties:
        app:
          description: |
            connected integration. Omit it for a script step.
          title: |
            app names the app an action belongs to — "erdo", or the slug of a
          type: string
        fallback:
          description: >
            only when nothing has answered yet. Steps otherwise run in order,
            each seeing what the ones before it produced, so leave this off
            unless the step exists to cover an earlier one drawing a blank.
          title: >
            fallback marks this step as an alternative to the ones before it: it
            runs
          type: boolean
        for_each:
          description: >
            {{placeholder}} resolving to one. The action is invoked once per
            element, with {{item}} and {{item\_index}} available in its input,
            and the step's result is the list of what came back. Every
            invocation counts against the run's budget.
          title: |
            for_each fans the action out over a list: a JSON array, or one
          type: object
        found_when:
          description: >
            found anything, e.g. "found". It exists because an action that found
            nothing rarely returns nothing: it returns an envelope saying so,
            and Erdo appends ok:true to every native action's result, so "did
            this answer" and "what should be recorded" are different questions.
            Without it a miss reads as a hit and the step behind it is
            unreachable. Defaults to "the extracted result is not empty".
          title: >
            found_when is a dotted path into the action's result that says
            whether it
          type: string
        input:
          description: >
            may contain {{column}} placeholders resolved against the row;
            everything else passes through untouched, so a numeric setting stays
            a number.
          title: >
            input carries the action's own parameters, as a JSON object. String
            values
          type: object
        key:
          title: >
            key names the action within that app, e.g. "send_email" or
            "web_search".
          type: string
        result_path:
          description: >
            Many actions wrap their answer in an envelope that echoes the
            request, so a search that found nothing still comes back non-empty;
            without a path every such action looks like it succeeded and the
            next step is never tried. It also becomes what {{result}} refers to.
            Defaults to the whole result.
          title: >
            result_path is a dotted path into the action's result, e.g.
            "results.0".
          type: string
        script:
          description: >
            is a function body receiving (row, result, steps, run): the row's
            columns, the answer the steps before it produced, their individual
            results, and {total, index, rows, budget} describing the run. What
            it returns becomes this step's result; returning nothing clears the
            answer, so a finding it judged unusable falls through to the next
            action instead of being recorded. It may also return a \`note\`
            explaining the decision, which is reported on the run result and
            never counts as an answer. A script shapes data: it cannot invoke
            actions or read datasets, because a declaration has to say what it
            calls and where it writes.
          title: >
            script is JavaScript run against the row instead of invoking an
            action. It
          type: string
      title: >
        One step of a declaration. A step is EITHER an action to invoke (app and
        key)
      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_...).

````