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

# SetWorkstreamExternalRefsAPI declares which provider-owned objects a


> workstream governs — the machine-readable replacement for a campaign allowlist hand-typed into a knowledge record.

REST only, with no MCP mirror, and the asymmetry is the point: an agent must be able to READ the set of live campaigns it may act on, and must not be able to widen it. The consumer holding the mapping declares scope here; agents read it back on every workstream and ledger read.




## OpenAPI

````yaml /api/openapi.json put /v1/workstreams/{workstreamSlug}/external-refs
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, and experiments. Authenticate with
    a Bearer API key (erdo_api_...) or scoped token (erdo_token_...).
  version: '2026-07-27'
servers:
  - url: https://api.erdo.ai
    description: Production
security:
  - bearerAuth: []
paths:
  /v1/workstreams/{workstreamSlug}/external-refs:
    put:
      summary: |
        SetWorkstreamExternalRefsAPI declares which provider-owned objects a
      description: >
        workstream governs — the machine-readable replacement for a campaign
        allowlist hand-typed into a knowledge record.


        REST only, with no MCP mirror, and the asymmetry is the point: an agent
        must be able to READ the set of live campaigns it may act on, and must
        not be able to widen it. The consumer holding the mapping declares scope
        here; agents read it back on every workstream and ledger read.
      operationId: PUT:mcp.SetWorkstreamExternalRefsAPI
      parameters:
        - allowEmptyValue: true
          explode: false
          in: path
          name: workstreamSlug
          required: true
          schema:
            type: string
          style: simple
      requestBody:
        content:
          application/json:
            schema:
              properties:
                external_refs:
                  items:
                    $ref: '#/components/schemas/types.ExternalRef'
                  type: array
              type: object
      responses:
        '200':
          content:
            application/json:
              schema:
                properties:
                  events:
                    description: >
                      (newest first), embedded so cards/agents see activity
                      without a second call.
                    items:
                      $ref: '#/components/schemas/types.WorkstreamEvent'
                    title: >
                      Events is the most recent slice of the workstream's
                      chronological log
                    type: array
                  following:
                    description: >
                      detail page can render its follow toggle without a second
                      call (Wave E).
                    title: >
                      Following is whether the requesting user follows this
                      workstream, so the
                    type: boolean
                  phases:
                    items:
                      $ref: '#/components/schemas/types.WorkstreamPhase'
                    type: array
                  resources:
                    items:
                      $ref: '#/components/schemas/types.WorkstreamResource'
                    type: array
                  workstream:
                    $ref: '#/components/schemas/types.Workstream'
                type: object
          description: Success response
        default:
          $ref: '#/components/responses/APIError'
