openapi: 3.0.3
info:
  title: Ditto External API
  version: 1.0.0
  description: |
    External API for ResearchGroup and ResearchStudy workflows.
    This spec covers the must-have endpoints for group recruitment,
    study creation, questions, completion, and sharing.
servers:
  - url: https://api.example.com
security:
  - bearerAuth: []
paths:
  /cli/auth/start:
    post:
      summary: Start CLI OAuth flow
      description: |
        Initializes a CLI OAuth session and returns an authorization URL.
        The redirect URI must be a loopback address (localhost or 127.0.0.1).
      tags:
        - CLIAuth
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CliAuthStartRequest'
      responses:
        '200':
          description: CLI auth session created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CliAuthStartResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /cli/auth/complete:
    post:
      summary: Complete CLI OAuth flow
      description: |
        Exchanges the authorization code for user info and returns a free-tier API key,
        or a login URL if the user belongs to a paid organization.
      tags:
        - CLIAuth
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CliAuthCompleteRequest'
      responses:
        '200':
          description: CLI auth completed
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/CliAuthCompleteResponse'
                  - $ref: '#/components/schemas/CliAuthLoginRequiredResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/free/questions:
    post:
      summary: Ask a question to the free-tier group
      description: |
        Sends a single question to the shared free-tier research group.
      tags:
        - FreeTier
        - Questions
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FreeTierQuestionCreateRequest'
      responses:
        '202':
          description: Free-tier question queued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FreeTierQuestionBroadcastResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/research-groups:
    get:
      summary: List research groups
      tags:
        - ResearchGroups
      parameters:
        - in: query
          name: limit
          schema:
            type: integer
            minimum: 1
            maximum: 500
        - in: query
          name: offset
          schema:
            type: integer
            minimum: 0
      responses:
        '200':
          description: Research groups list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResearchGroupListResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/research-groups/recruit:
    post:
      summary: Recruit a research group via filters
      tags:
        - ResearchGroups
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RecruitResearchGroupRequest'
      responses:
        '201':
          description: ResearchGroup created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RecruitResearchGroupResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/research-groups/{group_uuid}/append:
    post:
      summary: Add agents to an existing research group
      description: |
        Appends additional agents to an existing research group using the same filter DSL
        as recruitment. Existing members are preserved; only new agents are added.
      tags:
        - ResearchGroups
      parameters:
        - in: path
          name: group_uuid
          required: true
          schema:
            type: string
          description: Research group UUID.
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AppendResearchGroupRequest'
      responses:
        '200':
          description: ResearchGroup updated with appended members
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AppendResearchGroupResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/research-groups/{group_id}:
    get:
      summary: Get a research group
      description: |
        Returns the research group metadata along with member agent details.
      tags:
        - ResearchGroups
      parameters:
        - in: path
          name: group_id
          required: true
          schema:
            type: string
          description: Research group numeric ID or UUID.
      responses:
        '200':
          description: Research group details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResearchGroupDetailResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/research-groups/{group_id}/agents/remove:
    post:
      summary: Remove agents from a research group
      description: |
        Removes specific agents from a research group. Agents remain in the system;
        only the group membership is updated.
      tags:
        - ResearchGroups
      parameters:
        - in: path
          name: group_id
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RemoveGroupAgentsRequest'
      responses:
        '200':
          description: Removal confirmation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RemoveGroupAgentsResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
    patch:
      summary: Update a research group
      description: |
        Updates a group's name and description. Optionally appends new members using the
        group's original filters. Filter changes are not supported; create a new group
        to change filters.
      tags:
        - ResearchGroups
      parameters:
        - in: path
          name: group_id
          required: true
          schema:
            type: string
          description: Research group numeric ID or UUID.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ResearchGroupUpdateRequest'
      responses:
        '200':
          description: Research group updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResearchGroupUpdateResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
    delete:
      summary: Archive a research group
      description: |
        Soft-archives a research group. Study history is preserved while the group is removed
        from default lists.
      tags:
        - ResearchGroups
      parameters:
        - in: path
          name: group_id
          required: true
          schema:
            type: string
          description: Research group numeric ID or UUID.
      responses:
        '200':
          description: Research group archived
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResearchGroupArchiveResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/research-groups/{group_id}/questions:
    get:
      summary: List group questions with answers
      description: |
        Returns questions asked directly to a research group along with collected answers.
      tags:
        - ResearchGroups
        - GroupQuestions
      parameters:
        - in: path
          name: group_id
          required: true
          schema:
            type: string
          description: Research group numeric ID or UUID.
        - in: query
          name: question_id
          schema:
            type: integer
          description: Filter responses to a specific question ID.
      responses:
        '200':
          description: Group questions with answers
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GroupQuestionListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    post:
      summary: Ask a question to a research group
      description: |
        Sends a single question to a research group without creating a full study.
      tags:
        - ResearchGroups
        - GroupQuestions
      parameters:
        - in: path
          name: group_id
          required: true
          schema:
            type: string
          description: Research group numeric ID or UUID.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GroupQuestionCreateRequest'
      responses:
        '202':
          description: Group question queued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GroupQuestionBroadcastResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/research-agents/{agent_id}:
    get:
      summary: Get a research agent profile
      description: Returns a single agent profile for direct-question workflows.
      tags:
        - ResearchAgents
      parameters:
        - in: path
          name: agent_id
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Research agent profile
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResearchAgentResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/research-agents/{agent_id}/questions:
    get:
      summary: List direct questions for a research agent
      description: |
        Returns questions asked directly to an agent along with the collected answer payloads.
      tags:
        - ResearchAgents
        - AgentQuestions
      parameters:
        - in: path
          name: agent_id
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Agent questions with answers
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentQuestionListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    post:
      summary: Ask a question to a research agent
      description: |
        Sends a single question to a research agent outside of a study.
      tags:
        - ResearchAgents
        - AgentQuestions
      parameters:
        - in: path
          name: agent_id
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AgentQuestionCreateRequest'
      responses:
        '202':
          description: Agent question queued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentQuestionResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/research-studies/search:
    get:
      summary: Search research studies
      description: |
        Keyword search across study titles and objectives with optional filters.
      tags:
        - ResearchStudies
      parameters:
        - in: query
          name: q
          schema:
            type: string
          description: Keyword query.
        - in: query
          name: date_start
          schema:
            type: string
            format: date
          description: Start date filter (inclusive).
        - in: query
          name: date_end
          schema:
            type: string
            format: date
          description: End date filter (inclusive).
        - in: query
          name: audience
          schema:
            type: string
          description: Audience filter (segment or cohort label).
        - in: query
          name: study_type
          schema:
            type: string
          description: Study type filter.
        - in: query
          name: tags
          schema:
            type: array
            items:
              type: string
          style: form
          explode: true
          description: Tags to match (repeatable query param).
        - in: query
          name: limit
          schema:
            type: integer
            minimum: 1
            maximum: 500
        - in: query
          name: offset
          schema:
            type: integer
            minimum: 0
      responses:
        '200':
          description: ResearchStudy search results
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResearchStudySearchResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/research-studies/insights/compare:
    post:
      summary: Compare insights across studies
      description: |
        Returns a side-by-side comparison table, deltas, and caveats for the provided studies.
      tags:
        - ResearchStudies
        - StudyInsights
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StudyInsightsCompareRequest'
      responses:
        '200':
          description: Cross-study comparison insights
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StudyInsightsCompareResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/research-studies/insights/synthesize:
    post:
      summary: Synthesize insights across studies
      description: |
        Generates cross-study patterns, contradictions, and evidence lists from selected studies
        or filter-driven cohorts.
      tags:
        - ResearchStudies
        - StudyInsights
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StudyInsightsSynthesizeRequest'
      responses:
        '200':
          description: Cross-study synthesis insights
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StudyInsightsSynthesizeResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/research-studies:
    get:
      summary: List research studies
      description: |
        Returns research studies for the organization tied to the API key.
        Results include participant and question counts.
      tags:
        - ResearchStudies
      parameters:
        - in: query
          name: limit
          schema:
            type: integer
            minimum: 1
            maximum: 500
        - in: query
          name: offset
          schema:
            type: integer
            minimum: 0
      responses:
        '200':
          description: ResearchStudy list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResearchStudyListResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    post:
      summary: Create a research study
      tags:
        - ResearchStudies
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ResearchStudyCreateRequest'
      responses:
        '201':
          description: ResearchStudy created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResearchStudyResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/research-studies/{study_id}:
    get:
      summary: Get a research study
      description: Returns a single research study for the organization tied to the API key.
      tags:
        - ResearchStudies
      parameters:
        - in: path
          name: study_id
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: ResearchStudy details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResearchStudyDetailResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      summary: Delete a research study
      description: Deletes a study and its related questions/answers. The research group remains untouched.
      tags:
        - ResearchStudies
      parameters:
        - in: path
          name: study_id
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: ResearchStudy deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResearchStudyDeleteResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/research-studies/{study_id}/status:
    get:
      summary: Get study status
      description: |
        Returns stage, counts, percent complete, and status timestamps for the study.
      tags:
        - ResearchStudies
      parameters:
        - in: path
          name: study_id
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Study status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StudyStatusResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/research-studies/{study_id}/cancel:
    post:
      summary: Cancel a research study
      description: |
        Idempotently cancels a study, stopping pending jobs or broadcasts and marking the
        stage as cancelled. Returns the final cancelled status.
      tags:
        - ResearchStudies
      parameters:
        - in: path
          name: study_id
          required: true
          schema:
            type: integer
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StudyCancelRequest'
      responses:
        '200':
          description: Cancelled study status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StudyCancelResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/research-studies/{study_id}/insights:
    get:
      summary: Get study insights
      description: Returns the stored insights payload for the study.
      tags:
        - ResearchStudies
        - StudyInsights
      parameters:
        - in: path
          name: study_id
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Study insights
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StudyInsightsResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    post:
      summary: Generate study insights
      description: |
        Triggers generation or regeneration of the study insights payload and returns
        the generated insights.
      tags:
        - ResearchStudies
        - StudyInsights
      parameters:
        - in: path
          name: study_id
          required: true
          schema:
            type: integer
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StudyInsightsGenerateRequest'
      responses:
        '200':
          description: Study insights generated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StudyInsightsGenerateResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/research-studies/{study_id}/quotes:
    get:
      summary: Get curated study quotes
      description: Returns curated quotes extracted from the study.
      tags:
        - ResearchStudies
        - StudyInsights
      parameters:
        - in: path
          name: study_id
          required: true
          schema:
            type: integer
        - in: query
          name: topic
          schema:
            type: string
          description: Filter quotes to a topic tag.
        - in: query
          name: min_length
          schema:
            type: integer
            minimum: 1
          description: Minimum quote length in characters.
        - in: query
          name: limit
          schema:
            type: integer
            minimum: 1
            maximum: 500
        - in: query
          name: offset
          schema:
            type: integer
            minimum: 0
      responses:
        '200':
          description: Curated quotes
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StudyQuoteListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/research-studies/{study_id}/export:
    post:
      summary: Generate a study export
      description: |
        Generates a PDF, Slides, or Doc export for the study.
      tags:
        - ResearchStudies
        - StudyExports
      parameters:
        - in: path
          name: study_id
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StudyExportRequest'
      responses:
        '202':
          description: Export queued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StudyExportResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/research-studies/{study_id}/export/{export_id}:
    get:
      summary: Get study export status
      description: Returns export status and a download URL when ready.
      tags:
        - ResearchStudies
        - StudyExports
      parameters:
        - in: path
          name: study_id
          required: true
          schema:
            type: integer
        - in: path
          name: export_id
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Export status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StudyExportStatusResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/research-studies/{study_id}/schedule:
    post:
      summary: Schedule a research study run
      description: |
        Schedules a study to run at a future time with a specified timezone.
      tags:
        - ResearchStudies
        - StudyScheduling
      parameters:
        - in: path
          name: study_id
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StudyScheduleRequest'
      responses:
        '201':
          description: Study scheduled
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StudyScheduleResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/research-studies/{study_id}/summary:
    get:
      summary: Get cached study summary
      description: |
        Returns a cached short summary for quick responses. This may be generated from
        the study insights payload.
      tags:
        - ResearchStudies
        - StudyInsights
      parameters:
        - in: path
          name: study_id
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Study summary
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StudySummaryResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/research-studies/{study_id}/agents/remove:
    post:
      summary: Remove agents from a research study
      description: |
        Removes specific agents from a research study. Agents remain in their research groups;
        only the study membership is updated.
      tags:
        - ResearchStudies
      parameters:
        - in: path
          name: study_id
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RemoveStudyAgentsRequest'
      responses:
        '200':
          description: Removal confirmation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RemoveStudyAgentsResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/research-studies/{study_id}/questions:
    get:
      summary: List study questions with answers
      description: |
        Returns all questions for the study with any collected answers. Answers arrive asynchronously,
        so newly created questions may return an empty `answers` list until responses are stored.

        Questions are ordered by creation time (newest first). Answers are ordered by submission time.
      tags:
        - ResearchStudies
        - StudyQuestions
      parameters:
        - in: path
          name: study_id
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Study questions with answers
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StudyQuestionListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    post:
      summary: Create and broadcast a study question
      tags:
        - ResearchStudies
        - StudyQuestions
      parameters:
        - in: path
          name: study_id
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StudyQuestionCreateRequest'
      responses:
        '202':
          description: Broadcast queued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StudyQuestionBroadcastResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/research-studies/{study_id}/complete:
    post:
      summary: Trigger study completion workflow
      description: |
        Finalizes a study and runs the completion analysis pipeline. This action is irreversible and
        should only be called after data collection is complete.

        The completion pipeline generates:
        - Summary: a concise synthesis of the study and question responses.
        - Insights: key themes, notable patterns, and standout participant perspectives.
        - Correlations: relationships between response patterns and participant attributes (when available).
        - Final takeaways: short, action-oriented conclusions for reporting.

        Processing is asynchronous and typically takes 10-15 minutes. Use the job status endpoint
        to monitor completion.
      tags:
        - ResearchStudies
      parameters:
        - in: path
          name: study_id
          required: true
          schema:
            type: integer
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StudyCompletionRequest'
      responses:
        '202':
          description: Workflow queued (async). Returns job IDs and the steps queued.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StudyCompletionResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/research-studies/{study_id}/share:
    get:
      summary: Get study share info
      description: |
        Returns the current share settings and shareable link for a study.
        If sharing is enabled, the URL can be opened by anyone with the link
        (no authentication required) and shows the generated report.
      tags:
        - ResearchStudies
      parameters:
        - in: path
          name: study_id
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Share info
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ShareInfo'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    post:
      summary: Update study share settings
      description: |
        Enables or disables a public share link for the study. When enabled,
        the response includes a URL that can be opened by anyone with the link
        to view the generated report. Treat the link like a secret.
      tags:
        - ResearchStudies
      parameters:
        - in: path
          name: study_id
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ShareUpdateRequest'
      responses:
        '200':
          description: Share info
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ShareInfo'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/zeitgeist/surveys:
    post:
      summary: Create a Zeitgeist survey
      tags:
        - Zeitgeist
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ZeitgeistSurveyCreateRequest'
      responses:
        '201':
          description: Zeitgeist survey created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ZeitgeistSurveyResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/zeitgeist/surveys/{survey_id}:
    delete:
      summary: Delete a Zeitgeist survey
      description: Deletes a survey and its associated answers/results.
      tags:
        - Zeitgeist
      parameters:
        - in: path
          name: survey_id
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Zeitgeist survey deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ZeitgeistSurveyDeleteResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/zeitgeist/surveys/{survey_id}/results:
    get:
      summary: List Zeitgeist survey results
      description: Returns survey result snapshots with pagination and ordering.
      tags:
        - Zeitgeist
      parameters:
        - in: path
          name: survey_id
          required: true
          schema:
            type: integer
        - in: query
          name: page
          schema:
            type: integer
            minimum: 1
        - in: query
          name: page_size
          schema:
            type: integer
            minimum: 1
            maximum: 500
        - in: query
          name: order_by
          schema:
            type: string
            enum: [created_at, period_date, id, title]
        - in: query
          name: order
          schema:
            type: string
            enum: [asc, desc]
      responses:
        '200':
          description: Zeitgeist survey results
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ZeitgeistSurveyResultListResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/jobs/{job_id}:
    get:
      summary: Get job status
      tags:
        - Jobs
      parameters:
        - in: path
          name: job_id
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Job status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobStatus'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          description: Job failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobStatus'
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: APIKey
  responses:
    BadRequest:
      description: Bad request
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Unauthorized:
      description: Unauthorized
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Forbidden:
      description: Forbidden
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    NotFound:
      description: Not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Conflict:
      description: Conflict
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    InternalServerError:
      description: Internal server error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
  schemas:
    CliAuthStartRequest:
      type: object
      required:
        - redirect_uri
      properties:
        provider:
          type: string
          enum: [google]
          description: OAuth provider (defaults to google).
        redirect_uri:
          type: string
          description: Loopback redirect URI (localhost or 127.0.0.1).
    CliAuthStartResponse:
      type: object
      properties:
        ok:
          type: boolean
        session_id:
          type: string
        authorization_url:
          type: string
        expires_in:
          type: integer
      required:
        - ok
        - session_id
        - authorization_url
        - expires_in
    CliAuthCompleteRequest:
      type: object
      required:
        - session_id
        - code
        - state
      properties:
        session_id:
          type: string
        code:
          type: string
        state:
          type: string
    CliAuthCompleteResponse:
      type: object
      properties:
        ok:
          type: boolean
        status:
          type: string
          enum: [complete]
        api_key:
          type: string
        org_uuid:
          type: string
        next_steps:
          $ref: '#/components/schemas/CliAuthNextSteps'
      required:
        - ok
        - status
        - api_key
        - org_uuid
    CliAuthLoginRequiredResponse:
      type: object
      properties:
        ok:
          type: boolean
        status:
          type: string
          enum: [login_required]
        login_url:
          type: string
        next_steps:
          $ref: '#/components/schemas/CliAuthNextSteps'
      required:
        - ok
        - status
        - login_url
    CliAuthNextSteps:
      type: object
      properties:
        summary:
          type: string
        docs_url:
          type: string
    RecruitResearchGroupRequest:
      type: object
      required:
        - name
        - filters
      properties:
        name:
          type: string
        description:
          type: string
          nullable: true
        filters:
          allOf:
            - $ref: '#/components/schemas/RecruitFilters'
          description: |
            Accepts the full filter object (`{ match, filters: {...} }`) or a shorthand
            filter map with fields directly (for example: `{ country: "UK", age_min: 45, age_max: 60 }`).
        group_size:
          type: integer
          minimum: 1
          maximum: 20
          description: Requested group size (1-20).
        sample_method:
          type: string
          enum: [random, stratified]
        dedupe:
          type: boolean
          default: true
    RecruitFilters:
      type: object
      required:
        - filters
      properties:
        match:
          type: string
          enum: [all, any]
          default: all
          description: |
            Controls how multiple filter fields are combined:

            - all: every filter field must match (logical AND across fields)
            - any: at least one filter field must match (logical OR across fields)

            Note: For list filters, the list's `any`/`all` applies within that single field,
            while `match` applies across different fields.
        filters:
          type: object
          description: |
            Filterable agent properties and their types:

            - age: range (min/max)
            - income_annual_usd: range (min/max)
            - income_annual_local_currency: range (min/max)
            - state: list (any/all)
            - city: list (any/all)
            - country: list (any/all)
            - description: text (contains)
            - political_party: list (any/all)
            - religion: list (any/all)
            - industry: list (any/all)
            - occupation: list (any/all)
            - labour_status: list (any/all)
            - ethnicity: list (any/all)
            - gender_identity: list (any/all)
            - sex_at_birth: list (any/all)
            - is_parent: boolean
            - is_female: boolean

            Notes:
            - List filter values are case-insensitive and normalized to lowercase.
            - Range filters accept min and/or max (integers).
            - Boolean filters accept true/false.
            - Text filters accept `contains` (string) and match case-insensitive substrings.
            - Shorthand range keys (`age_min`, `age_max`, `income_annual_usd_min`, etc.)
              are accepted when provided alongside other filter fields.
            - Shorthand text keys (`description_contains`) are accepted in the same way.
            - `description` is reserved for the ResearchGroup description on recruit requests,
              so use nested filters or `description_contains` when filtering by agent description.
            - `state` should use 2-letter state or province codes for US/Canada
              (for example `CA`, `NY`, `ON`).
            - `country` matches against both `country` and `nation` fields and accepts
              common aliases (for example `UK`, `GB`, `United Kingdom`, `US`, `USA`).
          additionalProperties:
            $ref: '#/components/schemas/FilterValue'
    FilterValue:
      oneOf:
        - $ref: '#/components/schemas/RangeFilter'
        - $ref: '#/components/schemas/ListFilter'
        - $ref: '#/components/schemas/TextFilter'
        - type: boolean
        - type: string
    RangeFilter:
      type: object
      properties:
        min:
          type: number
        max:
          type: number
    ListFilter:
      type: object
      properties:
        any:
          type: array
          items:
            type: string
        all:
          type: array
          items:
            type: string
    TextFilter:
      type: object
      properties:
        contains:
          type: string
    ResearchGroup:
      type: object
      properties:
        id:
          type: integer
        uuid:
          type: string
        name:
          type: string
        description:
          type: string
          nullable: true
        agent_count:
          type: integer
        created_at:
          type: string
          format: date-time
    ResearchGroupListResponse:
      type: object
      properties:
        groups:
          type: array
          items:
            $ref: '#/components/schemas/ResearchGroup'
        count:
          type: integer
        total:
          type: integer
    ResearchGroupAgentDetail:
      type: object
      properties:
        name:
          type: string
        age:
          type: integer
        occupation:
          type: string
          nullable: true
        industry:
          type: string
          nullable: true
        city:
          type: string
          nullable: true
        state:
          type: string
          nullable: true
        country:
          type: string
          nullable: true
        sex:
          type: string
          nullable: true
        summary:
          type: string
          nullable: true
        annual_income_usd:
          type: integer
          nullable: true
        marital_status:
          type: string
          nullable: true
        num_children:
          type: integer
          nullable: true
    ResearchGroupDetail:
      type: object
      properties:
        id:
          type: integer
        uuid:
          type: string
        name:
          type: string
        description:
          type: string
          nullable: true
        agent_count:
          type: integer
        agents:
          type: array
          items:
            $ref: '#/components/schemas/ResearchGroupAgentDetail'
    ResearchGroupDetailResponse:
      type: object
      properties:
        group:
          $ref: '#/components/schemas/ResearchGroupDetail'
    RecruitResearchGroupResponse:
      type: object
      properties:
        group:
          $ref: '#/components/schemas/ResearchGroup'
        filters:
          $ref: '#/components/schemas/RecruitFilters'
      example:
        group:
          id: 42
          uuid: rg_123abc
          name: California parents
          description: Parents in CA with kids under 10
          agent_count: 250
        filters:
          match: all
          filters:
            state:
              any:
                - CA
            age:
              min: 25
              max: 45
    AppendResearchGroupRequest:
      type: object
      properties:
        filters:
          allOf:
            - $ref: '#/components/schemas/RecruitFilters'
          description: |
            Accepts the full filter object (`{ match, filters: {...} }`) or a shorthand
            filter map with fields directly (for example: `{ country: "UK", age_min: 45, age_max: 60 }`).
        group_size:
          type: integer
          minimum: 1
          maximum: 20
          description: Requested number of agents to append (1-20).
    AppendResearchGroupResponse:
      type: object
      properties:
        group:
          $ref: '#/components/schemas/ResearchGroup'
        appended:
          type: integer
          description: Number of agents added to the group.
        filters:
          $ref: '#/components/schemas/RecruitFilters'
      example:
        group:
          id: 42
          uuid: rg_123abc
          name: California parents
          description: Parents in CA with kids under 10
          agent_count: 255
        appended: 5
        filters:
          match: all
          filters:
            state:
              any:
                - CA
            age:
              min: 25
              max: 45
    ResearchGroupUpdateRequest:
      type: object
      properties:
        name:
          type: string
        description:
          type: string
          nullable: true
        append:
          type: object
          properties:
            group_size:
              type: integer
              minimum: 1
              maximum: 20
          description: Append additional agents using the group's stored filters.
      description: |
        Update group metadata. Filter changes are not supported.
        Use `append.group_size` to grow the group size with the original filters.
    ResearchGroupUpdateResponse:
      type: object
      properties:
        group:
          $ref: '#/components/schemas/ResearchGroup'
        appended:
          type: integer
          description: Number of agents appended (0 if no append requested).
    ResearchGroupArchiveResponse:
      type: object
      properties:
        group_id:
          type: string
        archived:
          type: boolean
        archived_at:
          type: string
          format: date-time
          nullable: true
    FreeTierQuestionCreateRequest:
      type: object
      required:
        - question
      properties:
        question:
          type: string
        attachments:
          type: array
          items:
            $ref: '#/components/schemas/QuestionAttachmentInput'
    FreeTierQuestionBroadcastResponse:
      type: object
      properties:
        ok:
          type: boolean
        group_id:
          type: integer
        count:
          type: integer
        job_id:
          type: string
        agent_ids:
          type: array
          items:
            type: integer
        agent_uuids:
          type: array
          items:
            type: string
        question:
          type: string
    GroupQuestionCreateRequest:
      type: object
      required:
        - question
      properties:
        question:
          type: string
        attachments:
          type: array
          items:
            $ref: '#/components/schemas/QuestionAttachmentInput'
    GroupQuestionBroadcastResponse:
      type: object
      properties:
        status:
          type: string
        group_id:
          type: string
        question_id:
          type: integer
        job_ids:
          type: array
          items:
            type: string
        agent_count_targeted:
          type: integer
        status_url:
          type: string
          format: uri
    DirectAnswer:
      type: object
      properties:
        response_text:
          type: string
        submitted_at:
          type: string
          format: date-time
          nullable: true
        agent_id:
          type: integer
          nullable: true
        agent_uuid:
          type: string
          nullable: true
        agent_name:
          type: string
          nullable: true
    GroupQuestion:
      type: object
      properties:
        id:
          type: integer
        group_id:
          type: string
        question:
          type: string
        status:
          type: string
        created_at:
          type: string
          format: date-time
          nullable: true
        answer_count:
          type: integer
        answers:
          type: array
          items:
            $ref: '#/components/schemas/DirectAnswer'
    GroupQuestionListResponse:
      type: object
      properties:
        group_id:
          type: string
        count:
          type: integer
        questions:
          type: array
          items:
            $ref: '#/components/schemas/GroupQuestion'
    ResearchAgent:
      type: object
      properties:
        id:
          type: integer
        uuid:
          type: string
        name:
          type: string
        description:
          type: string
          nullable: true
        age:
          type: integer
          nullable: true
        city:
          type: string
          nullable: true
        state:
          type: string
          nullable: true
        country:
          type: string
          nullable: true
        occupation:
          type: string
          nullable: true
        tags:
          type: array
          items:
            type: string
        created_at:
          type: string
          format: date-time
          nullable: true
        updated_at:
          type: string
          format: date-time
          nullable: true
    ResearchAgentResponse:
      type: object
      properties:
        agent:
          $ref: '#/components/schemas/ResearchAgent'
    AgentQuestionCreateRequest:
      type: object
      required:
        - question
      properties:
        question:
          type: string
        attachments:
          type: array
          items:
            $ref: '#/components/schemas/QuestionAttachmentInput'
    AgentQuestionResponse:
      type: object
      properties:
        status:
          type: string
        agent_id:
          type: integer
        question_id:
          type: integer
        job_id:
          type: string
        status_url:
          type: string
          format: uri
        answer:
          allOf:
            - $ref: '#/components/schemas/DirectAnswer'
          nullable: true
    AgentQuestion:
      type: object
      properties:
        id:
          type: integer
        question:
          type: string
        status:
          type: string
        created_at:
          type: string
          format: date-time
          nullable: true
        answer:
          allOf:
            - $ref: '#/components/schemas/DirectAnswer'
          nullable: true
    AgentQuestionListResponse:
      type: object
      properties:
        agent_id:
          type: integer
        count:
          type: integer
        questions:
          type: array
          items:
            $ref: '#/components/schemas/AgentQuestion'
    ResearchStudyCreateRequest:
      type: object
      required:
        - title
        - objective
      properties:
        title:
          type: string
        objective:
          type: string
        shareable:
          type: boolean
        research_group_uuid:
          type: string
    QuestionAttachmentInput:
      type: object
      properties:
        name:
          type: string
        filename:
          type: string
        label:
          type: string
        mime:
          type: string
          description: Allowed values: image/png, image/jpeg, image/gif, image/webp, application/pdf.
        encoding:
          type: string
          enum: [base64]
        data_url:
          type: string
          description: Data URL with base64 payload (for example data:image/png;base64,...).
        file_data:
          type: string
          description: Base64-encoded file contents.
        file_id:
          type: string
          description: Provider file ID (supported for direct agent questions).
        file_url:
          type: string
          format: uri
          description: HTTPS URL to a file (supported for direct agent questions).
        media_asset_id:
          type: string
          description: Existing media asset ID (supported for study questions).
      description: |
        Attach files or images to a question. Allowed types are images (PNG/JPEG/GIF/WEBP) and PDFs.
        For study questions, use `data_url`, `file_data` (base64), or `media_asset_id`.
        For direct agent questions, `file_id` or `file_url` is also accepted; include `mime`
        (or a filename with a supported extension) so the API can validate the file type.
    QuestionAttachment:
      type: object
      properties:
        name:
          type: string
        label:
          type: string
        media_asset_id:
          type: string
        preview_url:
          type: string
          nullable: true
        url:
          type: string
          nullable: true
        mime:
          type: string
        width:
          type: integer
          nullable: true
        height:
          type: integer
          nullable: true
    ResearchStudy:
      type: object
      properties:
        id:
          type: integer
        uuid:
          type: string
        title:
          type: string
        objective:
          type: string
        shareable:
          type: boolean
        organization_id:
          type: integer
        research_group_uuid:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
    ResearchStudyResponse:
      type: object
      properties:
        study:
          $ref: '#/components/schemas/ResearchStudy'
      example:
        study:
          id: 101
          uuid: rs_456def
          title: Q1 caregiver needs
          objective: Understand caregiver priorities in 2026
          shareable: false
          research_group_uuid: rg_123abc
    ResearchStudyListItem:
      type: object
      properties:
        id:
          type: integer
        uuid:
          type: string
        title:
          type: string
        objective:
          type: string
        stage:
          type: string
          nullable: true
        shareable:
          type: boolean
        share_url:
          type: string
          nullable: true
        participant_count:
          type: integer
        question_count:
          type: integer
        created_at:
          type: string
          format: date-time
          nullable: true
        updated_at:
          type: string
          format: date-time
          nullable: true
    ResearchStudyListResponse:
      type: object
      properties:
        studies:
          type: array
          items:
            $ref: '#/components/schemas/ResearchStudyListItem'
        count:
          type: integer
        total:
          type: integer
    ResearchStudySearchResponse:
      type: object
      properties:
        query:
          type: string
          nullable: true
        studies:
          type: array
          items:
            $ref: '#/components/schemas/ResearchStudyListItem'
        count:
          type: integer
        total:
          type: integer
    ResearchStudyDetailResponse:
      type: object
      properties:
        study:
          $ref: '#/components/schemas/ResearchStudyListItem'
    ResearchStudyDeleteResponse:
      type: object
      properties:
        ok:
          type: boolean
        deleted_id:
          type: integer
        deleted_uuid:
          type: string
    ZeitgeistAnswerOptions:
      description: Answer options as a list of labels, nested lists, or a structured object.
      oneOf:
        - type: array
          items:
            type: string
        - type: array
          items:
            type: array
            items:
              type: string
        - type: object
        - type: string
    ZeitgeistResultPayload:
      description: Aggregated counts keyed by answer option, or a structured payload.
      oneOf:
        - type: object
          additionalProperties:
            type: integer
        - type: array
          items:
            type: object
        - type: string
    ZeitgeistSurveyCreateRequest:
      type: object
      required:
        - title
        - question
        - research_group_uuid
      properties:
        title:
          type: string
        question:
          type: string
        description:
          type: string
          nullable: true
        instructions:
          type: string
          nullable: true
        research_group_uuid:
          type: string
        sample_size:
          type: integer
        day_of_week:
          type: integer
          minimum: 1
          maximum: 7
        day_of_month:
          type: integer
          minimum: 1
          maximum: 31
        answer_options:
          $ref: '#/components/schemas/ZeitgeistAnswerOptions'
        answer_colour:
          $ref: '#/components/schemas/ZeitgeistAnswerOptions'
        enabled:
          type: boolean
    ZeitgeistSurvey:
      type: object
      properties:
        id:
          type: integer
        title:
          type: string
        description:
          type: string
          nullable: true
        question:
          type: string
        instructions:
          type: string
          nullable: true
        research_group_id:
          type: integer
        research_group_uuid:
          type: string
        sample_size:
          type: integer
          nullable: true
        day_of_week:
          type: integer
          nullable: true
          minimum: 1
          maximum: 7
        day_of_month:
          type: integer
          nullable: true
          minimum: 1
          maximum: 31
        answer_options:
          $ref: '#/components/schemas/ZeitgeistAnswerOptions'
        answer_colour:
          $ref: '#/components/schemas/ZeitgeistAnswerOptions'
        enabled:
          type: boolean
        created_at:
          type: string
          format: date-time
          nullable: true
        updated_at:
          type: string
          format: date-time
          nullable: true
    ZeitgeistSurveyResponse:
      type: object
      properties:
        survey:
          $ref: '#/components/schemas/ZeitgeistSurvey'
    ZeitgeistSurveyDeleteResponse:
      type: object
      properties:
        ok:
          type: boolean
        deleted_id:
          type: integer
    ZeitgeistSurveyResult:
      type: object
      properties:
        id:
          type: integer
        survey_id:
          type: integer
        title:
          type: string
        period_date:
          type: string
          format: date
          nullable: true
        period_label:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
          nullable: true
        calculated_result:
          type: number
          nullable: true
        result:
          $ref: '#/components/schemas/ZeitgeistResultPayload'
    ZeitgeistSurveyResultListResponse:
      type: object
      properties:
        survey_id:
          type: integer
        results:
          type: array
          items:
            $ref: '#/components/schemas/ZeitgeistSurveyResult'
        count:
          type: integer
        total:
          type: integer
        page:
          type: integer
        page_size:
          type: integer
        order_by:
          type: string
        order:
          type: string
    RemoveGroupAgentsRequest:
      type: object
      required:
        - agent_ids
      properties:
        agent_ids:
          type: array
          items:
            type: integer
    RemoveGroupAgentsResponse:
      type: object
      properties:
        group_id:
          type: integer
        removed_agent_ids:
          type: array
          items:
            type: integer
        removed_count:
          type: integer
    RemoveStudyAgentsRequest:
      type: object
      required:
        - agent_ids
      properties:
        agent_ids:
          type: array
          items:
            type: integer
    RemoveStudyAgentsResponse:
      type: object
      properties:
        study_id:
          type: integer
        removed_agent_ids:
          type: array
          items:
            type: integer
        removed_count:
          type: integer
    StudyQuestionCreateRequest:
      type: object
      required:
        - question
      properties:
        question:
          type: string
        target_group_uuids:
          type: array
          items:
            type: string
        target_agent_uuids:
          type: array
          items:
            type: string
        attachments:
          type: array
          items:
            $ref: '#/components/schemas/QuestionAttachmentInput'
      description: |
        Questions are always broadcast. Provide `target_group_uuids` or `target_agent_uuids`.
        If omitted, the study's attached groups are used. A request with no targets returns 400.
        Allowed attachment types: images (image/png, image/jpeg, image/gif, image/webp) and PDFs (application/pdf).
        For study questions, use `data_url`, `file_data` (base64), or `media_asset_id`.
    StudyQuestion:
      type: object
      properties:
        id:
          type: integer
        study_id:
          type: integer
        question:
          type: string
        question_optimized:
          type: string
          nullable: true
        status:
          type: string
        created_at:
          type: string
          format: date-time
    StudyAnswer:
      type: object
      properties:
        id:
          type: integer
        study_id:
          type: integer
        question_id:
          type: integer
        submitted_at:
          type: string
          format: date-time
          nullable: true
        response_text:
          type: string
        agent_id:
          type: integer
          nullable: true
        agent_uuid:
          type: string
          nullable: true
        agent_name:
          type: string
          nullable: true
        agent_age:
          type: integer
          nullable: true
        agent_city:
          type: string
          nullable: true
        agent_state:
          type: string
          nullable: true
        agent_country:
          type: string
          nullable: true
        agent_location:
          type: string
          nullable: true
        agent_occupation:
          type: string
          nullable: true
        agent_summary:
          type: string
          nullable: true
        agent_elevenlabs_voice_id:
          type: string
          nullable: true
        agent_image:
          type: string
          nullable: true
        audio_file_id:
          type: string
          nullable: true
    StudyQuestionWithAnswers:
      type: object
      properties:
        id:
          type: integer
        study_id:
          type: integer
        question:
          type: string
        optimized_question:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
          nullable: true
        updated_at:
          type: string
          format: date-time
          nullable: true
        answer_count:
          type: integer
        answers:
          type: array
          items:
            $ref: '#/components/schemas/StudyAnswer'
        attachments:
          type: array
          items:
            $ref: '#/components/schemas/QuestionAttachment'
    StudyQuestionListResponse:
      type: object
      properties:
        study_id:
          type: integer
        study_title:
          type: string
        count:
          type: integer
        questions:
          type: array
          items:
            $ref: '#/components/schemas/StudyQuestionWithAnswers'
    StudyQuestionBroadcastResponse:
      type: object
      properties:
        status:
          type: string
          example: queued
        job_ids:
          type: array
          items:
            type: string
        study_id:
          type: integer
        question_id:
          type: integer
        agent_ids:
          type: array
          items:
            type: integer
      example:
        status: queued
        study_id: 101
        question_id: 555
        job_ids:
          - job_001
          - job_002
        agent_ids:
          - 2001
          - 2002
    StudyCompletionRequest:
      type: object
      properties:
        steps:
          type: array
          items:
            type: string
          description: |
            Optional list of pipeline steps to run. Defaults to full pipeline.

            Common step identifiers:
            - study_correlations (correlations)
            - study_recommendations (insights and recommended actions)
            - study_research_summary (summary)
            - study_tldr (final takeaways)
        force:
          type: boolean
          default: false
          description: Re-run steps that already completed.
    StudyCompletionResponse:
      type: object
      properties:
        job_ids:
          type: array
          items:
            type: string
          description: Background job IDs enqueued for the completion pipeline.
        queued_steps:
          type: array
          items:
            type: string
          description: Completion steps queued for execution (may be fewer if not forced).
    StudyStatusResponse:
      type: object
      properties:
        study_id:
          type: integer
        stage:
          type: string
        responded_count:
          type: integer
        participant_count:
          type: integer
        question_count:
          type: integer
        percent_complete:
          type: number
          format: float
          minimum: 0
          maximum: 1
          description: Fraction complete on a 0-1 scale.
        estimated_minutes_remaining:
          type: integer
          nullable: true
        created_at:
          type: string
          format: date-time
          nullable: true
        updated_at:
          type: string
          format: date-time
          nullable: true
        started_at:
          type: string
          format: date-time
          nullable: true
        completed_at:
          type: string
          format: date-time
          nullable: true
        cancelled_at:
          type: string
          format: date-time
          nullable: true
    StudyCancelRequest:
      type: object
      properties:
        reason:
          type: string
    StudyCancelResponse:
      type: object
      properties:
        status:
          $ref: '#/components/schemas/StudyStatusResponse'
        cancel_reason:
          type: string
          nullable: true
        cancelled:
          type: boolean
    StudyInsightsGenerateRequest:
      type: object
      properties:
        include_quotes:
          type: boolean
          default: true
        max_quotes:
          type: integer
          minimum: 1
          maximum: 100
        refresh:
          type: boolean
          default: false
    StudyInsightsGenerateResponse:
      type: object
      properties:
        study_id:
          type: integer
        insights:
          $ref: '#/components/schemas/StudyInsights'
    StudyInsightsResponse:
      type: object
      properties:
        study_id:
          type: integer
        insights:
          $ref: '#/components/schemas/StudyInsights'
    StudyInsights:
      type: object
      properties:
        headline:
          type: string
        tldr_bullets:
          type: array
          items:
            type: string
        themes:
          type: array
          items:
            $ref: '#/components/schemas/StudyInsightTheme'
        segment_deltas:
          type: array
          items:
            $ref: '#/components/schemas/StudyInsightSegmentDelta'
        top_quotes:
          type: array
          items:
            $ref: '#/components/schemas/StudyInsightQuote'
        caveats:
          type: array
          items:
            type: string
        share_url:
          type: string
          nullable: true
        generated_at:
          type: string
          format: date-time
          nullable: true
    StudyInsightTheme:
      type: object
      properties:
        title:
          type: string
        summary:
          type: string
        evidence:
          type: array
          items:
            type: string
    StudyInsightSegmentDelta:
      type: object
      properties:
        segment:
          type: string
        metric:
          type: string
        delta:
          type: string
        summary:
          type: string
    StudyInsightQuote:
      type: object
      properties:
        quote:
          type: string
        question_id:
          type: integer
          nullable: true
        agent_id:
          type: integer
          nullable: true
        topic_tags:
          type: array
          items:
            type: string
        sentiment:
          type: string
          nullable: true
    StudyQuote:
      type: object
      properties:
        id:
          type: integer
        quote:
          type: string
        question_id:
          type: integer
        agent_id:
          type: integer
        topic_tags:
          type: array
          items:
            type: string
        sentiment:
          type: string
          nullable: true
    StudyQuoteListResponse:
      type: object
      properties:
        study_id:
          type: integer
        count:
          type: integer
        total:
          type: integer
        quotes:
          type: array
          items:
            $ref: '#/components/schemas/StudyQuote'
    StudyInsightsCompareRequest:
      type: object
      required:
        - study_ids
      properties:
        study_ids:
          type: array
          minItems: 2
          items:
            type: integer
    StudyComparisonStudy:
      type: object
      properties:
        id:
          type: integer
        title:
          type: string
    StudyComparisonRow:
      type: object
      properties:
        metric:
          type: string
        values:
          type: object
          additionalProperties:
            type: string
        delta:
          type: string
          nullable: true
    StudyComparisonDelta:
      type: object
      properties:
        summary:
          type: string
        evidence:
          type: array
          items:
            type: string
    StudyInsightsCompareResponse:
      type: object
      properties:
        studies:
          type: array
          items:
            $ref: '#/components/schemas/StudyComparisonStudy'
        table:
          type: array
          items:
            $ref: '#/components/schemas/StudyComparisonRow'
        deltas:
          type: array
          items:
            $ref: '#/components/schemas/StudyComparisonDelta'
        caveats:
          type: array
          items:
            type: string
        generated_at:
          type: string
          format: date-time
          nullable: true
    StudySynthesisFilters:
      type: object
      properties:
        date_start:
          type: string
          format: date
        date_end:
          type: string
          format: date
        topic:
          type: string
        audience:
          type: string
        tags:
          type: array
          items:
            type: string
    StudyInsightsSynthesizeRequest:
      type: object
      properties:
        study_ids:
          type: array
          items:
            type: integer
        filters:
          $ref: '#/components/schemas/StudySynthesisFilters'
      description: |
        Provide `study_ids` or `filters` to define the cohort to synthesize.
    StudySynthesisFinding:
      type: object
      properties:
        summary:
          type: string
        studies:
          type: array
          items:
            type: integer
        evidence:
          type: array
          items:
            type: string
    StudySynthesisEvidence:
      type: object
      properties:
        study_id:
          type: integer
        question_id:
          type: integer
          nullable: true
        agent_id:
          type: integer
          nullable: true
        quote:
          type: string
    StudyInsightsSynthesizeResponse:
      type: object
      properties:
        patterns:
          type: array
          items:
            $ref: '#/components/schemas/StudySynthesisFinding'
        contradictions:
          type: array
          items:
            $ref: '#/components/schemas/StudySynthesisFinding'
        evidence:
          type: array
          items:
            $ref: '#/components/schemas/StudySynthesisEvidence'
        caveats:
          type: array
          items:
            type: string
        generated_at:
          type: string
          format: date-time
          nullable: true
    StudyExportRequest:
      type: object
      required:
        - format
      properties:
        format:
          type: string
          enum: [pdf, slides, doc]
        include_insights:
          type: boolean
          default: true
        include_quotes:
          type: boolean
          default: true
    StudyExportResponse:
      type: object
      properties:
        export_id:
          type: string
        status:
          type: string
        status_url:
          type: string
          format: uri
    StudyExportStatusResponse:
      type: object
      properties:
        export_id:
          type: string
        status:
          type: string
        download_url:
          type: string
          format: uri
          nullable: true
        expires_at:
          type: string
          format: date-time
          nullable: true
        error:
          type: string
          nullable: true
    StudyScheduleRequest:
      type: object
      required:
        - run_at
      properties:
        run_at:
          type: string
          format: date-time
        timezone:
          type: string
    StudyScheduleResponse:
      type: object
      properties:
        schedule_id:
          type: string
        status:
          type: string
        scheduled_for:
          type: string
          format: date-time
        timezone:
          type: string
          nullable: true
        status_url:
          type: string
          format: uri
    StudySummaryResponse:
      type: object
      properties:
        study_id:
          type: integer
        summary:
          type: string
        bullets:
          type: array
          items:
            type: string
        generated_at:
          type: string
          format: date-time
          nullable: true
    ShareUpdateRequest:
      type: object
      properties:
        enabled:
          type: boolean
        expires_at:
          type: string
          format: date-time
        shareable_until:
          type: string
          format: date-time
        regenerate:
          type: boolean
      example:
        enabled: true
        shareable_until: 2026-01-31T00:00:00Z
        regenerate: false
    ShareInfo:
      type: object
      properties:
        enabled:
          type: boolean
        url:
          type: string
          nullable: true
        token:
          type: string
          nullable: true
        expires_at:
          type: string
          format: date-time
          nullable: true
        is_expired:
          type: boolean
    JobStatus:
      type: object
      properties:
        status:
          type: string
        result:
          type: object
          nullable: true
        error:
          type: string
          nullable: true
        meta:
          type: object
      example:
        status: finished
        result: {}
        meta: {}
    ErrorResponse:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
            message:
              type: string
            details:
              type: object
