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

# ManageWorkstreamRecommendationsAPI records an opportunity and its requested next
step.


> Research and preparation wake the existing coordinator; provider actions retain their native approval gates.




## OpenAPI

````yaml /api/openapi.json post /v1/workstreams/{workstreamSlug}/recommendations
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-26'
servers:
  - url: https://api.erdo.ai
    description: Production
security:
  - bearerAuth: []
paths:
  /v1/workstreams/{workstreamSlug}/recommendations:
    post:
      summary: >
        ManageWorkstreamRecommendationsAPI records an opportunity and its
        requested next

        step.
      description: >
        Research and preparation wake the existing coordinator; provider actions
        retain their native approval gates.
      operationId: POST:mcp.ManageWorkstreamRecommendationsAPI
      parameters:
        - allowEmptyValue: true
          explode: false
          in: path
          name: workstreamSlug
          required: true
          schema:
            type: string
          style: simple
      requestBody:
        content:
          application/json:
            schema:
              properties:
                action:
                  type: string
                approval_id:
                  type: string
                assignment_phase:
                  type: string
                assignment_slug:
                  type: string
                changed_evidence_reason:
                  type: string
                content:
                  $ref: '#/components/schemas/types.RecommendationContent'
                evidence:
                  items:
                    $ref: '#/components/schemas/types.RecommendationEvidence'
                  type: array
                execution_workstream_slug:
                  type: string
                expected_revision:
                  format: int64
                  type: integer
                experiment_slug:
                  type: string
                measurement_evidence:
                  $ref: '#/components/schemas/types.RecommendationEvidence'
                reason:
                  type: string
                recommendation_slug:
                  type: string
                responsibility_key:
                  type: string
                review_summary:
                  type: string
              type: object
      responses:
        '200':
          content:
            application/json:
              schema:
                properties:
                  outcome:
                    type: string
                  recommendation:
                    $ref: '#/components/schemas/types.WorkstreamRecommendation'
                  review:
                    $ref: '#/components/schemas/types.RecommendationReview'
                  workstream_slug:
                    type: string
                type: object
          description: Success response
        default:
          $ref: '#/components/responses/APIError'