components:
  schemas:
    types.ExternalRef:
      description: >
        workstream governs.


        It exists because \`workstream\_resource\` cannot express this: that
        table keys on a uuid, and providers do not issue uuids — a Google Ads
        campaign is \`24033607833\`. With nowhere typed to put it, the only
        place scope could live was prose in a knowledge record, hand-mirrored
        from a mapping the job reading it could not see.


        Every field is an opaque provider string. Erdo does not parse them,
        validate them against the provider, or dereference them; it stores what
        the owning consumer resolved and hands it back. That is deliberate — the
        consumer holds the evidence that produced the mapping, and a platform
        that second-guessed it would have to reimplement that evidence.
      properties:
        account_id:
          description: >
            hold the same campaign id in different accounts, so a ref without it
            is not unique.
          title: >
            AccountID is the provider's account/customer identifier. Two
            customers can
          type: string
        external_id:
          title: |
            ExternalID is the provider's own identifier for the object.
          type: string
        kind:
          description: >
            "ad\_group". Free-form: providers keep inventing object types, and
            an allowlist here would need a platform release to track each one.
          title: |
            Kind is what the id names within that provider, e.g. "campaign",
          type: string
        label:
          description: >
            approval card can say "2200 Brickell - Wellness Led" rather than a
            number. Never used for matching.
          title: |
            Label is an optional human-readable name, carried so a log line or
          type: string
        provider:
          title: |
            Provider is the integration this ref belongs to, e.g. "google_ads".
          type: string
      title: |
        ExternalRef names one object in an external provider's account that a
      type: object
    types.WorkstreamEvent:
      description: >
        — a git-log-style record of what happened and what's next. Body is
        free-form; actor\_type/actor\_id is attribution (identity, not routing).
      properties:
        actor_id:
          format: uuid
          type: string
        actor_type:
          type: string
        body:
          type: string
        created_at:
          format: date-time
          type: string
        id:
          format: uuid
          type: string
        organization_id:
          format: uuid
          type: string
        workstream_id:
          format: uuid
          type: string
      title: >
        WorkstreamEvent is one entry in a workstream's append-only,
        chronological log
      type: object
    types.WorkstreamPhase:
      properties:
        completed_at:
          format: date-time
          type: string
        created_at:
          format: date-time
          type: string
        id:
          format: uuid
          type: string
        skill_slug:
          type: string
        slug:
          type: string
        sort_order:
          format: int32
          maximum: 2147483647
          minimum: -2147483648
          type: integer
        started_at:
          format: date-time
          type: string
        status:
          type: string
        thread_id:
          format: uuid
          type: string
        title:
          type: string
        updated_at:
          format: date-time
          type: string
        workstream_id:
          format: uuid
          type: string
      title: |
        WorkstreamPhase is an ordered step within a workstream.
      type: object
    types.WorkstreamResource:
      properties:
        created_at:
          format: date-time
          type: string
        id:
          format: uuid
          type: string
        phase_id:
          format: uuid
          type: string
        relationship_type:
          type: string
        resource_id:
          format: uuid
          type: string
        resource_type:
          type: string
        title:
          type: string
        workstream_id:
          format: uuid
          type: string
      title: >
        WorkstreamResource is a typed link from a workstream to another domain
        object.
      type: object
    types.Workstream:
      description: >
        Operational state machine for multi-step work (campaign launches,
        reports, monitors).
      properties:
        autonomy_mode:
          description: >
            (autopilot|propose|strict); nil inherits the org-level setting. The
            engine gate resolves effective mode = workstream ?? org ?? propose.
          title: |
            AutonomyMode is this workstream's autonomy override
          type: string
        completed_at:
          format: date-time
          type: string
        created_at:
          format: date-time
          type: string
        description:
          type: string
        escalation_budget_per_day:
          format: int32
          maximum: 2147483647
          minimum: -2147483648
          type: integer
        external_refs:
          description: >
            machine-readable answer to "which campaigns is this managing?".
            Always present (empty when nothing is declared) so a reader never
            has to tell "governs nothing" from "field absent". It rides along on
            every workstream read, including each entry of a root's
            ChildPortfolio, so a portfolio loop gets the whole account's scope
            from the ledger read it already makes.
          items:
            $ref: '#/components/schemas/types.ExternalRef'
          title: >
            ExternalRefs are the provider-owned objects this workstream governs
            — the
          type: array
        id:
          format: uuid
          type: string
        organization_id:
          format: uuid
          type: string
        primary_thread_id:
          format: uuid
          type: string
        project_id:
          format: uuid
          type: string
        slug:
          type: string
        source_template_id:
          format: uuid
          type: string
        spent_millicents:
          format: int64
          type: integer
        started_at:
          format: date-time
          type: string
        status:
          type: string
        title:
          type: string
        token_budget_millicents:
          description: >
            ceiling (nil = unlimited); SpentMillicents is the running total
            accrued from completed engine work; EscalationBudgetPerDay caps
            urgent items per rolling 24h (nil = org default). Millicents =
            tenths of a cent.
          format: int64
          title: |
            Budget envelope (work-engine D6). TokenBudgetMillicents is the spend
          type: integer
        updated_at:
          format: date-time
          type: string
      title: |
        Workstream represents a piece of business work in motion.
      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_...).

````