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

# ReadWorkstreamLedgerAPI mirrors erdo_read_workstream_ledger: the allocator's


> single ledger read (budget, experiments + observations, calibration, steering notes, attention items, allocator recommendation).




## OpenAPI

````yaml /api/openapi.json get /v1/workstreams/{workstreamSlug}/ledger
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-25'
servers:
  - url: https://api.erdo.ai
    description: Production
security:
  - bearerAuth: []
paths:
  /v1/workstreams/{workstreamSlug}/ledger:
    get:
      summary: >
        ReadWorkstreamLedgerAPI mirrors erdo_read_workstream_ledger: the
        allocator's
      description: >
        single ledger read (budget, experiments + observations, calibration,
        steering notes, attention items, allocator recommendation).
      operationId: GET:mcp.ReadWorkstreamLedgerAPI
      parameters:
        - allowEmptyValue: true
          explode: false
          in: path
          name: workstreamSlug
          required: true
          schema:
            type: string
          style: simple
        - allowEmptyValue: true
          explode: true
          in: query
          name: observations_per_experiment
          required: false
          schema:
            format: int32
            maximum: 2147483647
            minimum: -2147483648
            type: integer
          style: form
      responses:
        '200':
          content:
            application/json:
              schema:
                properties:
                  allocator_recommendation:
                    $ref: '#/components/schemas/types.AllocatorRecommendation'
                  calibration:
                    description: >
                      trust dial the loop uses to weight kills and screening.
                      Empty until judges have made forecasts reality later
                      measured. Each entry also carries its residual stream
                      (misordered\_examples) — the seed the loop hands to judge
                      generation when a persistent disagreement pattern emerges.
                    items:
                      $ref: '#/components/schemas/types.JudgeCalibrationStats'
                    title: >
                      Calibration is the per-judge track record over this
                      project's ledger — the
                    type: array
                  child_portfolio:
                    description: >
                      It is deliberately one level deep: portfolio supervision
                      does not require an unbounded graph traversal.
                    items:
                      $ref: '#/components/schemas/types.ChildPortfolioSummary'
                    title: >
                      ChildPortfolio contains direct same-org/project workstream
                      children only.
                    type: array
                  defects:
                    description: >
                      ledger — the seeded complement to the misordered\_examples
                      each judge carries. A defect costs no conversion, so the
                      residual stream can never surface it; the loop reads these
                      alongside misordered\_examples so judge generation can
                      propose a lens for a failure only a human eye caught.
                      Bounded per read.
                    items:
                      $ref: '#/components/schemas/types.ExperimentDefect'
                    title: >
                      Defects are the human "this variant is broken" verdicts on
                      this project's
                    type: array
                  experiments:
                    items:
                      $ref: '#/components/schemas/types.ExperimentWithObservations'
                    type: array
                  judged_bets:
                    format: int64
                    type: integer
                  open_attention_items:
                    items:
                      $ref: '#/components/schemas/types.AttentionItem'
                    type: array
                  phases:
                    items:
                      $ref: '#/components/schemas/types.WorkstreamPhase'
                    type: array
                  random_arm_bets:
                    description: >
                      this project: how many measured bets skipped judge
                      screening vs. were screened. The loop watches the split to
                      keep the random arm alive (roughly one in ten) so it can
                      price what the judges cost.
                    format: int64
                    title: >
                      RandomArmBets / JudgedBets are the meta loop's
                      screening-cost accounting for
                    type: integer
                  recent_events:
                    items:
                      $ref: '#/components/schemas/types.WorkstreamEvent'
                    type: array
                  recently_resolved_attention_items:
                    description: >
                      that a human (or the expiry safe-default) resolved within
                      the recent window. The loop reads these to act on ANSWERS
                      to the escalations/choices it asked — a self-reconciling
                      pass must be able to read back the human's reply, not just
                      re-ask. Carries the answer + applied safe\_default; newest
                      first.
                    items:
                      $ref: '#/components/schemas/types.AttentionItem'
                    title: >
                      RecentlyResolvedAttentionItems are items the loop raised
                      on this workstream
                    type: array
                  workstream:
                    $ref: '#/components/schemas/types.Workstream'
                type: object
          description: Success response
        default:
          $ref: '#/components/responses/APIError'