components:
  schemas:
    types.RecommendationContent:
      properties:
        disproof:
          type: string
        language:
          type: string
        maturity:
          type: string
        next_step:
          type: string
        property_fit:
          type: string
        proposed_test_markdown:
          type: string
        strongest_alternative:
          type: string
        summary:
          type: string
        target_market:
          type: string
        title:
          type: string
        unknowns:
          items:
            type: string
          type: array
        why_now:
          type: string
      type: object
    types.RecommendationEvidence:
      properties:
        checked_at:
          format: date-time
          type: string
        published_at:
          format: date-time
          type: string
        resource_id:
          type: string
        resource_type:
          type: string
        review_status:
          type: string
        revision_ref:
          type: string
        source_status:
          type: string
        summary:
          type: string
        title:
          type: string
        url:
          type: string
        window_end:
          format: date-time
          type: string
        window_start:
          format: date-time
          type: string
      type: object
    types.WorkstreamRecommendation:
      properties:
        approval_id:
          type: string
        approval_status:
          type: string
        approvals:
          items:
            $ref: '#/components/schemas/types.RecommendationApproval'
          type: array
        assignment:
          $ref: '#/components/schemas/types.AssignmentSummary'
        content:
          $ref: '#/components/schemas/types.RecommendationContent'
        created_at:
          format: date-time
          type: string
        decision:
          $ref: '#/components/schemas/types.DecisionSummary'
        evidence:
          items:
            $ref: '#/components/schemas/types.RecommendationEvidence'
          type: array
        execution_assignment:
          $ref: '#/components/schemas/types.AssignmentSummary'
        execution_assignment_slug:
          type: string
        execution_dispatch_key:
          type: string
        execution_generation:
          format: int64
          type: integer
        execution_status:
          type: string
        execution_workstream_id:
          format: uuid
          type: string
        experiment:
          $ref: '#/components/schemas/types.ExperimentRun'
        experiment_slug:
          type: string
        history:
          items:
            $ref: '#/components/schemas/types.RecommendationEvent'
          type: array
        id:
          format: uuid
          type: string
        measurement:
          $ref: '#/components/schemas/types.ExperimentObservation'
        measurement_evidence:
          $ref: '#/components/schemas/types.RecommendationEvidence'
        organization_id:
          format: uuid
          type: string
        pass_reason:
          type: string
        passed_at:
          format: date-time
          type: string
        preparation_assignment:
          $ref: '#/components/schemas/types.AssignmentSummary'
        preparation_assignment_slug:
          type: string
        preparation_dispatch_key:
          type: string
        preparation_generation:
          format: int64
          type: integer
        previous_executions:
          items:
            $ref: '#/components/schemas/types.RecommendationPriorExecution'
          type: array
        requested_action:
          type: string
        research_assignment:
          $ref: '#/components/schemas/types.AssignmentSummary'
        research_assignment_slug:
          type: string
        research_dispatch_key:
          type: string
        research_generation:
          format: int64
          type: integer
        responsibility_key:
          type: string
        revision:
          format: int64
          type: integer
        slug:
          type: string
        stage:
          type: string
        updated_at:
          format: date-time
          type: string
        workstream_id:
          format: uuid
          type: string
      type: object
    types.RecommendationReview:
      properties:
        last_completed_at:
          format: date-time
          type: string
        next_due_at:
          format: date-time
          type: string
        summary:
          type: string
      type: object
    types.RecommendationApproval:
      properties:
        action:
          type: string
        executed_at:
          format: date-time
          type: string
        execution_status:
          type: string
        id:
          format: uuid
          type: string
        status:
          type: string
      type: object
    types.AssignmentSummary:
      description: >
        what it is for, where the supervisor thinks it stands, where its RESULT
        stands, and what the latest worker did about it. The two state axes ride
        together because they answer different questions — a completed
        assignment carrying a rejected result is a true row, and one field could
        not say so.
      properties:
        acceptance_criteria:
          type: string
        adoption_state:
          type: string
        assigned_agent_key:
          type: string
        created_at:
          format: date-time
          type: string
        expected_result_kind:
          type: string
        id:
          format: uuid
          type: string
        latest_attempt:
          allOf:
            - $ref: '#/components/schemas/types.AssignmentAttemptSummary'
          description: |
            has attempted yet.
          title: >
            LatestAttempt is the most recent worker's go at it, absent on work
            nothing
        objective:
          type: string
        reason:
          type: string
        slug:
          type: string
        work_state:
          type: string
        work_state_reason:
          type: string
      title: >
        AssignmentSummary is one bounded responsibility as a coordinator reads
        it:
      type: object
    types.DecisionSummary:
      description: >
        continuing agent and a returning operator read.


        It is the same shape for both audiences on purpose. The agent's copy of
        what the organization has committed to cannot be the only one, or a
        human coming back to the workstream has no way to check what it is
        acting on — so the projection carries decision, rationale, authority,
        execution and supersession, and both surfaces render those five facts
        rather than each deriving their own.


        It is deliberately not the whole record: the envelope's declared
        effects, its per-action inputs and its measurement contract are a
        drill-in, and putting them here would push the ledger read past what a
        model can usefully hold.
      properties:
        age_seconds:
          format: int64
          type: integer
        agent_run_id:
          format: uuid
          type: string
        alternatives_considered:
          description: >
            before proposing this — the WHY's companion, so a reader can tell a
            choice from the only idea anyone had. Empty is ordinary: most calls
            had no real alternative, and the record says so rather than
            inventing one.
          title: >
            AlternativesConsidered is the producer's account of what else it
            weighed
          type: string
        applicability:
          description: >
            that outlives the work it authorized, or an authorization for
            exactly the actions it named. It travels with the summary because
            the two are read differently — a standing decision applies until its
            review deadline or explicit retirement, while a one\_shot in the
            current list is work still in flight to see through, not a strategy
            to keep running.
          title: >
            Applicability is standing or one_shot: whether this decision is a
            course
          type: string
        approval_id:
          description: >
            approval producer family. It travels on the row so the join runs in
            both directions from one read: a caller holding an approval filters
            the list by it, and a caller reading a page of decisions can index
            them by the approvals they answer without a call per row.
          format: uuid
          title: >
            ApprovalID is the ask this decision was made on, present only for
            the
          type: string
        authorized_at:
          format: date-time
          type: string
        decided_by_name:
          description: >
            name, else email), so a surface can say "X did Y" without carrying a
            user lookup of its own. Empty when no human decided (an agent or a
            policy did), or when the id could not be resolved in the caller's
            organization — a manager acting into a client org is the case that
            does not resolve, and the raw id stays on DecidedByUserID for that.
            It is display-only and never a filter.
          title: >
            DecidedByName is the human decider resolved to a name (display name,
            else
          type: string
        decided_by_user_id:
          format: uuid
          type: string
        decider_kind:
          description: |
            deterministic\_policy, safe\_default or system.
          title: |
            DeciderKind is who exercised the judgement: human, llm_agent,
          type: string
        decider_rationale:
          description: >
            they supplied any. Usually empty — approving a card records no
            sentence.
          title: >
            DeciderRationale is the deciding human's own words about the answer,
            when
          type: string
        decision_class:
          description: >
            (\`paid\_media.ad\_group.pause\`), provider-agnostic and stable
            across the tools that implement it. Source is the producer family
            that recorded it.
          title: |
            DecisionClass is what KIND of decision this is
          type: string
        evidence_kind:
          description: >
            specified state exists), experimental (a controlled comparison), or
            observational (a before/after movement with no control). Empty when
            the settled effects disagree — a decision with one experimental and
            one deterministic result has no single kind, and picking one would
            attach the stronger claim to both.
          title: |
            EvidenceKind is what KIND of evidence settled it: deterministic (the
          type: string
        execution_status:
          description: >
            change the decision authorized actually reached the outside world.
            pending | started | succeeded | failed | unknown, and empty when the
            decision authorized no action at all.
          title: >
            ExecutionStatus rolls the child actions up into one word: whether
            the
          type: string
        exit_condition:
          type: string
        has_pending_effects:
          description: >
            outcome. An effective decision can legitimately be in this state
            between its action landing and the settlement sweep starting
            measurement, so clients must not infer that an empty outcome means
            the decision is finished.
          title: >
            HasPendingEffects says whether at least one measurable effect still
            owes an
          type: boolean
        lapsed:
          type: boolean
        outcome_label:
          description: >
            says the same thing about the same row rather than each deriving its
            own vocabulary from the pair above. Only an experiment may say a
            change "worked"; deterministic evidence says "confirmed" and
            observational says "moved as expected".


            Empty whenever the pair cannot support one phrase — mixed outcomes,
            mixed evidence kinds — because there is no honest single sentence
            for those and the per-effect labels in the detail read are the
            answer.
          title: >
            OutcomeLabel is the server's own words for this outcome, so every
            surface
          type: string
        outcome_status:
          description: >
            not\_met, inconclusive, or \`mixed\` when they disagree. Empty while
            nothing has settled, which is a different fact from inconclusive —
            one says the answer is not in yet, the other says the evidence could
            not produce one.


            Effects that are unmeasurable, censored, or parked in
            measurement\_unavailable contribute nothing here. They are coverage,
            they are visible as such in the detail read and the scorecard, and
            folding them in would let a decision nobody could measure read as
            one that did not work.
          title: >
            OutcomeStatus rolls this decision's SETTLED effects into one word:
            met,
          type: string
        proposed_at:
          format: date-time
          type: string
        rejected_at:
          description: >
            whose status is \`rejected\`.


            It is nullable when the source has no trustworthy answer fact, and
            is never inferred from a write clock.
          format: date-time
          title: >
            RejectedAt is when the proposal was refused, and it is present only
            on a

            decision
          type: string
        review_by:
          description: >
            never a lifecycle status or a destructive rewrite of the original
            record.
          format: date-time
          title: >
            ReviewBy includes the legacy creation-time fallback. Lapsed is
            derived,
          type: string
        slug:
          type: string
        source:
          type: string
        status:
          type: string
        subject_kind:
          description: |
            names it — a campaign, a page, a lead form.
          title: >
            SubjectKind / SubjectLabel name what the decision is about, as the
            operator
          type: string
        subject_label:
          type: string
        superseded_by_decision_slug:
          description: >
            decision that later replaced this one. It carries the same
            past-authorization rule the currentness read applies — a reversal
            somebody PROPOSED and a human then refused has not replaced
            anything, and showing it here would report a course as retired
            exactly when the human voted to keep it.
          title: >
            SupersededByDecisionSlug is the other direction of the same link:
            the
          type: string
        supersedes_decision_slug:
          description: >
            the producer declared the link explicitly — a reversal states what
            it reverses, and nothing infers it from two decisions sharing a
            subject.
          title: >
            SupersedesDecisionSlug names the decision this one replaced. Set
            only when
          type: string
        what:
          description: >
            PRODUCER's rationale — the argument the proposal was made on. When a
            human gave their own reason for the answer it is in
            DeciderRationale, never merged into this one: reading a proposal's
            argument as the approver's reason for accepting it is how a record
            starts attributing motives.
          title: |
            What is the decision in one line of business English; Why is the
          type: string
        why:
          type: string
        workstream_id:
          description: >
            filed under and the run that proposed it. They travel on the summary
            so a surface rendering one row can link the workstream without a
            second read — the same reason ApprovalID travels. Both are optional
            in fact as well as shape: an org-level decision belongs to no
            workstream, and a deterministic producer may have no run.
          format: uuid
          title: >
            WorkstreamID / AgentRunID are provenance: the workstream the
            decision was
          type: string
      title: |
        DecisionSummary is the compact projection of one decision that both a
      type: object
    types.ExperimentRun:
      description: |
        Sibling to Workstream, can be hosted by a workstream or standalone.
      properties:
        created_at:
          format: date-time
          type: string
        decided_at:
          format: date-time
          type: string
        decision:
          type: string
        decision_rule_markdown:
          type: string
        guardrail_metrics:
          items:
            type: string
          type: array
        hypothesis_markdown:
          type: string
        id:
          format: uuid
          type: string
        organization_id:
          format: uuid
          type: string
        outcome_markdown:
          type: string
        primary_metric:
          type: string
        primary_thread_id:
          format: uuid
          type: string
        project_id:
          format: uuid
          type: string
        scope:
          type: string
        slug:
          type: string
        started_at:
          format: date-time
          type: string
        status:
          type: string
        title:
          type: string
        updated_at:
          format: date-time
          type: string
        workstream_id:
          format: uuid
          type: string
      title: |
        ExperimentRun is a structured test with hypothesis, variants, decision.
      type: object
    types.RecommendationEvent:
      properties:
        action:
          type: string
        actor_id:
          format: uuid
          type: string
        actor_kind:
          type: string
        approval_id:
          type: string
        assignment_phase:
          type: string
        assignment_slug:
          type: string
        at:
          format: date-time
          type: string
        content_basis:
          $ref: '#/components/schemas/types.RecommendationContent'
        evidence_basis:
          items:
            $ref: '#/components/schemas/types.RecommendationEvidence'
          type: array
        execution_workstream_id:
          format: uuid
          type: string
        experiment_slug:
          type: string
        measurement_evidence:
          $ref: '#/components/schemas/types.RecommendationEvidence'
        reason:
          type: string
      type: object
    types.ExperimentObservation:
      description: >
        read, a decision check, or an action the optimizer took. See
        migrations/002\_create\_experiment\_observation.up.sql for the full
        rationale — short version: experiment\_run carries state, this table
        carries the history needed to recover learning later from SQL rather
        than prose.
      properties:
        action_taken:
          type: string
        agent_run_id:
          format: uuid
          type: string
        created_at:
          format: date-time
          type: string
        details:
          type: object
        evidence_dataset_slug:
          type: string
        evidence_resource_id:
          format: uuid
          type: string
        experiment_run_id:
          format: uuid
          type: string
        id:
          format: uuid
          type: string
        job_execution_id:
          format: uuid
          type: string
        metric_name:
          type: string
        metric_value:
          type: number
        observation_type:
          type: string
        observed_at:
          format: date-time
          type: string
        organization_id:
          format: uuid
          type: string
        project_id:
          format: uuid
          type: string
        reason_code:
          type: string
        variant_key:
          type: string
        window_end:
          format: date-time
          type: string
        window_start:
          format: date-time
          type: string
      title: |
        ExperimentObservation is one append-only structured record of a metric
      type: object
    types.RecommendationPriorExecution:
      properties:
        approvals:
          items:
            $ref: '#/components/schemas/types.RecommendationApproval'
          type: array
        assignment:
          $ref: '#/components/schemas/types.AssignmentSummary'
      type: object
    types.AssignmentAttemptSummary:
      description: |
        status, which belongs to the agent service and is not mirrored here.
      properties:
        affected_output:
          type: string
        agent_run_id:
          format: uuid
          type: string
        attempt_number:
          format: int64
          type: integer
        ended_at:
          format: date-time
          type: string
        handles:
          description: >
            the revision the worker produced. This is what a reviewer validates
            and what an acceptance attaches; the summary is only how the worker
            described it.
          items:
            $ref: '#/components/schemas/types.AssignmentResourceHandleInput'
          title: >
            Handles are the domain objects the submission consists of, each
            pinned to
          type: array
        result_kind:
          description: >
            unaltered beside the assignment's expected\_result\_kind rather than
            reconciled with it, so a reviewer can see that the two disagree —
            which is a fact about the submission and often the reason to reject
            it.
          title: >
            ResultKind is what the worker BELIEVES it produced, as declared.
            Carried
          type: string
        state:
          type: string
        summary:
          description: >
            attempt submitted. It is never the result itself: a summary that
            could stand in for the work would let an unverified sentence become
            current state.
          title: >
            Summary is the submitted result's bounded description, present once
            the
          type: string
        terminal_cause:
          type: string
        terminal_reason:
          type: string
      title: >
        AssignmentAttemptSummary is one worker's execution lifecycle — never the
        run's
      type: object
    types.AssignmentResourceHandleInput:
      description: >
        service owns, as a dispatching producer declares it. It mirrors the
        handle the funnel stores, and it is what replaces "the worker can read
        the parent's transcript": an assignment attempted a week after its
        coordinator exited has no transcript to inherit, so every input it works
        from is named here. RevisionRef pins content that can change, so the
        worker and the coordinator who judges its result are looking at the same
        thing.
      properties:
        resource_id:
          type: string
        resource_type:
          type: string
        revision_ref:
          type: string
      title: |
        AssignmentResourceHandleInput is one typed pointer at something a domain
      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.