components:
  schemas:
    types.AllocatorRecommendation:
      description: >
        workstream: a per-bet action + posterior, plus the budget envelope it
        fit the recommendation inside. Deterministic — same ledger in, same
        recommendation out.
      properties:
        bets:
          items:
            $ref: '#/components/schemas/types.BetAllocation'
          type: array
        budget_exhausted:
          type: boolean
        notes:
          description: |
            weigh alongside the per-bet actions.
          items:
            type: string
          title: >
            Notes carries workstream-level remarks (e.g. budget exhausted) the
            agent should
          type: array
        remaining_millicents:
          description: >
            against (nil = unlimited budget, no budget gate). BudgetExhausted is
            true when a bounded envelope is already spent, so nothing new can be
            funded this pass.
          format: int64
          title: >
            RemainingMillicents is the envelope headroom the recommendation
            reserved
          type: integer
      title: |
        AllocatorRecommendation is the allocator's whole read-only output for a
      type: object
    types.JudgeCalibrationStats:
      description: >
        were later measured, and how often it ordered variants the way reality
        did. Pairwise agreement is the trust signal — it covers both prediction
        rows (two forecasts imply an ordering) and comparison rows (an explicit
        ordering) uniformly, against reality's ordering from the measured
        metric\_reads.
      properties:
        agreed_pairs:
          format: int64
          type: integer
        by_artifact_kind:
          additionalProperties:
            $ref: '#/components/schemas/types.JudgeCalibrationKindStats'
          description: >
            details\_json->>'artifact\_kind' tag, when judges set it (e.g.
            landing\_page vs dashboard). Empty when no prediction carried a
            kind.
          title: |
            ByArtifactKind breaks the prediction-derived stats out by the
          type: object
        by_grounding_source:
          additionalProperties:
            $ref: '#/components/schemas/types.JudgeCalibrationKindStats'
          description: >
            prediction was made by an AUTHORED persona (brand-brief cohort) or a
            MEASURED one (grounded from real sessions) — keyed
            "authored"/"measured". This is the acceptance test for persona
            grounding v1: grounded personas must out-agree authored ones.
            Populated only for persona judges when the caller supplied the
            measured-hash set; empty otherwise.
          title: >
            ByGroundingSource splits this judge's pairwise agreement by whether
            the
          type: object
        comparable_pairs:
          description: >
            where BOTH the judge and reality expressed an ordering; AgreedPairs
            is how many the judge got right. PairwiseAgreementRate =
            AgreedPairs/ComparablePairs (0 when there are no comparable pairs
            yet).
          format: int64
          title: >
            ComparablePairs is the number of variant pairs (within one
            experiment)
          type: integer
        direct_pairs:
          format: int64
          type: integer
        effective_agreement:
          description: >
            platform prior adjusted by this org's local pairs as they
            accumulate. With zero local pairs it equals the platform rate; with
            many it converges to the org-local rate (a divergent org overrides
            the prior once it has the evidence). EffectivePairs is the combined
            sample size behind it (capped platform prior + local pairs) — the
            sample count the trust gate reads. See HierarchicalAgreement.
          title: >
            EffectiveAgreement is the hierarchical (Beta-binomial shrinkage)
            estimate: the
          type: number
        effective_pairs:
          format: int64
          type: integer
        judge_slug:
          type: string
        misordered_examples:
          description: >
            of variant pairs where reality's measured ordering disagreed with
            the judge's (the surprises). These are the seed for judge
            GENERATION: a new judge is proposed to catch the failure these
            examples name. Capped per judge (judge generation wants the head of
            the stream, not its full history).
          items:
            $ref: '#/components/schemas/types.JudgeMisorderedPair'
          title: >
            MisorderedExamples is the residual stream for this judge — a bounded
            sample
          type: array
        paired_predictions:
          description: >
            metric\_read on the same (experiment\_run, variant\_key) — i.e.
            reality eventually spoke on that forecast. DirectPairs is the
            further subset where the measured metric\_name matched the
            forecast's (a value-vs-value pair, not only a ranking pair).
          format: int64
          title: |
            PairedPredictions is the subset of predictions that have a later
          type: integer
        pairwise_agreement_rate:
          type: number
        platform_agreement:
          description: >
            These three fields are populated ONLY for app-level default judges
            (the NULL-org seeded lenses Erdo ships). An org-authored judge's
            record stays org-private and never pools, so they are zero for it.


            PlatformAgreement / PlatformPairs are the judge's agreement rate and
            paired count POOLED across every org — statistics only, never
            verdict content and never an org identifier (see the
            platform-calibration privacy contract). They let a default judge's
            trust cold-start from the whole platform's record while a new org
            has zero local pairs.
          title: |
            --- hierarchical (platform-pooled) calibration ---
          type: number
        platform_pairs:
          format: int64
          type: integer
        random_arm_pairs:
          description: >
            against a variant that reached reality via the random arm (a bet
            flagged random\_arm=true, i.e. one that skipped judge screening and
            went to reality anyway — see the meta loop). The permanent random
            arm is how the engine measures what the judges cost: a judge's
            agreement over bets it did NOT gate is its track record on the
            counterfactual the screened path can never show.
          format: int64
          title: >
            RandomArmPairs is how many of this judge's ComparablePairs were
            scored
          type: integer
        total_comparisons:
          format: int64
          type: integer
        total_predictions:
          description: |
            judge across the scanned ledger.
          format: int64
          title: >
            TotalPredictions / TotalComparisons are raw row counts attributed to
            this
          type: integer
      title: >
        JudgeCalibrationStats is one judge's track record: how often its
        predictions
      type: object
    types.ChildPortfolioSummary:
      description: >
        uses to supervise its direct child workstreams without recursively
        walking the resource graph.
      properties:
        experiment_count:
          format: int64
          type: integer
        open_attention_count:
          format: int64
          type: integer
        running_experiment_count:
          format: int64
          type: integer
        workstream:
          $ref: '#/components/schemas/types.Workstream'
      title: >
        ChildPortfolioSummary is the bounded one-level projection a root
        workstream
      type: object
    types.ExperimentDefect:
      description: >
        — a 'defect' observation written when a human flagged a choice's
        variant. It is the seeded complement to the misordered-pair residual
        stream: a defect costs no conversion (a clipped hero, off-brand type,
        scaffolding text on a stat card), so it can never surface as a variant
        that "converted worse than predicted", yet it is exactly the kind of
        failure judge generation should learn to catch. JudgeSlug is
        "human:\<user\_id>"; Reason is the human's free text naming the problem.
      properties:
        experiment_run_id:
          type: string
        judge_slug:
          type: string
        observed_at:
          format: date-time
          type: string
        reason:
          type: string
        variant_key:
          type: string
      title: >
        ExperimentDefect is one human "this variant is broken" verdict from the
        ledger
      type: object
    types.ExperimentWithObservations:
      description: >
        of its observation ledger, so the loop can weigh a bet without a second
        read.
      properties:
        experiment:
          $ref: '#/components/schemas/types.ExperimentRun'
        recent_observations:
          items:
            $ref: '#/components/schemas/types.ExperimentObservation'
          type: array
        variants:
          items:
            $ref: '#/components/schemas/types.ExperimentVariant'
          type: array
      title: >
        ExperimentWithObservations pairs an experiment with its variants and the
        head
      type: object
    types.AttentionItem:
      description: >
        surfaced in the Activity feed. See
        migrations/008\_create\_attention\_item.
      properties:
        answer:
          type: object
        answered_at:
          format: date-time
          type: string
        answered_by_user_id:
          format: uuid
          type: string
        body_markdown:
          type: string
        created_at:
          format: date-time
          type: string
        expires_at:
          format: date-time
          type: string
        id:
          format: uuid
          type: string
        kind:
          title: |
            digest | choice | escalation | judge_rescreen
          type: string
        organization_id:
          format: uuid
          type: string
        payload:
          description: >
            {title, questions\[]} plus optional experiment mapping; for
            escalation {proposed\_answer, safe\_default}.
          title: |
            Payload carries the kind-specific shape: for choice the ask_user
          type: object
        read_at:
          format: date-time
          type: string
        severity:
          title: |
            info | attention | urgent
          type: string
        slug:
          type: string
        status:
          title: |
            open | answered | dismissed | expired
          type: string
        title:
          type: string
        workstream_id:
          format: uuid
          type: string
      title: |
        AttentionItem is one thing the engine wants a human to see or answer,
      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.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.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
        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
    types.BetAllocation:
      description: >
        active experiment. Action is fund | hold | kill\_candidate |
        lottery\_slot | champion\_trial; Rationale is one machine-written
        sentence naming the discipline that produced it. The posterior fields
        make the value estimate inspectable: which source produced it (reality
        vs a judge prior), its mean/stddev, the attributable cost, and the
        resulting value-per-cost — so a human (or the agent) can see WHY the
        action was recommended, not just what.
      properties:
        action:
          type: string
        champion_judge:
          type: string
        cost_attributed:
          title: |
            false = nominal fallback cost, not itemized spend
          type: boolean
        cost_millicents:
          title: >
            Cost + value-per-cost (value / attributable spend, floored so it's
            finite).
          type: number
        experiment_slug:
          type: string
        has_reality:
          title: |
            reality measured the primary metric for this bet
          type: boolean
        judge_standing:
          description: >
            floor test's input (reality rank when measured, else
            calibrated-judge-weighted win fraction). ChampionJudge names the
            calibrated dissenting judge when the action is champion\_trial.
          title: >
            JudgeStanding is the bet's position in its experiment's field in
            [0,1] — the
          type: number
        posterior_source:
          title: |
            reality | judge_prior | uninformed
          type: string
        rationale:
          type: string
        value_mean:
          title: |
            Value posterior over the experiment's primary metric.
          type: number
        value_per_cost_mean:
          type: number
        value_per_cost_stddev:
          type: number
        value_stddev:
          type: number
        variant_key:
          type: string
      title: >
        BetAllocation is the allocator's recommendation for one bet — one
        variant of one
      type: object
    types.JudgeCalibrationKindStats:
      description: >
        the "trust region" the input-dependent gate reads (Kennedy–O'Hagan): a
        judge is trusted to block a build only within an artifact kind its
        forecasts have been validated on. The Effective\* fields mirror the
        top-level hierarchical estimate but scoped to this ONE kind: kind-local
        pairs shrunk toward this kind's platform prior. A kind with no local or
        platform evidence has zero effective pairs, so the gate treats the judge
        as advisory there — exactly like an uncalibrated judge.
      properties:
        agreed_pairs:
          format: int64
          type: integer
        comparable_pairs:
          format: int64
          type: integer
        effective_agreement:
          type: number
        effective_pairs:
          format: int64
          type: integer
        paired_predictions:
          format: int64
          type: integer
        pairwise_agreement_rate:
          type: number
        platform_agreement:
          description: >
            (app-level default judges only; zero for org-authored judges, which
            never pool). EffectiveAgreement / EffectivePairs are the kind-scoped
            hierarchical estimate — the (rate, sample) the gate scores via
            TrustAgreementForKind.
          title: >
            PlatformAgreement / PlatformPairs are this kind's pooled prior
            across every org
          type: number
        platform_pairs:
          format: int64
          type: integer
        total_predictions:
          format: int64
          type: integer
      title: >
        JudgeCalibrationKindStats is the per-artifact-kind slice of a judge's
        record —
      type: object
    types.JudgeMisorderedPair:
      description: >
        preferred JudgeWinner over RealityWinner, but reality's measured Metric
        ranked RealityWinner higher. It is grounded — every field is a
        recomputable fact about what was measured, never a guess — so a judge
        proposed from it anchors to checkable reality, the anti-Goodhart
        property the meta loop depends on.
      properties:
        experiment_run_id:
          type: string
        judge_winner:
          type: string
        judge_winner_value:
          type: number
        metric:
          type: string
        reality_winner:
          type: string
        reality_winner_value:
          type: number
      title: >
        JudgeMisorderedPair is one residual: within one decided experiment the
        judge
      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.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.ExperimentVariant:
      properties:
        allocation_percent:
          format: int32
          maximum: 2147483647
          minimum: -2147483648
          type: integer
        created_at:
          format: date-time
          type: string
        experiment_run_id:
          format: uuid
          type: string
        id:
          format: uuid
          type: string
        is_control:
          type: boolean
        key:
          type: string
        label:
          type: string
        treatment_class:
          description: >
            (TreatmentClassTaxonomy) — DECLARED by the producing agent, never
            derived from content. It is the privacy boundary the cross-customer
            prior distiller aggregates on. Nil for the control and for
            treatments that carry no class.
          title: >
            TreatmentClass is the variant's structural class from the fixed
            taxonomy
          type: string
        treatment_resource_id:
          format: uuid
          type: string
        treatment_resource_type:
          type: string
        treatment_url:
          description: >
            page resource — the wind-tunnel persona panel can visit either. Set
            for variants whose page is not an Erdo artifact (an agency's client
            landing page); mutually exclusive with TreatmentResourceType/ID.
            Public http(s) URLs only, never a signed-in Erdo workspace URL.
          title: >
            TreatmentURL is an external page URL this variant tests INSTEAD OF
            an Erdo
          type: string
      title: |
        ExperimentVariant is one variant in an experiment.
      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_...).

````