openapi: 3.1.0
info:
  title: Workflow Platform API
  version: 1.0.0
  description: |
    REST API for the workflow platform: projects, issues, configurable workflows, boards, sprints,
    releases, search (a JQL-like query language), Git integration, provisioning and automation.

    Authenticate with the browser session cookie or a personal access token
    (`Authorization: Bearer wfm_pat_...`). Cookie-authenticated mutations must send
    `X-Requested-With: XMLHttpRequest`. OAuth access tokens issued to AI assistants
    (`wfm_oat_...`) are valid for `/mcp` only.

    Errors use RFC 9457 problem details (`application/problem+json`).
servers:
  - url: /api/v1
security:
  - cookieAuth: []
  - bearerAuth: []
tags:
  - name: auth
  - name: me
  - name: users
  - name: admin
  - name: projects
  - name: workflows
  - name: boards
  - name: sprints
  - name: releases
  - name: programmes
    description: Initiatives in a programme project that team projects' epics work toward.
  - name: issues
  - name: search
  - name: dashboards
  - name: metrics
  - name: scm
  - name: automation
  - name: webhooks
  - name: fields
  - name: handwritten
    description: Implemented outside the generated server (streaming).

paths:
  # ---------------------------------------------------------------------------
  # Auth & current user
  # ---------------------------------------------------------------------------
  /auth/providers:
    get:
      operationId: listAuthProviders
      tags: [auth]
      security: []
      summary: Sign-in options for the login page
      responses:
        "200":
          description: Enabled providers
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AuthProviders' }
        default: { $ref: '#/components/responses/Problem' }
  /auth/dev-login:
    post:
      operationId: devLogin
      tags: [auth]
      security: []
      summary: Passwordless login for local development (disabled in production)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email: { type: string, format: email }
                displayName: { type: string }
      responses:
        "200":
          description: Signed in
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Me' }
        default: { $ref: '#/components/responses/Problem' }
  /auth/local/login:
    post:
      operationId: localLogin
      tags: [auth]
      security: []
      summary: Sign in with an email address and a password (local accounts)
      description: |
        On success the session cookie is set (`status: signed_in`). Otherwise the result says what
        comes next - `totp_required`, `password_change_required` (a temporary password) or
        `totp_enrollment_required` (`WFM_LOCAL_REQUIRE_2FA`) - and an HttpOnly cookie carries the
        unfinished sign-in to that step. Wrong credentials always get the same 401. Throttled per
        client address and per email (429 with `Retry-After`); repeated failures lock the account
        for a while (429, code `locked`). 404 when `WFM_LOCAL_LOGIN=false`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password]
              properties:
                email: { type: string }
                password: { type: string }
      responses:
        "200":
          description: Signed in, or the next step
          content:
            application/json:
              schema: { $ref: '#/components/schemas/LocalLoginResult' }
        default: { $ref: '#/components/responses/Problem' }
  /auth/local/login/totp:
    post:
      operationId: localLoginTotp
      tags: [auth]
      security: []
      summary: Second sign-in step - an authenticator code or a recovery code
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code]
              properties:
                code: { type: string, description: '6-digit code, or a recovery code such as k7dmr-x3qpa' }
      responses:
        "200":
          description: Signed in, or the next step
          content:
            application/json:
              schema: { $ref: '#/components/schemas/LocalLoginResult' }
        default: { $ref: '#/components/responses/Problem' }
  /auth/local/login/new-password:
    post:
      operationId: localLoginNewPassword
      tags: [auth]
      security: []
      summary: Replace a temporary password during sign-in
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [newPassword]
              properties:
                newPassword: { type: string }
      responses:
        "200":
          description: Signed in, or the next step
          content:
            application/json:
              schema: { $ref: '#/components/schemas/LocalLoginResult' }
        default: { $ref: '#/components/responses/Problem' }
  /auth/local/login/totp-setup:
    post:
      operationId: localLoginTotpSetup
      tags: [auth]
      security: []
      summary: Start the two-factor enrolment required at sign-in
      responses:
        "200":
          description: The secret to add to an authenticator app
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TotpSetup' }
        default: { $ref: '#/components/responses/Problem' }
  /auth/local/login/totp-setup/confirm:
    post:
      operationId: localLoginTotpConfirm
      tags: [auth]
      security: []
      summary: Finish two-factor enrolment at sign-in; the result carries the recovery codes once
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code]
              properties:
                code: { type: string }
      responses:
        "200":
          description: Signed in
          content:
            application/json:
              schema: { $ref: '#/components/schemas/LocalLoginResult' }
        default: { $ref: '#/components/responses/Problem' }
  /auth/local/forgot-password:
    post:
      operationId: localForgotPassword
      tags: [auth]
      security: []
      summary: Email a password-reset link (valid for one hour) to a local account
      description: Answers 202 whether or not an account exists for the address.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email: { type: string }
      responses:
        "202": { description: If the address has a local account, a link is on its way }
        default: { $ref: '#/components/responses/Problem' }
  /auth/local/link:
    post:
      operationId: localLinkInfo
      tags: [auth]
      security: []
      summary: Check an invitation or password-reset link
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [token]
              properties:
                token: { type: string }
      responses:
        "200":
          description: Who the link is for (404 `invalid_link` when used, expired or unknown)
          content:
            application/json:
              schema: { $ref: '#/components/schemas/LocalLink' }
        default: { $ref: '#/components/responses/Problem' }
  /auth/local/link/redeem:
    post:
      operationId: localRedeemLink
      tags: [auth]
      security: []
      summary: Choose a password with an invitation or reset link (single use)
      description: Ends every session of the person; they then sign in with the new password (and two-factor if on).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [token, password]
              properties:
                token: { type: string }
                password: { type: string }
      responses:
        "200":
          description: Password set
          content:
            application/json:
              schema: { $ref: '#/components/schemas/LocalLink' }
        default: { $ref: '#/components/responses/Problem' }
  /auth/local/password-check:
    post:
      operationId: localCheckPassword
      tags: [auth]
      security: []
      summary: Rate a password against the policy while someone types it
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [password]
              properties:
                password: { type: string }
                email: { type: string }
                displayName: { type: string }
      responses:
        "200":
          description: Strength and policy problems
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PasswordCheck' }
        default: { $ref: '#/components/responses/Problem' }
  /auth/logout:
    post:
      operationId: logout
      tags: [auth]
      description: >-
        Ends the session. When it came from a sign-in provider with single logout on
        (WFM_OIDC_<ID>_SINGLE_LOGOUT), the response is the provider's end-session URL to send the
        browser to, so the person is signed out there too and comes back to /login.
      responses:
        "200":
          description: Signed out here; finish at the sign-in provider
          content:
            application/json:
              schema:
                type: object
                required: [redirectUrl]
                properties:
                  redirectUrl: { type: string, format: uri }
        "204": { description: Signed out }
        default: { $ref: '#/components/responses/Problem' }
  /me:
    get:
      operationId: getMe
      tags: [me]
      responses:
        "200":
          description: The signed-in user
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Me' }
        default: { $ref: '#/components/responses/Problem' }
  /me/tokens:
    get:
      operationId: listMyTokens
      tags: [me]
      responses:
        "200":
          description: Personal access tokens
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/ApiToken' }
        default: { $ref: '#/components/responses/Problem' }
    post:
      operationId: createMyToken
      tags: [me]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ApiTokenCreate' }
      responses:
        "201":
          description: Created; the token value is only returned once
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiTokenCreated' }
        default: { $ref: '#/components/responses/Problem' }
  /me/tokens/{tokenId}:
    delete:
      operationId: revokeMyToken
      tags: [me]
      parameters: [{ $ref: '#/components/parameters/TokenId' }]
      responses:
        "204": { description: Revoked }
        default: { $ref: '#/components/responses/Problem' }
  /me/security:
    get:
      operationId: getMySecurity
      tags: [me]
      summary: How the signed-in person signs in (password, two-factor)
      responses:
        "200":
          description: Sign-in and two-factor status
          content:
            application/json:
              schema: { $ref: '#/components/schemas/MySecurity' }
        default: { $ref: '#/components/responses/Problem' }
  /me/password:
    post:
      operationId: changeMyPassword
      tags: [me]
      summary: Change your password (local accounts); ends your other sessions
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [currentPassword, newPassword]
              properties:
                currentPassword: { type: string }
                newPassword: { type: string }
      responses:
        "204": { description: Changed }
        default: { $ref: '#/components/responses/Problem' }
  /me/totp/setup:
    post:
      operationId: setupMyTotp
      tags: [me]
      summary: Start setting up two-factor authentication
      responses:
        "200":
          description: The secret to add to an authenticator app; confirm it with a code
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TotpSetup' }
        default: { $ref: '#/components/responses/Problem' }
  /me/totp/confirm:
    post:
      operationId: confirmMyTotp
      tags: [me]
      summary: Turn two-factor authentication on with a code from the new secret
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code]
              properties:
                code: { type: string }
      responses:
        "200":
          description: On; the recovery codes are shown only now
          content:
            application/json:
              schema: { $ref: '#/components/schemas/RecoveryCodes' }
        default: { $ref: '#/components/responses/Problem' }
  /me/totp/disable:
    post:
      operationId: disableMyTotp
      tags: [me]
      summary: Turn two-factor authentication off (needs your password and a code)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [password, code]
              properties:
                password: { type: string }
                code: { type: string, description: Authenticator code or a recovery code }
      responses:
        "204": { description: Off }
        default: { $ref: '#/components/responses/Problem' }
  /me/totp/recovery-codes:
    post:
      operationId: regenerateMyRecoveryCodes
      tags: [me]
      summary: Replace your recovery codes (needs a code)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code]
              properties:
                code: { type: string }
      responses:
        "200":
          description: New codes; the old ones stop working
          content:
            application/json:
              schema: { $ref: '#/components/schemas/RecoveryCodes' }
        default: { $ref: '#/components/responses/Problem' }
  /notifications:
    get:
      operationId: listNotifications
      tags: [me]
      parameters:
        - { name: unread, in: query, schema: { type: boolean } }
        - { $ref: '#/components/parameters/Limit' }
      responses:
        "200":
          description: Notifications, newest first
          content:
            application/json:
              schema: { $ref: '#/components/schemas/NotificationList' }
        default: { $ref: '#/components/responses/Problem' }
  /notifications/read:
    post:
      operationId: markNotificationsRead
      tags: [me]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                ids:
                  description: Omit to mark everything read.
                  type: array
                  items: { type: integer, format: int64 }
      responses:
        "204": { description: Marked read }
        default: { $ref: '#/components/responses/Problem' }

  # ---------------------------------------------------------------------------
  # Email notifications
  # ---------------------------------------------------------------------------
  /me/email-notifications:
    get:
      operationId: getMyEmailNotifications
      tags: [me]
      summary: Your email notification settings
      responses:
        "200":
          description: Settings (the defaults until you change something)
          content:
            application/json:
              schema: { $ref: '#/components/schemas/EmailNotificationSettings' }
        default: { $ref: '#/components/responses/Problem' }
    patch:
      operationId: updateMyEmailNotifications
      tags: [me]
      summary: Change your email notification settings
      description: Only the fields sent change; preferences are merged by kind.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/EmailNotificationSettingsPatch' }
      responses:
        "200":
          description: The saved settings
          content:
            application/json:
              schema: { $ref: '#/components/schemas/EmailNotificationSettings' }
        default: { $ref: '#/components/responses/Problem' }
  /admin/email:
    get:
      operationId: adminGetEmailStatus
      tags: [admin]
      summary: How outgoing email is configured
      responses:
        "200":
          description: Email configuration (the SMTP password is never returned)
          content:
            application/json:
              schema: { $ref: '#/components/schemas/EmailStatus' }
        default: { $ref: '#/components/responses/Problem' }
  /admin/email/test:
    post:
      operationId: adminSendTestEmail
      tags: [admin]
      summary: Send a test email to yourself
      description: Sent right away rather than queued, so SMTP errors come back in the response (502).
      responses:
        "200":
          description: Sent (or kept in memory when no SMTP server is configured)
          content:
            application/json:
              schema: { $ref: '#/components/schemas/EmailTestResult' }
        default: { $ref: '#/components/responses/Problem' }

  # ---------------------------------------------------------------------------
  # Directory
  # ---------------------------------------------------------------------------
  /users:
    get:
      operationId: listUsers
      tags: [users]
      parameters:
        - { name: q, in: query, schema: { type: string } }
        - { name: project, in: query, description: Only users who can be assigned in this project (never customers), schema: { type: string } }
        - name: customers
          in: query
          description: |
            false leaves out customers (people who only use a help centre), for pickers of people
            who can become project members. Customers are never listed with project.
          schema: { type: boolean, default: true }
        - { $ref: '#/components/parameters/Limit' }
      responses:
        "200":
          description: Active users
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/User' }
        default: { $ref: '#/components/responses/Problem' }
  /groups:
    get:
      operationId: listGroups
      tags: [users]
      parameters:
        - { name: q, in: query, schema: { type: string } }
      responses:
        "200":
          description: Groups
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/Group' }
        default: { $ref: '#/components/responses/Problem' }
  /groups/{groupId}:
    get:
      operationId: getGroup
      tags: [users]
      parameters: [{ $ref: '#/components/parameters/GroupId' }]
      responses:
        "200":
          description: Group with members
          content:
            application/json:
              schema: { $ref: '#/components/schemas/GroupDetail' }
        default: { $ref: '#/components/responses/Problem' }
  /roles:
    get:
      operationId: listRoles
      tags: [users]
      responses:
        "200":
          description: Project roles
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/Role' }
        default: { $ref: '#/components/responses/Problem' }
  /permissions:
    get:
      operationId: listPermissions
      tags: [users]
      responses:
        "200":
          description: Named permissions that roles can hold
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/PermissionInfo' }
        default: { $ref: '#/components/responses/Problem' }
  /statuses:
    get:
      operationId: listStatuses
      tags: [workflows]
      responses:
        "200":
          description: All statuses across workflows
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/Status' }
        default: { $ref: '#/components/responses/Problem' }

  # ---------------------------------------------------------------------------
  # Administration
  # ---------------------------------------------------------------------------
  /admin/users:
    get:
      operationId: adminListUsers
      tags: [admin]
      parameters:
        - { name: q, in: query, schema: { type: string } }
        - { name: kind, in: query, schema: { $ref: '#/components/schemas/UserKind' } }
      responses:
        "200":
          description: All users including inactive
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/AdminUser' }
        default: { $ref: '#/components/responses/Problem' }
    post:
      operationId: adminCreateUser
      tags: [admin]
      description: Pre-provision a person, or create a service account (`kind=service`) for CI and the CLI.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [displayName]
              properties:
                kind: { $ref: '#/components/schemas/UserKind' }
                email: { type: string, format: email }
                displayName: { type: string, minLength: 1, maxLength: 200 }
                orgRole: { $ref: '#/components/schemas/OrgRole' }
                groupIds:
                  description: Local groups to add the person to.
                  type: array
                  items: { type: string, format: uuid }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AdminUser' }
        default: { $ref: '#/components/responses/Problem' }
  /admin/users/{userId}:
    patch:
      operationId: adminUpdateUser
      tags: [admin]
      parameters: [{ $ref: '#/components/parameters/UserId' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                displayName: { type: string, minLength: 1, maxLength: 200 }
                orgRole: { $ref: '#/components/schemas/OrgRole' }
                active: { type: boolean }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AdminUser' }
        default: { $ref: '#/components/responses/Problem' }
  /admin/users/{userId}/invitation:
    post:
      operationId: adminInviteUser
      tags: [admin]
      summary: Email a person an invitation to choose a password (makes them a local account)
      description: |
        A new invitation replaces any open one. Refused for people managed by SCIM or Google
        directory sync. emailed is false when the server has no SMTP server: the invitation exists
        but nobody receives it (set a temporary password instead, or set up email and resend it).
      parameters: [{ $ref: '#/components/parameters/UserId' }]
      responses:
        "200":
          description: Created, and emailed when the server can send email
          content:
            application/json:
              schema:
                type: object
                required: [expiresAt, emailed]
                properties:
                  expiresAt: { type: string, format: date-time }
                  emailed: { $ref: '#/components/schemas/Emailed' }
        default: { $ref: '#/components/responses/Problem' }
    delete:
      operationId: adminRevokeInvitation
      tags: [admin]
      parameters: [{ $ref: '#/components/parameters/UserId' }]
      responses:
        "204": { description: Revoked }
        default: { $ref: '#/components/responses/Problem' }
  /admin/users/{userId}/temporary-password:
    post:
      operationId: adminSetTemporaryPassword
      tags: [admin]
      summary: Set a generated temporary password (returned once; changed at next sign-in)
      parameters: [{ $ref: '#/components/parameters/UserId' }]
      responses:
        "200":
          description: The temporary password
          content:
            application/json:
              schema:
                type: object
                required: [password]
                properties:
                  password: { type: string }
        default: { $ref: '#/components/responses/Problem' }
  /admin/users/{userId}/password-reset:
    post:
      operationId: adminSendPasswordReset
      tags: [admin]
      summary: Email a local account a password-reset link
      description: emailed is false when the server has no SMTP server, so the link reaches nobody.
      parameters: [{ $ref: '#/components/parameters/UserId' }]
      responses:
        "202":
          description: Created, and emailed when the server can send email
          content:
            application/json:
              schema:
                type: object
                required: [emailed]
                properties:
                  emailed: { $ref: '#/components/schemas/Emailed' }
        default: { $ref: '#/components/responses/Problem' }
  /admin/users/{userId}/unlock:
    post:
      operationId: adminUnlockUser
      tags: [admin]
      summary: Clear a lockout after repeated failed sign-ins
      parameters: [{ $ref: '#/components/parameters/UserId' }]
      responses:
        "204": { description: Unlocked }
        default: { $ref: '#/components/responses/Problem' }
  /admin/users/{userId}/totp:
    delete:
      operationId: adminResetTotp
      tags: [admin]
      summary: Turn off a person's two-factor authentication (lost device)
      parameters: [{ $ref: '#/components/parameters/UserId' }]
      responses:
        "204": { description: Reset }
        default: { $ref: '#/components/responses/Problem' }
  /admin/users/{userId}/password:
    delete:
      operationId: adminRemovePassword
      tags: [admin]
      summary: Remove a person's password and two-factor (back to single sign-on only)
      parameters: [{ $ref: '#/components/parameters/UserId' }]
      responses:
        "204": { description: Removed }
        default: { $ref: '#/components/responses/Problem' }
  /admin/users/{userId}/tokens:
    get:
      operationId: adminListUserTokens
      tags: [admin]
      parameters: [{ $ref: '#/components/parameters/UserId' }]
      responses:
        "200":
          description: Tokens owned by the user
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/ApiToken' }
        default: { $ref: '#/components/responses/Problem' }
    post:
      operationId: adminCreateUserToken
      tags: [admin]
      description: Issue a token for a service account.
      parameters: [{ $ref: '#/components/parameters/UserId' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ApiTokenCreate' }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiTokenCreated' }
        default: { $ref: '#/components/responses/Problem' }
  /admin/tokens/{tokenId}:
    delete:
      operationId: adminRevokeToken
      tags: [admin]
      parameters: [{ $ref: '#/components/parameters/TokenId' }]
      responses:
        "204": { description: Revoked }
        default: { $ref: '#/components/responses/Problem' }
  /admin/groups:
    post:
      operationId: adminCreateGroup
      tags: [admin]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [displayName]
              properties:
                displayName: { type: string, minLength: 1, maxLength: 200 }
                description: { type: string }
                orgRole: { $ref: '#/components/schemas/OrgRole' }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Group' }
        default: { $ref: '#/components/responses/Problem' }
  /admin/groups/{groupId}:
    patch:
      operationId: adminUpdateGroup
      tags: [admin]
      description: Provisioned groups only accept `orgRole` (a local mapping); local groups accept everything.
      parameters: [{ $ref: '#/components/parameters/GroupId' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                displayName: { type: string, minLength: 1, maxLength: 200 }
                description: { type: string }
                orgRole:
                  oneOf:
                    - $ref: '#/components/schemas/OrgRole'
                    - type: 'null'
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Group' }
        default: { $ref: '#/components/responses/Problem' }
    delete:
      operationId: adminDeleteGroup
      tags: [admin]
      parameters: [{ $ref: '#/components/parameters/GroupId' }]
      responses:
        "204": { description: Deleted }
        default: { $ref: '#/components/responses/Problem' }
  /admin/groups/{groupId}/members/{userId}:
    put:
      operationId: adminAddGroupMember
      tags: [admin]
      parameters: [{ $ref: '#/components/parameters/GroupId' }, { $ref: '#/components/parameters/UserId' }]
      responses:
        "204": { description: Added }
        default: { $ref: '#/components/responses/Problem' }
    delete:
      operationId: adminRemoveGroupMember
      tags: [admin]
      parameters: [{ $ref: '#/components/parameters/GroupId' }, { $ref: '#/components/parameters/UserId' }]
      responses:
        "204": { description: Removed }
        default: { $ref: '#/components/responses/Problem' }
  /admin/roles:
    post:
      operationId: adminCreateRole
      tags: [admin]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/RoleInput' }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Role' }
        default: { $ref: '#/components/responses/Problem' }
  /admin/roles/{roleId}:
    put:
      operationId: adminUpdateRole
      tags: [admin]
      parameters: [{ $ref: '#/components/parameters/RoleId' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/RoleInput' }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Role' }
        default: { $ref: '#/components/responses/Problem' }
    delete:
      operationId: adminDeleteRole
      tags: [admin]
      parameters: [{ $ref: '#/components/parameters/RoleId' }]
      responses:
        "204": { description: Deleted }
        default: { $ref: '#/components/responses/Problem' }
  /admin/provisioning:
    get:
      operationId: adminGetProvisioning
      tags: [admin]
      summary: Google sign-in, SCIM and directory sync configuration and status
      responses:
        "200":
          description: Provisioning overview
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ProvisioningStatus' }
        default: { $ref: '#/components/responses/Problem' }
  /admin/scim-connections:
    post:
      operationId: adminCreateScimConnection
      tags: [admin]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string, minLength: 1, maxLength: 100 }
      responses:
        "201":
          description: Created; the bearer token is only returned once
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScimConnectionCreated' }
        default: { $ref: '#/components/responses/Problem' }
  /admin/scim-connections/{connectionId}:
    delete:
      operationId: adminRevokeScimConnection
      tags: [admin]
      parameters: [{ $ref: '#/components/parameters/ConnectionId' }]
      responses:
        "204": { description: Revoked }
        default: { $ref: '#/components/responses/Problem' }
  /admin/scim-connections/{connectionId}/rotate:
    post:
      operationId: adminRotateScimConnection
      tags: [admin]
      parameters: [{ $ref: '#/components/parameters/ConnectionId' }]
      responses:
        "200":
          description: New token
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScimConnectionCreated' }
        default: { $ref: '#/components/responses/Problem' }
  /admin/directory-sync/run:
    post:
      operationId: adminRunDirectorySync
      tags: [admin]
      responses:
        "202":
          description: Sync job enqueued
          content:
            application/json:
              schema:
                type: object
                required: [jobId]
                properties:
                  jobId: { type: integer, format: int64 }
        default: { $ref: '#/components/responses/Problem' }
  /admin/audit:
    get:
      operationId: adminListAudit
      tags: [admin]
      parameters:
        - { name: action, in: query, description: Prefix match, e.g. "scim." , schema: { type: string } }
        - { name: actorId, in: query, schema: { type: string, format: uuid } }
        - { name: before, in: query, description: Return events with id lower than this, schema: { type: integer, format: int64 } }
        - { $ref: '#/components/parameters/Limit' }
      responses:
        "200":
          description: Audit events, newest first
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/AuditEvent' }
        default: { $ref: '#/components/responses/Problem' }
  /admin/scm-connections:
    get:
      operationId: adminListScmConnections
      tags: [scm]
      responses:
        "200":
          description: GitHub Apps and GitLab instances
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/ScmConnection' }
        default: { $ref: '#/components/responses/Problem' }
    post:
      operationId: adminCreateScmConnection
      tags: [scm]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ScmConnectionInput' }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScmConnectionCreated' }
        default: { $ref: '#/components/responses/Problem' }
  /admin/scm-connections/{connectionId}:
    patch:
      operationId: adminUpdateScmConnection
      tags: [scm]
      parameters: [{ $ref: '#/components/parameters/ConnectionId' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ScmConnectionPatch' }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScmConnection' }
        default: { $ref: '#/components/responses/Problem' }
    delete:
      operationId: adminDeleteScmConnection
      tags: [scm]
      parameters: [{ $ref: '#/components/parameters/ConnectionId' }]
      responses:
        "204": { description: Deleted }
        default: { $ref: '#/components/responses/Problem' }
  /admin/scm-connections/{connectionId}/sync:
    post:
      operationId: adminSyncScmConnection
      tags: [scm]
      summary: Discover repositories and reconcile links from the provider API
      parameters: [{ $ref: '#/components/parameters/ConnectionId' }]
      responses:
        "202":
          description: Reconcile job enqueued
          content:
            application/json:
              schema:
                type: object
                required: [jobId]
                properties:
                  jobId: { type: integer, format: int64 }
        default: { $ref: '#/components/responses/Problem' }
  /admin/scm-connections/{connectionId}/events:
    get:
      operationId: adminListWebhookEvents
      tags: [scm]
      parameters: [{ $ref: '#/components/parameters/ConnectionId' }, { $ref: '#/components/parameters/Limit' }]
      responses:
        "200":
          description: Recent inbound webhook deliveries
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/WebhookEvent' }
        default: { $ref: '#/components/responses/Problem' }
  /admin/webhook-events/{eventId}/reprocess:
    post:
      operationId: adminReprocessWebhookEvent
      tags: [scm]
      parameters: [{ $ref: '#/components/parameters/EventId' }]
      responses:
        "202": { description: Re-queued }
        default: { $ref: '#/components/responses/Problem' }
  # Import from Jira (organisation admins): connect, choose projects, scan, map, dry-run, import.
  /admin/jira-imports:
    get:
      operationId: adminListJiraImports
      tags: [admin]
      summary: Imports from Jira, newest first
      responses:
        "200":
          description: Imports (without scan details)
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/JiraImport' }
        default: { $ref: '#/components/responses/Problem' }
    post:
      operationId: adminCreateJiraImport
      tags: [admin]
      summary: Connect a Jira site (the connection is tested first; the token is stored encrypted)
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/JiraConnectionInput' }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/JiraImport' }
        default: { $ref: '#/components/responses/Problem' }
  /admin/jira-imports/test-connection:
    post:
      operationId: adminTestJiraConnection
      tags: [admin]
      summary: Check a Jira site URL and credentials without saving them
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/JiraConnectionInput' }
      responses:
        "200":
          description: The site answered and accepted the credentials
          content:
            application/json:
              schema: { $ref: '#/components/schemas/JiraConnectionTest' }
        default: { $ref: '#/components/responses/Problem' }
  /admin/jira-imports/{importId}:
    parameters:
      - { name: importId, in: path, required: true, schema: { type: string, format: uuid } }
    get:
      operationId: adminGetJiraImport
      tags: [admin]
      summary: An import with its configuration (unset mappings pre-filled), scan, targets and progress
      responses:
        "200":
          description: The import
          content:
            application/json:
              schema: { $ref: '#/components/schemas/JiraImport' }
        default: { $ref: '#/components/responses/Problem' }
    patch:
      operationId: adminUpdateJiraImport
      tags: [admin]
      summary: Save the projects and mapping, or enter new credentials
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/JiraImportPatch' }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema: { $ref: '#/components/schemas/JiraImport' }
        default: { $ref: '#/components/responses/Problem' }
    delete:
      operationId: adminDeleteJiraImport
      tags: [admin]
      summary: Delete the import record (imported data stays; re-importing the site still updates instead of duplicating)
      responses:
        "204": { description: Deleted }
        default: { $ref: '#/components/responses/Problem' }
  /admin/jira-imports/{importId}/credentials:
    parameters:
      - { name: importId, in: path, required: true, schema: { type: string, format: uuid } }
    delete:
      operationId: adminDeleteJiraImportCredentials
      tags: [admin]
      summary: Forget the stored Jira token
      responses:
        "200":
          description: Credentials deleted
          content:
            application/json:
              schema: { $ref: '#/components/schemas/JiraImport' }
        default: { $ref: '#/components/responses/Problem' }
  /admin/jira-imports/{importId}/jira-projects:
    parameters:
      - { name: importId, in: path, required: true, schema: { type: string, format: uuid } }
    get:
      operationId: adminListJiraProjects
      tags: [admin]
      summary: Projects the Jira account can browse
      responses:
        "200":
          description: Jira projects
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/JiraProject' }
        default: { $ref: '#/components/responses/Problem' }
  /admin/jira-imports/{importId}/scan:
    parameters:
      - { name: importId, in: path, required: true, schema: { type: string, format: uuid } }
    post:
      operationId: adminScanJiraImport
      tags: [admin]
      summary: Analyse the selected projects in the background (counts, values in use, people)
      responses:
        "202":
          description: Scan started; poll the import for progress
          content:
            application/json:
              schema: { $ref: '#/components/schemas/JiraImport' }
        default: { $ref: '#/components/responses/Problem' }
  /admin/jira-imports/{importId}/dry-run:
    parameters:
      - { name: importId, in: path, required: true, schema: { type: string, format: uuid } }
    post:
      operationId: adminDryRunJiraImport
      tags: [admin]
      summary: What the import would do and the problems it would meet, without writing anything
      responses:
        "200":
          description: Dry run
          content:
            application/json:
              schema: { $ref: '#/components/schemas/JiraDryRun' }
        default: { $ref: '#/components/responses/Problem' }
  /admin/jira-imports/{importId}/run:
    parameters:
      - { name: importId, in: path, required: true, schema: { type: string, format: uuid } }
    post:
      operationId: adminRunJiraImport
      tags: [admin]
      summary: Start the import, or resume an interrupted one, in the background
      responses:
        "202":
          description: Import started; poll the import for progress
          content:
            application/json:
              schema: { $ref: '#/components/schemas/JiraImport' }
        default: { $ref: '#/components/responses/Problem' }
  /admin/jira-imports/{importId}/cancel:
    parameters:
      - { name: importId, in: path, required: true, schema: { type: string, format: uuid } }
    post:
      operationId: adminCancelJiraImport
      tags: [admin]
      summary: Stop the running scan or import after its current step
      responses:
        "200":
          description: Cancel requested
          content:
            application/json:
              schema: { $ref: '#/components/schemas/JiraImport' }
        default: { $ref: '#/components/responses/Problem' }
  /scm/repositories:
    get:
      operationId: listScmRepositories
      tags: [scm]
      parameters:
        - { name: q, in: query, schema: { type: string } }
      responses:
        "200":
          description: Repositories known from all connections
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/ScmRepository' }
        default: { $ref: '#/components/responses/Problem' }

  # ---------------------------------------------------------------------------
  # Custom fields
  # ---------------------------------------------------------------------------
  /fields:
    get:
      operationId: listFields
      tags: [fields]
      responses:
        "200":
          description: Custom field definitions
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/FieldDefinition' }
        default: { $ref: '#/components/responses/Problem' }
    post:
      operationId: createField
      tags: [fields]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/FieldDefinitionInput' }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/FieldDefinition' }
        default: { $ref: '#/components/responses/Problem' }
  /fields/{fieldId}:
    put:
      operationId: updateField
      tags: [fields]
      parameters: [{ $ref: '#/components/parameters/FieldId' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/FieldDefinitionInput' }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema: { $ref: '#/components/schemas/FieldDefinition' }
        default: { $ref: '#/components/responses/Problem' }
    delete:
      operationId: deleteField
      tags: [fields]
      parameters: [{ $ref: '#/components/parameters/FieldId' }]
      responses:
        "204": { description: Deleted }
        default: { $ref: '#/components/responses/Problem' }

  # ---------------------------------------------------------------------------
  # Projects
  # ---------------------------------------------------------------------------
  /project-templates:
    get:
      operationId: listProjectTemplates
      tags: [projects]
      responses:
        "200":
          description: Templates a project can be created from
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/ProjectTemplate' }
        default: { $ref: '#/components/responses/Problem' }
  /projects:
    get:
      operationId: listProjects
      tags: [projects]
      responses:
        "200":
          description: Projects the caller can browse
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/Project' }
        default: { $ref: '#/components/responses/Problem' }
    post:
      operationId: createProject
      tags: [projects]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ProjectCreate' }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Project' }
        default: { $ref: '#/components/responses/Problem' }
  /projects/{projectKey}:
    get:
      operationId: getProject
      tags: [projects]
      parameters: [{ $ref: '#/components/parameters/ProjectKey' }]
      responses:
        "200":
          description: Project
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Project' }
        default: { $ref: '#/components/responses/Problem' }
    patch:
      operationId: updateProject
      tags: [projects]
      parameters: [{ $ref: '#/components/parameters/ProjectKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ProjectPatch' }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Project' }
        default: { $ref: '#/components/responses/Problem' }
    delete:
      operationId: deleteProject
      tags: [projects]
      parameters: [{ $ref: '#/components/parameters/ProjectKey' }]
      responses:
        "204": { description: Deleted }
        default: { $ref: '#/components/responses/Problem' }
  /projects/{projectKey}/members:
    get:
      operationId: listProjectMembers
      tags: [projects]
      parameters: [{ $ref: '#/components/parameters/ProjectKey' }]
      responses:
        "200":
          description: Direct and group memberships
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/ProjectMember' }
        default: { $ref: '#/components/responses/Problem' }
    post:
      operationId: addProjectMember
      tags: [projects]
      parameters: [{ $ref: '#/components/parameters/ProjectKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [roleKey]
              properties:
                userId: { type: string, format: uuid }
                groupId: { type: string, format: uuid }
                roleKey: { type: string }
      responses:
        "201":
          description: Added
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ProjectMember' }
        default: { $ref: '#/components/responses/Problem' }
  /projects/{projectKey}/members/{memberId}:
    patch:
      operationId: updateProjectMember
      tags: [projects]
      parameters: [{ $ref: '#/components/parameters/ProjectKey' }, { $ref: '#/components/parameters/MemberId' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [roleKey]
              properties:
                roleKey: { type: string }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ProjectMember' }
        default: { $ref: '#/components/responses/Problem' }
    delete:
      operationId: removeProjectMember
      tags: [projects]
      parameters: [{ $ref: '#/components/parameters/ProjectKey' }, { $ref: '#/components/parameters/MemberId' }]
      responses:
        "204": { description: Removed }
        default: { $ref: '#/components/responses/Problem' }
  /projects/{projectKey}/labels:
    get:
      operationId: listProjectLabels
      tags: [projects]
      parameters: [{ $ref: '#/components/parameters/ProjectKey' }]
      responses:
        "200":
          description: Labels in use
          content:
            application/json:
              schema:
                type: array
                items: { type: string }
        default: { $ref: '#/components/responses/Problem' }
  /projects/{projectKey}/issue-types:
    get:
      operationId: listIssueTypes
      tags: [projects]
      parameters: [{ $ref: '#/components/parameters/ProjectKey' }]
      responses:
        "200":
          description: Issue types and their workflow mapping
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/IssueType' }
        default: { $ref: '#/components/responses/Problem' }
    post:
      operationId: createIssueType
      tags: [projects]
      parameters: [{ $ref: '#/components/parameters/ProjectKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/IssueTypeInput' }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/IssueType' }
        default: { $ref: '#/components/responses/Problem' }
  /projects/{projectKey}/issue-types/{typeId}:
    patch:
      operationId: updateIssueType
      tags: [projects]
      description: |
        Changing `workflowId` re-maps existing issues of this type; statuses missing from the new
        workflow must be mapped in `statusMapping` (old status name -> new status name).
      parameters: [{ $ref: '#/components/parameters/ProjectKey' }, { $ref: '#/components/parameters/TypeId' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string, minLength: 1, maxLength: 60 }
                description: { type: string }
                icon: { type: string }
                color: { type: string }
                workflowId: { type: string, format: uuid }
                statusMapping:
                  type: object
                  additionalProperties: { type: string }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema: { $ref: '#/components/schemas/IssueType' }
        default: { $ref: '#/components/responses/Problem' }
    delete:
      operationId: deleteIssueType
      tags: [projects]
      parameters: [{ $ref: '#/components/parameters/ProjectKey' }, { $ref: '#/components/parameters/TypeId' }]
      responses:
        "204": { description: Deleted }
        default: { $ref: '#/components/responses/Problem' }
  /projects/{projectKey}/repositories:
    get:
      operationId: listProjectRepositories
      tags: [scm]
      parameters: [{ $ref: '#/components/parameters/ProjectKey' }]
      responses:
        "200":
          description: Repositories connected to the project
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/ScmRepository' }
        default: { $ref: '#/components/responses/Problem' }
    post:
      operationId: linkProjectRepository
      tags: [scm]
      parameters: [{ $ref: '#/components/parameters/ProjectKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [repositoryId]
              properties:
                repositoryId: { type: string, format: uuid }
      responses:
        "201":
          description: Linked
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScmRepository' }
        default: { $ref: '#/components/responses/Problem' }
  /projects/{projectKey}/repositories/{repositoryId}:
    delete:
      operationId: unlinkProjectRepository
      tags: [scm]
      parameters: [{ $ref: '#/components/parameters/ProjectKey' }, { $ref: '#/components/parameters/RepositoryId' }]
      responses:
        "204": { description: Unlinked }
        default: { $ref: '#/components/responses/Problem' }
  /projects/{projectKey}/reports/cycle-time:
    get:
      operationId: getCycleTimeReport
      tags: [projects]
      parameters:
        - { $ref: '#/components/parameters/ProjectKey' }
        - { name: days, in: query, schema: { type: integer, default: 30, minimum: 1, maximum: 365 } }
      responses:
        "200":
          description: Cycle time of issues resolved in the window, derived from issue history
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CycleTimeReport' }
        default: { $ref: '#/components/responses/Problem' }
  /projects/{projectKey}/risks:
    get:
      operationId: getRiskRegister
      tags: [projects]
      description: |
        The project's risk register: every issue of a risk type (newest first, up to 2000), how
        each is scored and how much of the work linked to it with `mitigates` is done, with
        totals and the likelihood × impact matrix of open risks.
      parameters: [{ $ref: '#/components/parameters/ProjectKey' }]
      responses:
        "200":
          description: Risk register
          content:
            application/json:
              schema: { $ref: '#/components/schemas/RiskRegister' }
        default: { $ref: '#/components/responses/Problem' }
  /projects/{projectKey}/programme:
    get:
      operationId: getProgrammeOverview
      tags: [programmes]
      summary: A programme's roadmap - its initiatives, the team epics under them and how far along they are
      description: |
        The programme's initiatives in rank order (up to 500), each with the epics under it in the
        team projects you can browse, the work under those epics by status category, a forecast
        (the latest due date among the epic and its issues) and flags: late, overdue, unscheduled,
        and blocked by unfinished work in another project. Teams you can't browse are left out,
        not counted. Also lists open epics in the teams that belong to no initiative.
      parameters:
        - { $ref: '#/components/parameters/ProjectKey' }
        - { name: doneDays, in: query, description: 'Include initiatives resolved within this many days (default 90)', schema: { type: integer, minimum: 0, maximum: 3650 } }
      responses:
        "200":
          description: Programme overview
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ProgrammeOverview' }
        default: { $ref: '#/components/responses/Problem' }
  /projects/{projectKey}/programme/teams:
    get:
      operationId: listProgrammeTeams
      tags: [programmes]
      summary: The team projects of a programme that you can browse
      parameters: [{ $ref: '#/components/parameters/ProjectKey' }]
      responses:
        "200":
          description: Teams
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ProgrammeTeams' }
        default: { $ref: '#/components/responses/Problem' }
    post:
      operationId: addProgrammeTeam
      tags: [programmes]
      summary: Add a team project to a programme (programme admins; you must be able to browse the team)
      description: |
        Its epics can then have the programme's initiatives as their parent. Membership gives no
        access to the team's issues: people see only the teams they can browse.
      parameters: [{ $ref: '#/components/parameters/ProjectKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [projectKey]
              properties:
                projectKey: { type: string, description: The team project's key }
      responses:
        "201":
          description: Teams
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ProgrammeTeams' }
        default: { $ref: '#/components/responses/Problem' }
  /projects/{projectKey}/programme/teams/{teamKey}:
    delete:
      operationId: removeProgrammeTeam
      tags: [programmes]
      summary: Take a team project out of a programme, unlinking its epics from the programme's initiatives
      parameters:
        - { $ref: '#/components/parameters/ProjectKey' }
        - { name: teamKey, in: path, required: true, description: The team project's key, schema: { type: string } }
      responses:
        "200":
          description: Removed
          content:
            application/json:
              schema:
                type: object
                required: [unlinkedEpics]
                properties:
                  unlinkedEpics: { type: integer, description: Epics that lost their initiative }
        default: { $ref: '#/components/responses/Problem' }
  /projects/{projectKey}/programmes:
    get:
      operationId: listProjectProgrammes
      tags: [programmes]
      summary: The programmes a project is a team of (those you can browse)
      parameters: [{ $ref: '#/components/parameters/ProjectKey' }]
      responses:
        "200":
          description: Programmes
          content:
            application/json:
              schema: { type: array, items: { $ref: '#/components/schemas/ProjectRef' } }
        default: { $ref: '#/components/responses/Problem' }
  /projects/{projectKey}/decisions:
    get:
      operationId: getDecisionLog
      tags: [projects]
      description: |
        The project's decision log: every issue of a decision type (newest first, up to 2000),
        with the decisions it supersedes and is superseded by and the work implementing it.
      parameters: [{ $ref: '#/components/parameters/ProjectKey' }]
      responses:
        "200":
          description: Decision log
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DecisionLog' }
        default: { $ref: '#/components/responses/Problem' }
  /projects/{projectKey}/velocity:
    get:
      operationId: getVelocity
      tags: [sprints]
      parameters:
        - { $ref: '#/components/parameters/ProjectKey' }
        - { name: count, in: query, description: How many of the most recently completed sprints, schema: { type: integer, default: 7, minimum: 1, maximum: 20 } }
      responses:
        "200":
          description: Committed and completed work per completed sprint, oldest first
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Velocity' }
        default: { $ref: '#/components/responses/Problem' }

  # ---------------------------------------------------------------------------
  # Workflows
  # ---------------------------------------------------------------------------
  /workflows:
    get:
      operationId: listWorkflows
      tags: [workflows]
      parameters:
        - { name: project, in: query, description: Project key; returns its workflows plus shared ones, schema: { type: string } }
      responses:
        "200":
          description: Workflows
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/Workflow' }
        default: { $ref: '#/components/responses/Problem' }
    post:
      operationId: createWorkflow
      tags: [workflows]
      description: Create a workflow from YAML and publish version 1. Omit `projectKey` for a shared workflow (org admin).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [yaml]
              properties:
                projectKey: { type: string }
                yaml: { type: string }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WorkflowDetail' }
        default: { $ref: '#/components/responses/Problem' }
  /workflows/apply:
    put:
      operationId: applyWorkflow
      tags: [workflows]
      description: |
        Workflows as code. Create the named workflow or publish a new version when the document changed;
        a no-op when it is identical. Used by `wfm workflow apply` from CI.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [yaml]
              properties:
                projectKey: { type: string }
                yaml: { type: string }
                statusMapping:
                  type: object
                  additionalProperties: { type: string }
                dryRun: { type: boolean, default: false }
      responses:
        "200":
          description: Result of the apply
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WorkflowApplyResult' }
        default: { $ref: '#/components/responses/Problem' }
  /workflows/validate:
    post:
      operationId: validateWorkflow
      tags: [workflows]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [yaml]
              properties:
                yaml: { type: string }
                workflowId:
                  description: When set, also report statuses removed relative to the current version.
                  type: string
                  format: uuid
      responses:
        "200":
          description: Validation result (always 200; check `valid`)
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WorkflowValidation' }
        default: { $ref: '#/components/responses/Problem' }
  /workflows/{workflowId}:
    get:
      operationId: getWorkflow
      tags: [workflows]
      parameters: [{ $ref: '#/components/parameters/WorkflowId' }]
      responses:
        "200":
          description: Workflow with its current version
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WorkflowDetail' }
        default: { $ref: '#/components/responses/Problem' }
    delete:
      operationId: deleteWorkflow
      tags: [workflows]
      parameters: [{ $ref: '#/components/parameters/WorkflowId' }]
      responses:
        "204": { description: Deleted }
        default: { $ref: '#/components/responses/Problem' }
  /workflows/{workflowId}/versions:
    get:
      operationId: listWorkflowVersions
      tags: [workflows]
      parameters: [{ $ref: '#/components/parameters/WorkflowId' }]
      responses:
        "200":
          description: Published versions, newest first
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/WorkflowVersionSummary' }
        default: { $ref: '#/components/responses/Problem' }
    post:
      operationId: publishWorkflowVersion
      tags: [workflows]
      description: |
        Publish a new version. When it removes a status that issues are in, `statusMapping`
        (old status name -> new status name) is required; a migration job then moves the issues.
      parameters: [{ $ref: '#/components/parameters/WorkflowId' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [yaml]
              properties:
                yaml: { type: string }
                statusMapping:
                  type: object
                  additionalProperties: { type: string }
      responses:
        "201":
          description: Published
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WorkflowDetail' }
        default: { $ref: '#/components/responses/Problem' }
  /workflows/{workflowId}/versions/{version}:
    get:
      operationId: getWorkflowVersion
      tags: [workflows]
      parameters:
        - { $ref: '#/components/parameters/WorkflowId' }
        - { name: version, in: path, required: true, schema: { type: integer } }
      responses:
        "200":
          description: A specific version
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WorkflowVersion' }
        default: { $ref: '#/components/responses/Problem' }

  # ---------------------------------------------------------------------------
  # Boards, sprints, releases
  # ---------------------------------------------------------------------------
  /projects/{projectKey}/boards:
    get:
      operationId: listBoards
      tags: [boards]
      parameters: [{ $ref: '#/components/parameters/ProjectKey' }]
      responses:
        "200":
          description: Boards
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/Board' }
        default: { $ref: '#/components/responses/Problem' }
    post:
      operationId: createBoard
      tags: [boards]
      parameters: [{ $ref: '#/components/parameters/ProjectKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/BoardInput' }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Board' }
        default: { $ref: '#/components/responses/Problem' }
  /boards/{boardId}:
    get:
      operationId: getBoard
      tags: [boards]
      parameters: [{ $ref: '#/components/parameters/BoardId' }]
      responses:
        "200":
          description: Board configuration
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Board' }
        default: { $ref: '#/components/responses/Problem' }
    put:
      operationId: updateBoard
      tags: [boards]
      parameters: [{ $ref: '#/components/parameters/BoardId' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/BoardInput' }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Board' }
        default: { $ref: '#/components/responses/Problem' }
    delete:
      operationId: deleteBoard
      tags: [boards]
      parameters: [{ $ref: '#/components/parameters/BoardId' }]
      responses:
        "204": { description: Deleted }
        default: { $ref: '#/components/responses/Problem' }
  /boards/{boardId}/data:
    get:
      operationId: getBoardData
      tags: [boards]
      parameters:
        - { $ref: '#/components/parameters/BoardId' }
        - { name: jql, in: query, description: Quick filter applied on top of the board filter, schema: { type: string } }
      responses:
        "200":
          description: Columns with issues (Scrum boards show the active sprint). Risks and decisions are never on a board.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BoardData' }
        default: { $ref: '#/components/responses/Problem' }
  /boards/{boardId}/backlog:
    get:
      operationId: getBacklog
      tags: [boards]
      parameters:
        - { $ref: '#/components/parameters/BoardId' }
        - { name: jql, in: query, schema: { type: string } }
      responses:
        "200":
          description: Active and future sprints with their issues, then the backlog (work only, never risks or decisions)
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Backlog' }
        default: { $ref: '#/components/responses/Problem' }
  /projects/{projectKey}/sprints:
    get:
      operationId: listSprints
      tags: [sprints]
      parameters:
        - { $ref: '#/components/parameters/ProjectKey' }
        - { name: state, in: query, schema: { $ref: '#/components/schemas/SprintState' } }
      responses:
        "200":
          description: Sprints
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/Sprint' }
        default: { $ref: '#/components/responses/Problem' }
    post:
      operationId: createSprint
      tags: [sprints]
      parameters: [{ $ref: '#/components/parameters/ProjectKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/SprintInput' }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Sprint' }
        default: { $ref: '#/components/responses/Problem' }
  /sprints/{sprintId}:
    patch:
      operationId: updateSprint
      tags: [sprints]
      parameters: [{ $ref: '#/components/parameters/SprintId' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/SprintInput' }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Sprint' }
        default: { $ref: '#/components/responses/Problem' }
    delete:
      operationId: deleteSprint
      tags: [sprints]
      parameters: [{ $ref: '#/components/parameters/SprintId' }]
      responses:
        "204": { description: Deleted; its issues return to the backlog }
        default: { $ref: '#/components/responses/Problem' }
  /sprints/{sprintId}/start:
    post:
      operationId: startSprint
      tags: [sprints]
      parameters: [{ $ref: '#/components/parameters/SprintId' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [startDate, endDate]
              properties:
                startDate: { type: string, format: date }
                endDate: { type: string, format: date }
                goal: { type: string }
      responses:
        "200":
          description: Started
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Sprint' }
        default: { $ref: '#/components/responses/Problem' }
  /sprints/{sprintId}/complete:
    post:
      operationId: completeSprint
      tags: [sprints]
      parameters: [{ $ref: '#/components/parameters/SprintId' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                moveOpenIssuesTo:
                  description: Sprint id for unfinished issues; omit to return them to the backlog.
                  type: string
                  format: uuid
      responses:
        "200":
          description: Completed
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Sprint' }
        default: { $ref: '#/components/responses/Problem' }
  /sprints/{sprintId}/report:
    get:
      operationId: getSprintReport
      tags: [sprints]
      parameters: [{ $ref: '#/components/parameters/SprintId' }]
      responses:
        "200":
          description: Burndown and completion for the sprint
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SprintReport' }
        default: { $ref: '#/components/responses/Problem' }
  /projects/{projectKey}/releases:
    get:
      operationId: listReleases
      tags: [releases]
      parameters: [{ $ref: '#/components/parameters/ProjectKey' }]
      responses:
        "200":
          description: Releases (versions)
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/Release' }
        default: { $ref: '#/components/responses/Problem' }
    post:
      operationId: createRelease
      tags: [releases]
      parameters: [{ $ref: '#/components/parameters/ProjectKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ReleaseInput' }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Release' }
        default: { $ref: '#/components/responses/Problem' }
  /releases/{releaseId}:
    get:
      operationId: getRelease
      tags: [releases]
      summary: A release with its progress, notes, Git tag and the environments it reached
      parameters: [{ $ref: '#/components/parameters/ReleaseId' }]
      responses:
        "200":
          description: Release
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ReleaseDetail' }
        default: { $ref: '#/components/responses/Problem' }
    patch:
      operationId: updateRelease
      tags: [releases]
      parameters: [{ $ref: '#/components/parameters/ReleaseId' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ReleaseInput' }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Release' }
        default: { $ref: '#/components/responses/Problem' }
    delete:
      operationId: deleteRelease
      tags: [releases]
      parameters: [{ $ref: '#/components/parameters/ReleaseId' }]
      responses:
        "204": { description: Deleted }
        default: { $ref: '#/components/responses/Problem' }
  /releases/{releaseId}/release:
    post:
      operationId: shipRelease
      tags: [releases]
      parameters: [{ $ref: '#/components/parameters/ReleaseId' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                moveOpenIssuesTo:
                  description: Release id for unfinished issues; omit to leave them unassigned.
                  type: string
                  format: uuid
      responses:
        "200":
          description: Marked released
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Release' }
        default: { $ref: '#/components/responses/Problem' }
  /releases/{releaseId}/readiness:
    get:
      operationId: getReleaseReadiness
      tags: [releases]
      summary: What stands in the way of shipping the release
      description: |
        Warnings, each with the issues concerned: issues that are not done, issues with open
        (unmerged) pull or merge requests, issues whose latest pipeline failed, and a release date
        that has passed. Shipping stays possible whatever they say.
      parameters: [{ $ref: '#/components/parameters/ReleaseId' }]
      responses:
        "200":
          description: Readiness
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ReleaseReadiness' }
        default: { $ref: '#/components/responses/Problem' }
  /releases/{releaseId}/burnup:
    get:
      operationId: getReleaseBurnup
      tags: [releases]
      summary: Done work against the release's scope, day by day
      parameters: [{ $ref: '#/components/parameters/ReleaseId' }]
      responses:
        "200":
          description: Burnup
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ReleaseBurnup' }
        default: { $ref: '#/components/responses/Problem' }
  /releases/{releaseId}/notes/generated:
    get:
      operationId: generateReleaseNotes
      tags: [releases]
      summary: Release notes generated from the release's done issues (not saved)
      description: |
        Markdown with a section per issue type (Features from stories, Bug fixes from bugs,
        Improvements from tasks, else the type's plural name) and a line "summary (KEY)" per done
        issue, sub-tasks left out. Save it with `PATCH /releases/{releaseId}` (`notes`).
      parameters: [{ $ref: '#/components/parameters/ReleaseId' }]
      responses:
        "200":
          description: Generated notes
          content:
            application/json:
              schema: { $ref: '#/components/schemas/GeneratedReleaseNotes' }
        default: { $ref: '#/components/responses/Problem' }
  /projects/{projectKey}/release-settings:
    get:
      operationId: getReleaseSettings
      tags: [releases]
      summary: What new Git tags do to the project's releases
      parameters: [{ $ref: '#/components/parameters/ProjectKey' }]
      responses:
        "200":
          description: Settings
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ReleaseSettings' }
        default: { $ref: '#/components/responses/Problem' }
    patch:
      operationId: updateReleaseSettings
      tags: [releases]
      parameters: [{ $ref: '#/components/parameters/ProjectKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ReleaseSettingsPatch' }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ReleaseSettings' }
        default: { $ref: '#/components/responses/Problem' }

  # ---------------------------------------------------------------------------
  # Issues
  # ---------------------------------------------------------------------------
  /timeline:
    get:
      operationId: getTimeline
      tags: [boards]
      summary: Epics and issues with dates, dependencies, sprints and releases across one or more projects
      parameters:
        - { name: projects, in: query, description: 'Comma-separated project keys; omit for every project you can browse', schema: { type: string } }
        - { name: programme, in: query, description: "A programme's key: its initiatives and the team projects you can browse (not with projects)", schema: { type: string } }
        - { name: jql, in: query, description: Optional extra query filter (e.g. assignee = currentUser()), schema: { type: string } }
        - { name: doneDays, in: query, description: 'Include issues resolved within this many days (default 90)', schema: { type: integer, minimum: 0, maximum: 3650 } }
      responses:
        "200":
          description: Timeline data. Sub-tasks, risks and decisions are not included.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Timeline' }
        default: { $ref: '#/components/responses/Problem' }
  /dashboards:
    get:
      operationId: listDashboards
      tags: [dashboards]
      responses:
        "200":
          description: Your dashboards, then those others share with everyone
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/Dashboard' }
        default: { $ref: '#/components/responses/Problem' }
    post:
      operationId: createDashboard
      tags: [dashboards]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/DashboardInput' }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Dashboard' }
        default: { $ref: '#/components/responses/Problem' }
  /dashboards/{dashboardId}:
    get:
      operationId: getDashboard
      tags: [dashboards]
      parameters: [{ $ref: '#/components/parameters/DashboardId' }]
      responses:
        "200":
          description: The dashboard
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Dashboard' }
        default: { $ref: '#/components/responses/Problem' }
    put:
      operationId: updateDashboard
      tags: [dashboards]
      description: Owner or organisation admin only.
      parameters: [{ $ref: '#/components/parameters/DashboardId' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/DashboardInput' }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Dashboard' }
        default: { $ref: '#/components/responses/Problem' }
    delete:
      operationId: deleteDashboard
      tags: [dashboards]
      parameters: [{ $ref: '#/components/parameters/DashboardId' }]
      responses:
        "204": { description: Deleted }
        default: { $ref: '#/components/responses/Problem' }
  /metrics/summary:
    get:
      operationId: getMetricsSummary
      tags: [metrics]
      summary: Counts and totals for the matching issues (open, created and resolved in the window, overdue...)
      parameters: [{ $ref: '#/components/parameters/MetricProjects' }, { $ref: '#/components/parameters/MetricJql' }, { $ref: '#/components/parameters/MetricDays' }]
      responses:
        "200":
          description: Summary
          content:
            application/json:
              schema: { $ref: '#/components/schemas/MetricsSummary' }
        default: { $ref: '#/components/responses/Problem' }
  /metrics/flow:
    get:
      operationId: getCumulativeFlow
      tags: [metrics]
      summary: Cumulative flow, the number of matching issues in each status (or category) at the end of each day
      parameters:
        - { $ref: '#/components/parameters/MetricProjects' }
        - { $ref: '#/components/parameters/MetricJql' }
        - { $ref: '#/components/parameters/MetricDays' }
        - { name: by, in: query, schema: { type: string, enum: [category, status], default: category } }
      responses:
        "200":
          description: 'One series per category or status (todo, in progress, done). Issues resolved before the window are left out, so the done band is work finished in it.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CumulativeFlow' }
        default: { $ref: '#/components/responses/Problem' }
  /metrics/created-resolved:
    get:
      operationId: getCreatedResolved
      tags: [metrics]
      summary: Issues created and resolved per day or week, with the number open at the end of each
      parameters:
        - { $ref: '#/components/parameters/MetricProjects' }
        - { $ref: '#/components/parameters/MetricJql' }
        - { $ref: '#/components/parameters/MetricDays' }
        - { name: interval, in: query, schema: { type: string, enum: [day, week], default: day } }
      responses:
        "200":
          description: Buckets, oldest first
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CreatedResolved' }
        default: { $ref: '#/components/responses/Problem' }
  /metrics/breakdown:
    get:
      operationId: getBreakdown
      tags: [metrics]
      summary: Matching issues grouped by a field, with counts and story points
      parameters:
        - { $ref: '#/components/parameters/MetricProjects' }
        - { $ref: '#/components/parameters/MetricJql' }
        - { name: by, in: query, required: true, schema: { $ref: '#/components/schemas/BreakdownBy' } }
      responses:
        "200":
          description: 'Groups, largest first ("None" last); priority and status category keep their natural order. An issue with several labels or releases counts in each.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Breakdown' }
        default: { $ref: '#/components/responses/Problem' }
  /metrics/cycle-time:
    get:
      operationId: getCycleTimeMetrics
      tags: [metrics]
      summary: Cycle time of matching issues resolved in the window (first in progress to resolved)
      parameters: [{ $ref: '#/components/parameters/MetricProjects' }, { $ref: '#/components/parameters/MetricJql' }, { $ref: '#/components/parameters/MetricDays' }]
      responses:
        "200":
          description: Cycle times
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CycleTimeReport' }
        default: { $ref: '#/components/responses/Problem' }
  /search:
    get:
      operationId: searchIssues
      tags: [search]
      parameters:
        - { name: jql, in: query, schema: { type: string, default: '' } }
        - { $ref: '#/components/parameters/Limit' }
        - { $ref: '#/components/parameters/Offset' }
      responses:
        "200":
          description: Matching issues
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SearchResult' }
        default: { $ref: '#/components/responses/Problem' }
  /search/fields:
    get:
      operationId: getSearchMetadata
      tags: [search]
      summary: Fields, operators and functions for query autocompletion
      responses:
        "200":
          description: Query language metadata
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SearchMetadata' }
        default: { $ref: '#/components/responses/Problem' }
  /filters:
    get:
      operationId: listFilters
      tags: [search]
      responses:
        "200":
          description: The caller's filters and filters shared with everyone
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/SavedFilter' }
        default: { $ref: '#/components/responses/Problem' }
    post:
      operationId: createFilter
      tags: [search]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/SavedFilterInput' }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SavedFilter' }
        default: { $ref: '#/components/responses/Problem' }
  /filters/{filterId}:
    put:
      operationId: updateFilter
      tags: [search]
      parameters: [{ $ref: '#/components/parameters/FilterId' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/SavedFilterInput' }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SavedFilter' }
        default: { $ref: '#/components/responses/Problem' }
    delete:
      operationId: deleteFilter
      tags: [search]
      parameters: [{ $ref: '#/components/parameters/FilterId' }]
      responses:
        "204": { description: Deleted }
        default: { $ref: '#/components/responses/Problem' }
  /issues:
    post:
      operationId: createIssue
      tags: [issues]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/IssueCreate' }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/IssueDetail' }
        default: { $ref: '#/components/responses/Problem' }
  /issues/{issueKey}:
    get:
      operationId: getIssue
      tags: [issues]
      parameters: [{ $ref: '#/components/parameters/IssueKey' }]
      responses:
        "200":
          description: Issue
          content:
            application/json:
              schema: { $ref: '#/components/schemas/IssueDetail' }
        default: { $ref: '#/components/responses/Problem' }
    patch:
      operationId: updateIssue
      tags: [issues]
      description: Send `version` to reject the write with 409 if someone else changed the issue first.
      parameters: [{ $ref: '#/components/parameters/IssueKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/IssuePatch' }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema: { $ref: '#/components/schemas/IssueDetail' }
        default: { $ref: '#/components/responses/Problem' }
    delete:
      operationId: deleteIssue
      tags: [issues]
      parameters: [{ $ref: '#/components/parameters/IssueKey' }]
      responses:
        "204": { description: Deleted }
        default: { $ref: '#/components/responses/Problem' }
  /issues/{issueKey}/transitions:
    get:
      operationId: listTransitions
      tags: [issues]
      parameters: [{ $ref: '#/components/parameters/IssueKey' }]
      responses:
        "200":
          description: Transitions the caller may run (conditions already evaluated)
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/AvailableTransition' }
        default: { $ref: '#/components/responses/Problem' }
    post:
      operationId: transitionIssue
      tags: [issues]
      description: Runs conditions and validators; a 422 problem lists failed validators.
      parameters: [{ $ref: '#/components/parameters/IssueKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TransitionRequest' }
      responses:
        "200":
          description: Transitioned
          content:
            application/json:
              schema: { $ref: '#/components/schemas/IssueDetail' }
        default: { $ref: '#/components/responses/Problem' }
  /issues/{issueKey}/move:
    post:
      operationId: moveIssue
      tags: [issues]
      description: |
        Board and backlog drag-and-drop: optionally transition to a status, change sprint and re-rank
        between neighbours, atomically. Moving to a status whose transition needs a screen returns
        422 with code `screen_required`.
      parameters: [{ $ref: '#/components/parameters/IssueKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/MoveRequest' }
      responses:
        "200":
          description: Moved
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Issue' }
        default: { $ref: '#/components/responses/Problem' }
  /issues/{issueKey}/comments:
    get:
      operationId: listComments
      tags: [issues]
      parameters: [{ $ref: '#/components/parameters/IssueKey' }]
      responses:
        "200":
          description: Comments, oldest first
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/Comment' }
        default: { $ref: '#/components/responses/Problem' }
    post:
      operationId: addComment
      tags: [issues]
      parameters: [{ $ref: '#/components/parameters/IssueKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CommentInput' }
      responses:
        "201":
          description: Added
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Comment' }
        default: { $ref: '#/components/responses/Problem' }
  /comments/{commentId}:
    patch:
      operationId: updateComment
      tags: [issues]
      parameters: [{ $ref: '#/components/parameters/CommentId' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CommentInput' }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Comment' }
        default: { $ref: '#/components/responses/Problem' }
    delete:
      operationId: deleteComment
      tags: [issues]
      parameters: [{ $ref: '#/components/parameters/CommentId' }]
      responses:
        "204": { description: Deleted }
        default: { $ref: '#/components/responses/Problem' }
  /issues/{issueKey}/history:
    get:
      operationId: listIssueHistory
      tags: [issues]
      parameters: [{ $ref: '#/components/parameters/IssueKey' }]
      responses:
        "200":
          description: Field changes, oldest first
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/HistoryEntry' }
        default: { $ref: '#/components/responses/Problem' }
  /issues/{issueKey}/metrics:
    get:
      operationId: getIssueMetrics
      tags: [issues]
      summary: Lead time, cycle time and time in each status, derived from the issue's history
      parameters: [{ $ref: '#/components/parameters/IssueKey' }]
      responses:
        "200":
          description: Metrics
          content:
            application/json:
              schema: { $ref: '#/components/schemas/IssueMetrics' }
        default: { $ref: '#/components/responses/Problem' }
  # Time tracking: worklogs, the timesheet and the working day/week durations use.
  /issues/{issueKey}/worklogs:
    get:
      operationId: listWorklogs
      tags: [issues]
      parameters: [{ $ref: '#/components/parameters/IssueKey' }]
      responses:
        "200":
          description: Work logged on the issue, in the order it was done
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/Worklog' }
        default: { $ref: '#/components/responses/Problem' }
    post:
      operationId: addWorklog
      tags: [issues]
      description: |
        Log work on an issue (needs permission to edit it). The remaining estimate goes down by the
        time logged (never below zero) unless the request sets it (`remainingEstimateSeconds`) or
        asks to leave it (`adjustEstimate: leave`).
      parameters: [{ $ref: '#/components/parameters/IssueKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/WorklogInput' }
      responses:
        "201":
          description: Logged
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Worklog' }
        default: { $ref: '#/components/responses/Problem' }
  /worklogs/{worklogId}:
    patch:
      operationId: updateWorklog
      tags: [issues]
      description: Change your own worklog (project admins can change anyone's). With `adjustEstimate` auto, the remaining estimate absorbs the difference.
      parameters: [{ name: worklogId, in: path, required: true, schema: { type: string, format: uuid } }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/WorklogPatch' }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Worklog' }
        default: { $ref: '#/components/responses/Problem' }
    delete:
      operationId: deleteWorklog
      tags: [issues]
      description: Delete your own worklog (project admins can delete anyone's). By default the remaining estimate grows back by its time.
      parameters:
        - { name: worklogId, in: path, required: true, schema: { type: string, format: uuid } }
        - { name: adjustEstimate, in: query, schema: { $ref: '#/components/schemas/AdjustEstimate' } }
        - { name: remainingEstimateSeconds, in: query, description: 'Set the remaining estimate to this', schema: { type: integer, format: int64, minimum: 0 } }
      responses:
        "204": { description: Deleted }
        default: { $ref: '#/components/responses/Problem' }
  /worklogs:
    get:
      operationId: getTimesheet
      tags: [metrics]
      summary: Timesheet, the time logged per person per day (and per issue) on issues you can browse
      parameters:
        - { name: from, in: query, required: true, description: First day, schema: { type: string, format: date } }
        - { name: to, in: query, required: true, description: 'Last day (inclusive); at most 366 days after from', schema: { type: string, format: date } }
        - { $ref: '#/components/parameters/MetricProjects' }
        - { name: user, in: query, description: Only this person's work, schema: { type: string, format: uuid } }
        - { name: tz, in: query, description: 'IANA time zone that days are counted in (default UTC)', schema: { type: string } }
      responses:
        "200":
          description: Totals per person and day
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Timesheet' }
        default: { $ref: '#/components/responses/Problem' }
  /time-tracking/settings:
    get:
      operationId: getTimeTrackingSettings
      tags: [issues]
      summary: The working day and week behind durations such as 1w 2d (WFM_TIME_HOURS_PER_DAY, WFM_TIME_DAYS_PER_WEEK)
      responses:
        "200":
          description: Settings
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TimeTrackingSettings' }
        default: { $ref: '#/components/responses/Problem' }
  /issues/{issueKey}/links:
    post:
      operationId: createIssueLink
      tags: [issues]
      parameters: [{ $ref: '#/components/parameters/IssueKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [type, targetKey]
              properties:
                type: { $ref: '#/components/schemas/LinkType' }
                targetKey: { type: string }
                direction:
                  description: outward = "this issue <type> target"; inward reverses it.
                  type: string
                  enum: [outward, inward]
                  default: outward
      responses:
        "201":
          description: Linked
          content:
            application/json:
              schema: { $ref: '#/components/schemas/IssueLink' }
        default: { $ref: '#/components/responses/Problem' }
  /issues/{issueKey}/supersede:
    post:
      operationId: supersedeDecision
      tags: [issues]
      description: |
        Marks a decision as superseded by a newer one in one step: links the newer decision to it
        with `supersedes` (unless already linked) and runs the transition to Superseded. Both must
        be decisions; the workflow decides from which statuses that is allowed (Accepted, by default).
      parameters: [{ $ref: '#/components/parameters/IssueKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/SupersedeRequest' }
      responses:
        "200":
          description: The superseded decision
          content:
            application/json:
              schema: { $ref: '#/components/schemas/IssueDetail' }
        default: { $ref: '#/components/responses/Problem' }
  /issue-links/{linkId}:
    delete:
      operationId: deleteIssueLink
      tags: [issues]
      parameters: [{ $ref: '#/components/parameters/LinkId' }]
      responses:
        "204": { description: Removed }
        default: { $ref: '#/components/responses/Problem' }
  /issues/{issueKey}/watchers:
    get:
      operationId: listWatchers
      tags: [issues]
      parameters: [{ $ref: '#/components/parameters/IssueKey' }]
      responses:
        "200":
          description: Watchers
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/User' }
        default: { $ref: '#/components/responses/Problem' }
    post:
      operationId: addWatcher
      tags: [issues]
      parameters: [{ $ref: '#/components/parameters/IssueKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: Omit userId to watch the issue yourself.
              properties:
                userId: { type: string, format: uuid }
      responses:
        "204": { description: Watching }
        default: { $ref: '#/components/responses/Problem' }
  /issues/{issueKey}/watchers/{userId}:
    delete:
      operationId: removeWatcher
      tags: [issues]
      parameters: [{ $ref: '#/components/parameters/IssueKey' }, { $ref: '#/components/parameters/UserId' }]
      responses:
        "204": { description: Removed }
        default: { $ref: '#/components/responses/Problem' }
  /issues/{issueKey}/attachments:
    post:
      operationId: uploadAttachment
      tags: [issues]
      parameters: [{ $ref: '#/components/parameters/IssueKey' }]
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file: { type: string, format: binary }
      responses:
        "201":
          description: Uploaded
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Attachment' }
        default: { $ref: '#/components/responses/Problem' }
  /attachments/{attachmentId}:
    get:
      operationId: downloadAttachment
      tags: [issues]
      parameters: [{ $ref: '#/components/parameters/AttachmentId' }]
      responses:
        "200":
          description: File content
          headers:
            Content-Disposition: { schema: { type: string } }
          content:
            application/octet-stream:
              schema: { type: string, format: binary }
        "302":
          description: Redirect to a pre-signed object storage URL
          headers:
            Location: { schema: { type: string } }
        default: { $ref: '#/components/responses/Problem' }
    delete:
      operationId: deleteAttachment
      tags: [issues]
      parameters: [{ $ref: '#/components/parameters/AttachmentId' }]
      responses:
        "204": { description: Deleted }
        default: { $ref: '#/components/responses/Problem' }
  /issues/{issueKey}/branches:
    post:
      operationId: createIssueBranch
      tags: [scm]
      summary: Create a branch named from the issue key and summary
      parameters: [{ $ref: '#/components/parameters/IssueKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [repositoryId]
              properties:
                repositoryId: { type: string, format: uuid }
                name: { type: string, description: Defaults to <key>-<summary-slug> }
                fromRef: { type: string, description: Defaults to the repository default branch }
      responses:
        "201":
          description: Branch created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DevLink' }
        default: { $ref: '#/components/responses/Problem' }

  # ---------------------------------------------------------------------------
  # Automation & outbound webhooks
  # ---------------------------------------------------------------------------
  /projects/{projectKey}/automation-rules:
    get:
      operationId: listAutomationRules
      tags: [automation]
      parameters: [{ $ref: '#/components/parameters/ProjectKey' }]
      responses:
        "200":
          description: Rules
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/AutomationRule' }
        default: { $ref: '#/components/responses/Problem' }
    post:
      operationId: createAutomationRule
      tags: [automation]
      parameters: [{ $ref: '#/components/parameters/ProjectKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/AutomationRuleInput' }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AutomationRule' }
        default: { $ref: '#/components/responses/Problem' }
  /automation-rules/{ruleId}:
    put:
      operationId: updateAutomationRule
      tags: [automation]
      parameters: [{ $ref: '#/components/parameters/RuleId' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/AutomationRuleInput' }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AutomationRule' }
        default: { $ref: '#/components/responses/Problem' }
    delete:
      operationId: deleteAutomationRule
      tags: [automation]
      parameters: [{ $ref: '#/components/parameters/RuleId' }]
      responses:
        "204": { description: Deleted }
        default: { $ref: '#/components/responses/Problem' }
  /automation-rules/{ruleId}/runs:
    get:
      operationId: listAutomationRuns
      tags: [automation]
      parameters: [{ $ref: '#/components/parameters/RuleId' }, { $ref: '#/components/parameters/Limit' }]
      responses:
        "200":
          description: Recent runs
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/AutomationRun' }
        default: { $ref: '#/components/responses/Problem' }
  /projects/{projectKey}/chat-channels:
    get:
      operationId: listChatChannels
      tags: [webhooks]
      summary: Slack and Mattermost channels that receive this project's notifications
      parameters: [{ $ref: '#/components/parameters/ProjectKey' }]
      responses:
        "200":
          description: Channels
          content:
            application/json:
              schema: { type: array, items: { $ref: '#/components/schemas/ChatChannel' } }
        default: { $ref: '#/components/responses/Problem' }
    post:
      operationId: createChatChannel
      tags: [webhooks]
      parameters: [{ $ref: '#/components/parameters/ProjectKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ChatChannelInput' }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ChatChannel' }
        default: { $ref: '#/components/responses/Problem' }
  /chat-channels/{channelId}:
    patch:
      operationId: updateChatChannel
      tags: [webhooks]
      parameters: [{ $ref: '#/components/parameters/ChannelId' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ChatChannelPatch' }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ChatChannel' }
        default: { $ref: '#/components/responses/Problem' }
    delete:
      operationId: deleteChatChannel
      tags: [webhooks]
      parameters: [{ $ref: '#/components/parameters/ChannelId' }]
      responses:
        "204": { description: Deleted }
        default: { $ref: '#/components/responses/Problem' }
  /chat-channels/{channelId}/test:
    post:
      operationId: testChatChannel
      tags: [webhooks]
      summary: Post a test message to the channel
      parameters: [{ $ref: '#/components/parameters/ChannelId' }]
      responses:
        "202": { description: Queued }
        default: { $ref: '#/components/responses/Problem' }
  /chat-channels/{channelId}/deliveries:
    get:
      operationId: listChatDeliveries
      tags: [webhooks]
      parameters: [{ $ref: '#/components/parameters/ChannelId' }, { $ref: '#/components/parameters/Limit' }]
      responses:
        "200":
          description: Recent messages, newest first
          content:
            application/json:
              schema: { type: array, items: { $ref: '#/components/schemas/ChatDelivery' } }
        default: { $ref: '#/components/responses/Problem' }
  /webhooks:
    get:
      operationId: listWebhooks
      tags: [webhooks]
      parameters:
        - { name: project, in: query, description: Project key; omit for organisation-wide subscriptions, schema: { type: string } }
      responses:
        "200":
          description: Outbound webhook subscriptions
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/WebhookSubscription' }
        default: { $ref: '#/components/responses/Problem' }
    post:
      operationId: createWebhook
      tags: [webhooks]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/WebhookSubscriptionInput' }
      responses:
        "201":
          description: Created; the signing secret is only returned once
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WebhookSubscriptionCreated' }
        default: { $ref: '#/components/responses/Problem' }
  /webhooks/{webhookId}:
    put:
      operationId: updateWebhook
      tags: [webhooks]
      parameters: [{ $ref: '#/components/parameters/WebhookId' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/WebhookSubscriptionInput' }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WebhookSubscription' }
        default: { $ref: '#/components/responses/Problem' }
    delete:
      operationId: deleteWebhook
      tags: [webhooks]
      parameters: [{ $ref: '#/components/parameters/WebhookId' }]
      responses:
        "204": { description: Deleted }
        default: { $ref: '#/components/responses/Problem' }
  /webhooks/{webhookId}/deliveries:
    get:
      operationId: listWebhookDeliveries
      tags: [webhooks]
      parameters: [{ $ref: '#/components/parameters/WebhookId' }, { $ref: '#/components/parameters/Limit' }]
      responses:
        "200":
          description: Recent deliveries
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/WebhookDelivery' }
        default: { $ref: '#/components/responses/Problem' }
  /webhooks/{webhookId}/test:
    post:
      operationId: testWebhook
      tags: [webhooks]
      parameters: [{ $ref: '#/components/parameters/WebhookId' }]
      responses:
        "202": { description: A ping delivery was queued }
        default: { $ref: '#/components/responses/Problem' }

  # ---------------------------------------------------------------------------
  # AI assistants: OAuth consent and connected apps
  # ---------------------------------------------------------------------------
  /oauth/consent:
    get:
      operationId: getOAuthConsent
      tags: [me]
      summary: What an AI assistant asks for, shown on the consent page (browser session only)
      parameters:
        - { name: request, in: query, required: true, description: The sealed authorisation request /oauth/authorize passed to the consent page, schema: { type: string } }
      responses:
        "200":
          description: The app, where it returns to and the access it asks for
          content:
            application/json:
              schema: { $ref: '#/components/schemas/OAuthConsent' }
        default: { $ref: '#/components/responses/Problem' }
    post:
      operationId: decideOAuthConsent
      tags: [me]
      summary: Approve or deny an AI assistant (browser session only); returns where to send the browser
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/OAuthConsentDecision' }
      responses:
        "200":
          description: The app's redirect URI with an authorisation code, or with error=access_denied
          content:
            application/json:
              schema: { $ref: '#/components/schemas/OAuthRedirect' }
        default: { $ref: '#/components/responses/Problem' }
  /me/connected-apps:
    get:
      operationId: listMyConnectedApps
      tags: [me]
      summary: AI assistants and other apps you allowed to act for you over OAuth
      responses:
        "200":
          description: Connected apps, most recently used first
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/ConnectedApp' }
        default: { $ref: '#/components/responses/Problem' }
  /me/connected-apps/{grantId}:
    delete:
      operationId: revokeMyConnectedApp
      tags: [me]
      summary: Disconnect an app; its tokens stop working and it must ask again
      parameters: [{ $ref: '#/components/parameters/GrantId' }]
      responses:
        "204": { description: Disconnected }
        default: { $ref: '#/components/responses/Problem' }
  /admin/connected-apps:
    get:
      operationId: adminListConnectedApps
      tags: [admin]
      summary: Every person's connected apps
      responses:
        "200":
          description: Connected apps, most recently used first
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/ConnectedApp' }
        default: { $ref: '#/components/responses/Problem' }
  /admin/connected-apps/{grantId}:
    delete:
      operationId: adminRevokeConnectedApp
      tags: [admin]
      summary: Disconnect anyone's app
      parameters: [{ $ref: '#/components/parameters/GrantId' }]
      responses:
        "204": { description: Disconnected }
        default: { $ref: '#/components/responses/Problem' }

  # ---------------------------------------------------------------------------
  # Service desk: agents (project settings, queues, requests) and the customer portal
  # ---------------------------------------------------------------------------
  /projects/{projectKey}/service-desk:
    get:
      operationId: getServiceDesk
      tags: [service-desk]
      summary: A service project's portal settings
      parameters: [{ $ref: '#/components/parameters/ProjectKey' }]
      responses:
        "200":
          description: Settings
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ServiceDesk' }
        default: { $ref: '#/components/responses/Problem' }
    patch:
      operationId: updateServiceDesk
      tags: [service-desk]
      parameters: [{ $ref: '#/components/parameters/ProjectKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ServiceDeskPatch' }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ServiceDesk' }
        default: { $ref: '#/components/responses/Problem' }
  /projects/{projectKey}/request-types:
    get:
      operationId: listRequestTypes
      tags: [service-desk]
      parameters: [{ $ref: '#/components/parameters/ProjectKey' }]
      responses:
        "200":
          description: Request types in portal order
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/RequestType' }
        default: { $ref: '#/components/responses/Problem' }
    post:
      operationId: createRequestType
      tags: [service-desk]
      parameters: [{ $ref: '#/components/parameters/ProjectKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/RequestTypeInput' }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/RequestType' }
        default: { $ref: '#/components/responses/Problem' }
  /request-types/{requestTypeId}:
    put:
      operationId: updateRequestType
      tags: [service-desk]
      summary: Replace a request type (its form and approval included)
      parameters: [{ name: requestTypeId, in: path, required: true, schema: { type: string, format: uuid } }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/RequestTypeInput' }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema: { $ref: '#/components/schemas/RequestType' }
        default: { $ref: '#/components/responses/Problem' }
    delete:
      operationId: deleteRequestType
      tags: [service-desk]
      description: Refused (409) while requests use it.
      parameters: [{ name: requestTypeId, in: path, required: true, schema: { type: string, format: uuid } }]
      responses:
        "204": { description: Deleted }
        default: { $ref: '#/components/responses/Problem' }
  /projects/{projectKey}/customers:
    get:
      operationId: listServiceCustomers
      tags: [service-desk]
      parameters:
        - { $ref: '#/components/parameters/ProjectKey' }
        - { name: q, in: query, schema: { type: string } }
        - { $ref: '#/components/parameters/Limit' }
      responses:
        "200":
          description: Customers
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/ServiceCustomer' }
        default: { $ref: '#/components/responses/Problem' }
    post:
      operationId: addServiceCustomer
      tags: [service-desk]
      summary: Add a customer by email
      description: |
        Someone new gets a customer account and an email invitation to choose a password (local
        accounts; emailed says whether it was sent); an existing account (a customer of another
        project, an organisation member, a single sign-on user) is added as it is. Needs the
        customer.manage permission.
      parameters: [{ $ref: '#/components/parameters/ProjectKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ServiceCustomerInput' }
      responses:
        "201":
          description: Added
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ServiceCustomerAdded' }
        default: { $ref: '#/components/responses/Problem' }
  /projects/{projectKey}/customers/{userId}:
    delete:
      operationId: removeServiceCustomer
      tags: [service-desk]
      parameters: [{ $ref: '#/components/parameters/ProjectKey' }, { $ref: '#/components/parameters/UserId' }]
      responses:
        "204": { description: Removed }
        default: { $ref: '#/components/responses/Problem' }
  /projects/{projectKey}/queues:
    get:
      operationId: listQueues
      tags: [service-desk]
      parameters: [{ $ref: '#/components/parameters/ProjectKey' }]
      responses:
        "200":
          description: Queues in order, with the number of requests in each
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/Queue' }
        default: { $ref: '#/components/responses/Problem' }
    post:
      operationId: createQueue
      tags: [service-desk]
      parameters: [{ $ref: '#/components/parameters/ProjectKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/QueueInput' }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Queue' }
        default: { $ref: '#/components/responses/Problem' }
  /queues/{queueId}:
    patch:
      operationId: updateQueue
      tags: [service-desk]
      parameters: [{ name: queueId, in: path, required: true, schema: { type: string, format: uuid } }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/QueueInput' }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Queue' }
        default: { $ref: '#/components/responses/Problem' }
    delete:
      operationId: deleteQueue
      tags: [service-desk]
      parameters: [{ name: queueId, in: path, required: true, schema: { type: string, format: uuid } }]
      responses:
        "204": { description: Deleted }
        default: { $ref: '#/components/responses/Problem' }
  /queues/{queueId}/issues:
    get:
      operationId: listQueueIssues
      tags: [service-desk]
      parameters:
        - { name: queueId, in: path, required: true, schema: { type: string, format: uuid } }
        - { $ref: '#/components/parameters/Limit' }
        - { $ref: '#/components/parameters/Offset' }
      responses:
        "200":
          description: The queue's requests with their SLA clocks
          content:
            application/json:
              schema: { $ref: '#/components/schemas/QueueIssues' }
        default: { $ref: '#/components/responses/Problem' }
  /projects/{projectKey}/slas:
    get:
      operationId: getSlaConfig
      tags: [service-desk]
      parameters: [{ $ref: '#/components/parameters/ProjectKey' }]
      responses:
        "200":
          description: Calendars and SLA goals
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SlaConfig' }
        default: { $ref: '#/components/responses/Problem' }
    put:
      operationId: updateSlaConfig
      tags: [service-desk]
      summary: Replace the calendars and SLA goals
      description: Calendars are matched by name. Open requests' clocks are recomputed in the background.
      parameters: [{ $ref: '#/components/parameters/ProjectKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/SlaConfig' }
      responses:
        "200":
          description: Saved
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SlaConfig' }
        default: { $ref: '#/components/responses/Problem' }
  /issues/{issueKey}/request:
    get:
      operationId: getServiceRequest
      tags: [service-desk]
      summary: A service request's request type, SLA clocks, approvals and participants (agents)
      parameters: [{ $ref: '#/components/parameters/IssueKey' }]
      responses:
        "200":
          description: Request
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ServiceRequest' }
        default: { $ref: '#/components/responses/Problem' }
    patch:
      operationId: updateServiceRequest
      tags: [service-desk]
      summary: Set the request type (the portal shows only issues that have one)
      parameters: [{ $ref: '#/components/parameters/IssueKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ServiceRequestPatch' }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ServiceRequest' }
        default: { $ref: '#/components/responses/Problem' }
  /issues/{issueKey}/participants:
    post:
      operationId: addRequestParticipant
      tags: [service-desk]
      summary: Share a request with someone who may use the portal
      parameters: [{ $ref: '#/components/parameters/IssueKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ParticipantInput' }
      responses:
        "200":
          description: Shared
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ServiceRequest' }
        default: { $ref: '#/components/responses/Problem' }
  /issues/{issueKey}/participants/{userId}:
    delete:
      operationId: removeRequestParticipant
      tags: [service-desk]
      parameters: [{ $ref: '#/components/parameters/IssueKey' }, { $ref: '#/components/parameters/UserId' }]
      responses:
        "204": { description: Removed }
        default: { $ref: '#/components/responses/Problem' }
  /issues/{issueKey}/approvals/{approvalId}:
    post:
      operationId: decideApproval
      tags: [service-desk]
      summary: Approve or decline as one of the approvers
      parameters: [{ $ref: '#/components/parameters/IssueKey' }, { name: approvalId, in: path, required: true, schema: { type: string, format: uuid } }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ApprovalDecision' }
      responses:
        "200":
          description: Recorded
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ServiceRequest' }
        default: { $ref: '#/components/responses/Problem' }
  /portal/projects:
    get:
      operationId: listPortalDesks
      tags: [portal]
      summary: The help centre - service desks the caller may raise requests in
      responses:
        "200":
          description: Desks
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/PortalDesk' }
        default: { $ref: '#/components/responses/Problem' }
  /portal/projects/{projectKey}:
    get:
      operationId: getPortalDesk
      tags: [portal]
      summary: A desk's request types and their forms
      parameters: [{ $ref: '#/components/parameters/ProjectKey' }]
      responses:
        "200":
          description: Desk
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PortalDeskDetail' }
        default: { $ref: '#/components/responses/Problem' }
  /portal/projects/{projectKey}/whats-new:
    get:
      operationId: listPortalReleaseNotes
      tags: [portal]
      summary: What's new - the published release notes of the software projects the desk shows
      description: |
        Only the notes themselves, with the product's and the release's names and dates: never
        issues. Answers 404 to anyone who may not raise requests in the desk.
      parameters: [{ $ref: '#/components/parameters/ProjectKey' }]
      responses:
        "200":
          description: Release notes, newest first
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/PortalReleaseNote' }
        default: { $ref: '#/components/responses/Problem' }
  /portal/projects/{projectKey}/requests:
    post:
      operationId: createPortalRequest
      tags: [portal]
      summary: Raise a request by filling in a request type's form
      parameters: [{ $ref: '#/components/parameters/ProjectKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PortalRequestCreate' }
      responses:
        "201":
          description: Raised
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PortalRequestDetail' }
        default: { $ref: '#/components/responses/Problem' }
  /portal/requests:
    get:
      operationId: listPortalRequests
      tags: [portal]
      summary: My requests - raised by the caller or shared with them
      parameters:
        - { name: status, in: query, schema: { type: string, enum: [open, closed, all], default: all } }
        - { name: project, in: query, description: Desk (project key), schema: { type: string } }
        - { $ref: '#/components/parameters/Limit' }
      responses:
        "200":
          description: Requests, most recently updated first
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/PortalRequest' }
        default: { $ref: '#/components/responses/Problem' }
  /portal/requests/{issueKey}:
    get:
      operationId: getPortalRequest
      tags: [portal]
      summary: A request as its reporter, participants and approvers see it (no internal notes)
      parameters: [{ $ref: '#/components/parameters/IssueKey' }]
      responses:
        "200":
          description: Request
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PortalRequestDetail' }
        default: { $ref: '#/components/responses/Problem' }
  /portal/requests/{issueKey}/comments:
    post:
      operationId: addPortalComment
      tags: [portal]
      parameters: [{ $ref: '#/components/parameters/IssueKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PortalCommentInput' }
      responses:
        "201":
          description: Added
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PortalComment' }
        default: { $ref: '#/components/responses/Problem' }
  /portal/requests/{issueKey}/attachments:
    post:
      operationId: uploadPortalAttachment
      tags: [portal]
      parameters: [{ $ref: '#/components/parameters/IssueKey' }]
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file: { type: string, format: binary }
      responses:
        "201":
          description: Uploaded
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PortalAttachment' }
        default: { $ref: '#/components/responses/Problem' }
  /portal/requests/{issueKey}/approval:
    post:
      operationId: decidePortalApproval
      tags: [portal]
      summary: Approve or decline a request waiting for the caller's decision
      parameters: [{ $ref: '#/components/parameters/IssueKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ApprovalDecision' }
      responses:
        "200":
          description: Recorded
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PortalRequestDetail' }
        default: { $ref: '#/components/responses/Problem' }
  /portal/attachments/{attachmentId}:
    get:
      operationId: downloadPortalAttachment
      tags: [portal]
      parameters: [{ $ref: '#/components/parameters/AttachmentId' }]
      responses:
        "200":
          description: File content
          headers:
            Content-Disposition: { schema: { type: string } }
          content:
            application/octet-stream:
              schema: { type: string, format: binary }
        "302":
          description: Redirect to a pre-signed object storage URL
          headers:
            Location: { schema: { type: string } }
        default: { $ref: '#/components/responses/Problem' }
  /portal/approvals:
    get:
      operationId: listPortalApprovals
      tags: [portal]
      summary: Requests waiting for the caller's approval
      parameters: [{ $ref: '#/components/parameters/Limit' }]
      responses:
        "200":
          description: Requests
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/PortalRequest' }
        default: { $ref: '#/components/responses/Problem' }
  /portal/signup/{projectKey}:
    get:
      operationId: getPortalSignup
      tags: [portal]
      security: []
      summary: A desk that lets people sign up (404 unless self sign-up is on)
      parameters: [{ $ref: '#/components/parameters/ProjectKey' }]
      responses:
        "200":
          description: Desk
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PortalSignupInfo' }
        default: { $ref: '#/components/responses/Problem' }
    post:
      operationId: portalSignup
      tags: [portal]
      security: []
      summary: Create a customer account (confirmed by email)
      description: |
        Answers 202 the same way whether or not the address already has an account. A new
        address gets a customer account and an email to choose a password; an existing one gets
        an email saying how to sign in. Throttled like the sign-in endpoints.
      parameters: [{ $ref: '#/components/parameters/ProjectKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PortalSignup' }
      responses:
        "202": { description: Check your email }
        default: { $ref: '#/components/responses/Problem' }

  # ---------------------------------------------------------------------------
  # Live updates
  # ---------------------------------------------------------------------------
  /events:
    get:
      operationId: streamEvents
      tags: [handwritten]
      summary: Server-sent events for open boards and issue views
      parameters:
        - { name: project, in: query, required: true, schema: { type: string } }
      responses:
        "200":
          description: "`text/event-stream` of `issue.created`, `issue.updated`, `issue.deleted`, `board.updated` events"
          content:
            text/event-stream:
              schema: { type: string }
        default: { $ref: '#/components/responses/Problem' }

components:
  securitySchemes:
    cookieAuth:
      type: apiKey
      in: cookie
      name: wfm_session
    bearerAuth:
      type: http
      scheme: bearer
      description: Personal access token or service-account token (`wfm_pat_...`)

  parameters:
    Limit:
      { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 500, default: 50 } }
    Offset:
      { name: offset, in: query, schema: { type: integer, minimum: 0, default: 0 } }
    ProjectKey:
      { name: projectKey, in: path, required: true, description: Project key (or id), schema: { type: string } }
    IssueKey:
      { name: issueKey, in: path, required: true, description: Issue key such as PLAT-12 (or id), schema: { type: string } }
    UserId: { name: userId, in: path, required: true, schema: { type: string, format: uuid } }
    GroupId: { name: groupId, in: path, required: true, schema: { type: string, format: uuid } }
    RoleId: { name: roleId, in: path, required: true, schema: { type: string, format: uuid } }
    TokenId: { name: tokenId, in: path, required: true, schema: { type: string, format: uuid } }
    MemberId: { name: memberId, in: path, required: true, schema: { type: string, format: uuid } }
    TypeId: { name: typeId, in: path, required: true, schema: { type: string, format: uuid } }
    WorkflowId: { name: workflowId, in: path, required: true, schema: { type: string, format: uuid } }
    BoardId: { name: boardId, in: path, required: true, schema: { type: string, format: uuid } }
    SprintId: { name: sprintId, in: path, required: true, schema: { type: string, format: uuid } }
    DashboardId: { name: dashboardId, in: path, required: true, schema: { type: string, format: uuid } }
    MetricProjects: { name: projects, in: query, description: 'Comma-separated project keys; omit for every project you can browse', schema: { type: string } }
    MetricJql: { name: jql, in: query, description: 'Optional query the issues must also match. Metrics count work only: never risks or decisions.', schema: { type: string } }
    MetricDays: { name: days, in: query, description: Window in days, ending today, schema: { type: integer, default: 30, minimum: 1, maximum: 365 } }
    ReleaseId: { name: releaseId, in: path, required: true, schema: { type: string, format: uuid } }
    CommentId: { name: commentId, in: path, required: true, schema: { type: string, format: uuid } }
    LinkId: { name: linkId, in: path, required: true, schema: { type: string, format: uuid } }
    AttachmentId: { name: attachmentId, in: path, required: true, schema: { type: string, format: uuid } }
    FilterId: { name: filterId, in: path, required: true, schema: { type: string, format: uuid } }
    ConnectionId: { name: connectionId, in: path, required: true, schema: { type: string, format: uuid } }
    RepositoryId: { name: repositoryId, in: path, required: true, schema: { type: string, format: uuid } }
    RuleId: { name: ruleId, in: path, required: true, schema: { type: string, format: uuid } }
    WebhookId: { name: webhookId, in: path, required: true, schema: { type: string, format: uuid } }
    ChannelId: { name: channelId, in: path, required: true, schema: { type: string, format: uuid } }
    FieldId: { name: fieldId, in: path, required: true, schema: { type: string, format: uuid } }
    EventId: { name: eventId, in: path, required: true, schema: { type: string, format: uuid } }
    GrantId: { name: grantId, in: path, required: true, schema: { type: string, format: uuid } }

  responses:
    Problem:
      description: Error
      content:
        application/problem+json:
          schema: { $ref: '#/components/schemas/Problem' }

  schemas:
    Problem:
      type: object
      required: [title, status]
      properties:
        type: { type: string }
        title: { type: string }
        status: { type: integer }
        detail: { type: string }
        code: { type: string, description: 'Machine-readable error code, e.g. transition_not_allowed' }
        errors:
          type: array
          items: { $ref: '#/components/schemas/FieldError' }
        position: { type: integer, description: Character offset of a query syntax error }
    FieldError:
      type: object
      required: [message]
      properties:
        field: { type: string }
        message: { type: string }

    OrgRole: { type: string, enum: [admin, member, guest, customer] }
    StatusCategory: { type: string, enum: [todo, in_progress, done] }
    Priority: { type: string, enum: [highest, high, medium, low, lowest] }
    LinkDirection: { type: string, enum: [outward, inward] }
    LinkType:
      type: string
      enum: [blocks, relates, duplicates, clones, mitigates, implements, supersedes]
      description: 'mitigates: work → the risk it mitigates; implements: work → the decision it implements; supersedes: a decision → the decision it replaces'
    TokenScope:
      type: string
      enum: [read, write, admin]
      description: read = GET only; write = all project and issue operations; admin = organisation administration (if the owner is an admin).

    AuthProviders:
      type: object
      required: [providers, devLogin, localLogin, emailDelivery]
      properties:
        providers:
          type: array
          items: { $ref: '#/components/schemas/AuthProvider' }
        devLogin: { type: boolean }
        localLogin: { type: boolean, description: Email and password sign-in for local accounts (WFM_LOCAL_LOGIN) }
        emailDelivery:
          type: boolean
          description: |
            The server sends email (WFM_SMTP_HOST is set). Without it, password reset links and help
            centre sign-up links reach nobody, so those pages say to ask an administrator or the team.
    AuthProvider:
      type: object
      required: [id, name, loginUrl]
      properties:
        id: { type: string }
        name: { type: string }
        loginUrl: { type: string }
    UserKind: { type: string, enum: [human, service] }
    UserSource: { type: string, enum: [jit, scim, google, keycloak, admin, dev, system] }

    User:
      type: object
      required: [id, displayName, email, kind]
      properties:
        id: { type: string, format: uuid }
        displayName: { type: string }
        email: { type: string }
        avatarUrl: { type: string }
        kind: { $ref: '#/components/schemas/UserKind' }
    Me:
      allOf:
        - $ref: '#/components/schemas/User'
        - type: object
          required: [orgRole, source]
          properties:
            orgRole: { $ref: '#/components/schemas/OrgRole' }
            source: { $ref: '#/components/schemas/UserSource', description: 'Where the account is managed; scim and google profiles are read-only here' }
            version: { type: string, description: 'The server''s version (a release such as 0.2.0, or a development build), shown to signed-in people only' }
    AdminUser:
      allOf:
        - $ref: '#/components/schemas/User'
        - type: object
          required: [username, orgRole, active, source, createdAt]
          properties:
            username: { type: string }
            orgRole: { $ref: '#/components/schemas/OrgRole' }
            effectiveOrgRole: { $ref: '#/components/schemas/OrgRole' }
            active: { type: boolean }
            source: { $ref: '#/components/schemas/UserSource' }
            googleLinked: { type: boolean }
            lastLoginAt: { type: string, format: date-time }
            deactivatedAt: { type: string, format: date-time }
            deletedAt: { type: string, format: date-time }
            createdAt: { type: string, format: date-time }
            localAccount: { $ref: '#/components/schemas/LocalAccountStatus' }
    LocalAccountStatus:
      type: object
      description: Present for people who sign in with a password here (or are invited to).
      required: [status, totpEnabled]
      properties:
        status:
          type: string
          enum: [invited, active, password_change_required]
          description: invited = no password yet; password_change_required = a temporary password is set
        invitationExpiresAt: { type: string, format: date-time }
        totpEnabled: { type: boolean }
        lockedUntil: { type: string, format: date-time, description: Set while locked after repeated failures }
        passwordChangedAt: { type: string, format: date-time }
    LocalLoginResult:
      type: object
      required: [status]
      properties:
        status: { type: string, enum: [signed_in, totp_required, password_change_required, totp_enrollment_required] }
        me: { $ref: '#/components/schemas/Me' }
        displayName: { type: string, description: 'With password_change_required: the name a new password must not contain' }
        recoveryCodes:
          description: After two-factor enrolment at sign-in; shown only once.
          type: array
          items: { type: string }
    TotpSetup:
      type: object
      required: [secret, otpauthUri, qrCode]
      properties:
        secret: { type: string, description: Base32 secret for manual entry }
        otpauthUri: { type: string }
        qrCode: { type: string, description: 'The otpauth URI as a QR code: a data: URI of an SVG image' }
    RecoveryCodes:
      type: object
      required: [codes]
      properties:
        codes: { type: array, items: { type: string } }
    MySecurity:
      type: object
      required: [localLoginEnabled, localAccount, totpEnabled, recoveryCodesRemaining, require2fa]
      properties:
        localLoginEnabled: { type: boolean }
        localAccount: { type: boolean, description: You have a password here }
        passwordChangedAt: { type: string, format: date-time }
        totpEnabled: { type: boolean }
        totpEnabledAt: { type: string, format: date-time }
        recoveryCodesRemaining: { type: integer }
        require2fa: { type: boolean, description: Two-factor authentication is required for local accounts }
    LocalLink:
      type: object
      required: [purpose, email, displayName]
      properties:
        purpose: { type: string, enum: [invite, reset] }
        email: { type: string }
        displayName: { type: string }
        expiresAt: { type: string, format: date-time }
    PasswordCheck:
      type: object
      required: [score, label, problems, acceptable]
      properties:
        score: { type: integer, minimum: 0, maximum: 4, description: '0 not allowed, 1 weak, 2 fair, 3 good, 4 strong' }
        label: { type: string }
        problems: { type: array, items: { type: string } }
        acceptable: { type: boolean }
    ApiToken:
      type: object
      required: [id, name, prefix, scopes, expiresAt, createdAt]
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        prefix: { type: string }
        scopes: { type: array, items: { $ref: '#/components/schemas/TokenScope' } }
        expiresAt: { type: string, format: date-time }
        createdAt: { type: string, format: date-time }
        lastUsedAt: { type: string, format: date-time }
    ApiTokenCreate:
      type: object
      required: [name, scopes, expiresInDays]
      properties:
        name: { type: string, minLength: 1, maxLength: 100 }
        scopes: { type: array, minItems: 1, items: { $ref: '#/components/schemas/TokenScope' } }
        expiresInDays: { type: integer, minimum: 1, maximum: 366 }
    ApiTokenCreated:
      allOf:
        - $ref: '#/components/schemas/ApiToken'
        - type: object
          required: [token]
          properties:
            token: { type: string }
    Group:
      type: object
      required: [id, displayName, description, source, memberCount]
      properties:
        id: { type: string, format: uuid }
        displayName: { type: string }
        description: { type: string }
        source: { $ref: '#/components/schemas/GroupSource' }
        email: { type: string }
        orgRole: { $ref: '#/components/schemas/OrgRole' }
        memberCount: { type: integer }
    GroupSource:
      type: string
      description: 'local: managed here. scim, google, keycloak: synced from that directory. oidc: from a sign-in token''s groups claim.'
      enum: [local, scim, google, keycloak, oidc]
    GroupDetail:
      allOf:
        - $ref: '#/components/schemas/Group'
        - type: object
          required: [members]
          properties:
            members: { type: array, items: { $ref: '#/components/schemas/User' } }
    Role:
      type: object
      required: [id, key, name, description, permissions, builtin]
      properties:
        id: { type: string, format: uuid }
        key: { type: string }
        name: { type: string }
        description: { type: string }
        permissions: { type: array, items: { type: string } }
        builtin: { type: boolean }
    RoleInput:
      type: object
      required: [key, name, permissions]
      properties:
        key: { type: string, pattern: '^[a-z][a-z0-9_-]{1,40}$' }
        name: { type: string, minLength: 1, maxLength: 80 }
        description: { type: string }
        permissions: { type: array, items: { type: string } }
    PermissionInfo:
      type: object
      required: [key, description]
      properties:
        key: { type: string }
        description: { type: string }
    AuditEvent:
      type: object
      required: [id, actorType, action, data, createdAt]
      properties:
        id: { type: integer, format: int64 }
        actor: { $ref: '#/components/schemas/User' }
        actorType: { type: string }
        action: { type: string }
        targetType: { type: string }
        targetId: { type: string }
        projectId: { type: string, format: uuid }
        data: { type: object, additionalProperties: true }
        ip: { type: string }
        createdAt: { type: string, format: date-time }
    ProvisioningStatus:
      type: object
      required: [oidc, scim, directorySync]
      properties:
        oidc: { $ref: '#/components/schemas/OidcStatus' }
        scim: { $ref: '#/components/schemas/ScimStatus' }
        directorySync: { $ref: '#/components/schemas/DirectorySyncStatus' }
    OidcStatus:
      type: object
      required: [providers, allowedDomains, jitProvisioning, bootstrapAdmins]
      properties:
        providers: { type: array, items: { type: string } }
        allowedDomains: { type: array, items: { type: string } }
        jitProvisioning: { type: boolean }
        bootstrapAdmins: { type: array, items: { type: string } }
    ScimStatus:
      type: object
      required: [baseUrl, connections]
      properties:
        baseUrl: { type: string }
        connections: { type: array, items: { $ref: '#/components/schemas/ScimConnection' } }
    DirectorySyncStatus:
      type: object
      required: [configured, intervalMinutes, runs]
      properties:
        configured: { type: boolean }
        source: { type: string, enum: [google, keycloak], description: The configured directory (absent when none is) }
        intervalMinutes: { type: integer, description: Minutes between scheduled runs; 0 means manual runs only }
        group: { type: string, description: 'Access group: a Google group email, or a Keycloak group path' }
        orgUnit: { type: string }
        syncGroups: { type: boolean, description: Groups are mirrored too }
        groupPrefix: { type: string, description: Only groups whose email (Google) or name (Keycloak) starts with this are mirrored }
        url: { type: string, description: Keycloak base URL }
        realm: { type: string, description: Keycloak realm }
        provider: { type: string, description: 'Sign-in provider (WFM_OIDC_PROVIDERS id) whose subjects are the Keycloak user ids' }
        runs: { type: array, items: { $ref: '#/components/schemas/DirectorySyncRun' } }
    ScimConnection:
      type: object
      required: [id, name, prefix, createdAt]
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        prefix: { type: string }
        createdAt: { type: string, format: date-time }
        lastUsedAt: { type: string, format: date-time }
        userCount: { type: integer }
        groupCount: { type: integer }
    ScimConnectionCreated:
      allOf:
        - $ref: '#/components/schemas/ScimConnection'
        - type: object
          required: [token, baseUrl]
          properties:
            token: { type: string }
            baseUrl: { type: string }
    DirectorySyncRun:
      type: object
      required: [id, trigger, source, status, stats, startedAt]
      properties:
        id: { type: string, format: uuid }
        trigger: { type: string }
        source: { type: string, enum: [google, keycloak], description: The directory that was synced }
        status: { type: string, enum: [running, succeeded, failed] }
        stats: { type: object, additionalProperties: { type: integer } }
        error: { type: string }
        startedAt: { type: string, format: date-time }
        finishedAt: { type: string, format: date-time }

    Status:
      type: object
      required: [id, name, category]
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        category: { $ref: '#/components/schemas/StatusCategory' }

    ProjectTemplate:
      type: object
      required: [id, name, description, boardType, issueTypes, workflows, risksDecisions]
      properties:
        id: { type: string }
        name: { type: string }
        description: { type: string }
        boardType: { $ref: '#/components/schemas/BoardType' }
        risksDecisions: { type: boolean, description: Whether new projects from this template start with the Risks and decisions section on }
        issueTypes:
          type: array
          items: { $ref: '#/components/schemas/TemplateIssueType' }
        workflows: { type: array, items: { type: string } }
    TemplateIssueType:
      type: object
      required: [key, name, level]
      properties:
        key: { type: string }
        name: { type: string }
        level: { type: integer }
    Project:
      type: object
      required: [id, key, name, description, template, access, risksDecisions, createdAt, myPermissions]
      properties:
        id: { type: string, format: uuid }
        key: { type: string }
        name: { type: string }
        description: { type: string }
        lead: { $ref: '#/components/schemas/User' }
        template: { type: string }
        access: { $ref: '#/components/schemas/ProjectAccess' }
        openRoleKey: { type: string }
        keyAliases: { type: array, items: { type: string } }
        risksDecisions: { type: boolean, description: The Risks and decisions section is on (risk register and decision log) }
        createdAt: { type: string, format: date-time }
        myPermissions: { type: array, items: { type: string } }
    ProjectAccess: { type: string, enum: [open, private] }
    ProjectCreate:
      type: object
      required: [key, name, template]
      properties:
        key: { type: string, pattern: '^[A-Z][A-Z0-9]{1,9}$' }
        name: { type: string, minLength: 1, maxLength: 120 }
        description: { type: string }
        template: { type: string }
        access: { $ref: '#/components/schemas/ProjectAccess' }
        openRoleKey: { type: string, default: member }
        risksDecisions: { type: boolean, description: Turn the Risks and decisions section on or off; defaults to the template's setting }
    ProjectPatch:
      type: object
      properties:
        key: { type: string, pattern: '^[A-Z][A-Z0-9]{1,9}$', description: Renaming keeps the old key as an alias }
        name: { type: string, minLength: 1, maxLength: 120 }
        description: { type: string }
        leadId: { type: [string, 'null'], format: uuid }
        access: { $ref: '#/components/schemas/ProjectAccess' }
        openRoleKey: { type: string }
        risksDecisions:
          type: boolean
          description: |
            On installs the Risk and Decision issue types, their workflows and fields (again if
            anything is missing) and shows the section; off hides it and keeps the issues.
    ProjectMember:
      type: object
      required: [id, role]
      properties:
        id: { type: string, format: uuid }
        user: { $ref: '#/components/schemas/User' }
        group: { $ref: '#/components/schemas/Group' }
        role: { $ref: '#/components/schemas/Role' }
    IssueType:
      type: object
      required: [id, key, name, description, icon, color, level, kind, position, workflowId, workflowName]
      properties:
        id: { type: string, format: uuid }
        key: { type: string }
        name: { type: string }
        description: { type: string }
        icon: { type: string }
        color: { type: string }
        level: { type: integer, description: 'Hierarchy level: epic 1, standard 0, sub-task -1' }
        kind: { $ref: '#/components/schemas/IssueTypeKind' }
        position: { type: integer }
        workflowId: { type: string, format: uuid }
        workflowName: { type: string }
    IssueTypeInput:
      type: object
      required: [key, name, level, workflowId]
      properties:
        key: { type: string, pattern: '^[a-z][a-z0-9_]{0,30}$' }
        name: { type: string, minLength: 1, maxLength: 60 }
        description: { type: string }
        icon: { type: string }
        color: { type: string }
        level: { type: integer, minimum: -1, maximum: 2 }
        kind: { $ref: '#/components/schemas/IssueTypeKind', description: 'Defaults to work; risk and decision types must be standard (level 0)' }
        workflowId: { type: string, format: uuid }
    IssueTypeKind:
      type: string
      enum: [work, risk, decision]
      description: |
        work: planned work (boards, backlog, sprints, timeline, delivery metrics). risk and decision:
        the project's risk register and decision log, kept out of planning. Fixed once created.
    CycleTimeReport:
      type: object
      required: [days, issues, averageHours, medianHours, p85Hours]
      properties:
        days: { type: integer }
        averageHours: { type: number, format: double }
        medianHours: { type: number, format: double }
        p85Hours: { type: number, format: double }
        issues:
          type: array
          items: { $ref: '#/components/schemas/CycleTimeIssue' }
        truncated: { type: boolean, description: 'More issues were resolved than a report reads (100,000); the most recent count' }
    CycleTimeIssue:
      type: object
      required: [key, summary, cycleHours, resolvedAt]
      properties:
        key: { type: string }
        summary: { type: string }
        cycleHours: { type: number, format: double }
        resolvedAt: { type: string, format: date-time }
        projectKey: { type: string }

    IssueMetrics:
      type: object
      required: [createdAt, ageHours, statusSince, statusAgeHours, timeInStatus, reopened, sprints]
      properties:
        createdAt: { type: string, format: date-time }
        startedAt: { type: string, format: date-time, description: First entered an in-progress status }
        resolvedAt: { type: string, format: date-time }
        leadTimeHours: { type: number, format: double, description: Created to resolved (resolved issues only) }
        cycleTimeHours: { type: number, format: double, description: 'Started to resolved, or started to now while still open' }
        ageHours: { type: number, format: double, description: 'Created to now, or to resolution' }
        statusSince: { type: string, format: date-time, description: When the issue entered its current status }
        statusAgeHours: { type: number, format: double }
        timeInStatus:
          description: Total time in each status visited, in the order first visited
          type: array
          items: { $ref: '#/components/schemas/StatusTime' }
        reopened: { type: integer, description: Times it left a done status for another }
        sprints: { type: integer, description: Sprints it has been part of }
    StatusTime:
      type: object
      required: [status, hours, visits]
      properties:
        status: { $ref: '#/components/schemas/Status' }
        hours: { type: number, format: double }
        visits: { type: integer }

    Dashboard:
      type: object
      required: [id, name, description, owner, shared, canEdit, gadgets, createdAt, updatedAt]
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        description: { type: string }
        owner: { $ref: '#/components/schemas/User' }
        shared: { type: boolean, description: 'Visible to everyone in the organisation. Each gadget still shows only what the viewer may browse.' }
        canEdit: { type: boolean }
        gadgets: { type: array, items: { $ref: '#/components/schemas/Gadget' } }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
    DashboardInput:
      type: object
      required: [name, gadgets]
      properties:
        name: { type: string, minLength: 1, maxLength: 80 }
        description: { type: string, maxLength: 500 }
        shared: { type: boolean }
        gadgets: { type: array, maxItems: 40, items: { $ref: '#/components/schemas/Gadget' } }
    Gadget:
      type: object
      required: [id, type, width, config]
      properties:
        id: { type: string, minLength: 1, maxLength: 40, description: Chosen by the client; unique within the dashboard }
        type: { $ref: '#/components/schemas/GadgetType' }
        title: { type: string, maxLength: 80 }
        width: { type: integer, minimum: 1, maximum: 3, description: Columns of a three-column grid }
        config: { $ref: '#/components/schemas/GadgetConfig' }
    GadgetType:
      type: string
      enum: [sprint_burndown, sprint_burnup, sprint_health, velocity, cumulative_flow, created_resolved, cycle_time, breakdown, stat, issue_list]
    GadgetConfig:
      type: object
      description: |
        Settings for a gadget; which apply depends on its type. Sprint gadgets and velocity use
        projectKey (and sprintId, else the active sprint, else the last completed one); the others
        use projectKeys (none means every project the viewer can browse) and jql.
      properties:
        projectKey: { type: string }
        sprintId: { type: string, format: uuid }
        projectKeys: { type: array, maxItems: 20, items: { type: string } }
        jql: { type: string, maxLength: 2000 }
        days: { type: integer, minimum: 1, maximum: 365 }
        interval: { type: string, enum: [day, week] }
        by: { $ref: '#/components/schemas/BreakdownBy' }
        flowBy: { type: string, enum: [category, status] }
        measure: { type: string, enum: [issues, points, hours], description: 'hours (remaining estimate and time logged) applies to sprint gadgets' }
        chart: { type: string, enum: [bar, donut] }
        stat: { $ref: '#/components/schemas/MetricStat' }
        count: { type: integer, minimum: 1, maximum: 20, description: Sprints shown by velocity }
        limit: { type: integer, minimum: 1, maximum: 50, description: Rows shown by an issue list }
    MetricStat:
      type: string
      enum: [open, todo, in_progress, created, resolved, overdue, unassigned, open_points, resolved_points, median_cycle_hours]

    MetricsSummary:
      type: object
      required: [days, open, todo, inProgress, created, resolved, overdue, unassigned, openPoints, resolvedPoints]
      properties:
        days: { type: integer }
        open: { type: integer, description: Not in a done status }
        todo: { type: integer }
        inProgress: { type: integer }
        created: { type: integer, description: Created in the window }
        resolved: { type: integer, description: Resolved in the window }
        overdue: { type: integer, description: Open with a due date before today }
        unassigned: { type: integer, description: Open without an assignee }
        openPoints: { type: number, format: double }
        resolvedPoints: { type: number, format: double, description: Story points resolved in the window }
        medianCycleHours: { type: number, format: double, description: Of issues resolved in the window }
    CumulativeFlow:
      type: object
      required: [days, dates, series]
      properties:
        days: { type: integer }
        dates: { type: array, items: { type: string, format: date } }
        series:
          type: array
          items: { $ref: '#/components/schemas/FlowSeries' }
        truncated: { type: boolean, description: 'More issues match than a report reads (100,000); the most recently created count' }
    FlowSeries:
      type: object
      required: [key, label, category, counts]
      properties:
        key: { type: string, description: A status category, or a status id }
        label: { type: string }
        category: { $ref: '#/components/schemas/StatusCategory' }
        counts: { type: array, items: { type: integer }, description: One per date }
    CreatedResolved:
      type: object
      required: [days, interval, buckets]
      properties:
        days: { type: integer }
        interval: { type: string, enum: [day, week] }
        buckets:
          type: array
          items: { $ref: '#/components/schemas/CreatedResolvedBucket' }
    CreatedResolvedBucket:
      type: object
      required: [start, created, resolved, open]
      properties:
        start: { type: string, format: date }
        created: { type: integer }
        resolved: { type: integer }
        open: { type: integer, description: Open at the end of the bucket }
    BreakdownBy:
      type: string
      enum: [status, statusCategory, priority, type, assignee, label, epic, sprint, release, project]
    Breakdown:
      type: object
      required: [by, total, totalPoints, groups]
      properties:
        by: { $ref: '#/components/schemas/BreakdownBy' }
        total: { type: integer }
        totalPoints: { type: number, format: double }
        groups:
          type: array
          items: { $ref: '#/components/schemas/BreakdownGroup' }
    BreakdownGroup:
      type: object
      required: [key, label, count, points]
      properties:
        key: { type: string, description: 'The value (an id, key or name); empty for "None"' }
        label: { type: string }
        count: { type: integer }
        points: { type: number, format: double }
        category: { $ref: '#/components/schemas/StatusCategory' }
        color: { type: string }
        user: { $ref: '#/components/schemas/User' }
    Velocity:
      type: object
      required: [sprints, averagePoints, averageIssues]
      properties:
        averagePoints: { type: number, format: double, description: Completed points per sprint }
        averageIssues: { type: number, format: double }
        sprints:
          type: array
          items: { $ref: '#/components/schemas/VelocitySprint' }
    VelocitySprint:
      type: object
      required: [sprint, committedPoints, completedPoints, committedIssues, completedIssues]
      properties:
        sprint: { $ref: '#/components/schemas/Sprint' }
        committedPoints: { type: number, format: double }
        completedPoints: { type: number, format: double, description: 'All work done in the sprint, including work added after the start' }
        committedIssues: { type: integer }
        completedIssues: { type: integer }

    Workflow:
      type: object
      required: [id, name, description, shared, currentVersion, updatedAt, usage]
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        description: { type: string }
        projectKey: { type: string }
        shared: { type: boolean }
        currentVersion: { type: integer }
        updatedAt: { type: string, format: date-time }
        usage:
          type: array
          items: { $ref: '#/components/schemas/WorkflowUsage' }
    WorkflowUsage:
      type: object
      required: [projectKey, issueTypeKey]
      properties:
        projectKey: { type: string }
        issueTypeKey: { type: string }
    WorkflowDetail:
      allOf:
        - $ref: '#/components/schemas/Workflow'
        - type: object
          required: [version]
          properties:
            version: { $ref: '#/components/schemas/WorkflowVersion' }
            pendingMigration: { type: boolean }
    WorkflowVersionSummary:
      type: object
      required: [version, publishedAt]
      properties:
        version: { type: integer }
        publishedAt: { type: string, format: date-time }
        publishedBy: { $ref: '#/components/schemas/User' }
        issueCount: { type: integer, description: Issues still pinned to this version }
    WorkflowVersion:
      allOf:
        - $ref: '#/components/schemas/WorkflowVersionSummary'
        - type: object
          required: [yaml, definition]
          properties:
            yaml: { type: string }
            definition: { $ref: '#/components/schemas/WorkflowDefinition' }
    WorkflowDefinition:
      type: object
      required: [name, statuses, transitions]
      properties:
        name: { type: string }
        description: { type: string }
        statuses:
          type: array
          items: { $ref: '#/components/schemas/WorkflowStatusDef' }
        transitions:
          type: array
          items: { $ref: '#/components/schemas/TransitionDefinition' }
        layout:
          description: Node positions for the visual editor, keyed by status name.
          type: object
          additionalProperties: { $ref: '#/components/schemas/NodePosition' }
    WorkflowStatusDef:
      type: object
      required: [id, name, category, initial]
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        category: { $ref: '#/components/schemas/StatusCategory' }
        initial: { type: boolean }
    NodePosition:
      type: object
      required: [x, y]
      properties:
        x: { type: number, format: double }
        y: { type: number, format: double }
    TransitionDefinition:
      type: object
      required: [id, name, to]
      properties:
        id: { type: string }
        name: { type: string }
        from:
          description: Source status ids; omitted means any status.
          type: array
          items: { type: string, format: uuid }
        to: { type: string, format: uuid }
        condition: { type: string, description: CEL expression deciding who may see and run the transition }
        validators:
          type: array
          items: { $ref: '#/components/schemas/ValidatorDef' }
        screen:
          type: array
          items: { $ref: '#/components/schemas/ScreenField' }
        postFunctions:
          type: array
          items: { $ref: '#/components/schemas/PostFunction' }
    ValidatorDef:
      type: object
      required: [expr]
      properties:
        expr: { type: string, description: CEL expression that must evaluate to true }
        message: { type: string }
    ScreenField:
      type: object
      required: [field]
      properties:
        field: { type: string, description: resolution, comment, assignee, a system field or a custom field key }
        required: { type: boolean }
        options:
          type: array
          items: { type: string }
          description: 'resolution only: the resolutions this screen offers, instead of Done, Won''t Do, Duplicate and Cannot Reproduce'
    PostFunction:
      type: object
      description: Exactly one key is set.
      properties:
        assignTo: { type: string, description: 'actor | reporter | unassigned | <email>' }
        setField: { $ref: '#/components/schemas/SetFieldAction' }
        clearField: { type: string }
        notify: { type: string, description: 'assignee | reporter | watchers' }
        webhook: { type: string, description: URL receiving a signed JSON POST }
    SetFieldAction:
      type: object
      required: [field]
      properties:
        field: { type: string }
        value: {}
    WorkflowValidation:
      type: object
      required: [valid, errors]
      properties:
        valid: { type: boolean }
        errors:
          type: array
          items: { $ref: '#/components/schemas/ValidationError' }
        definition: { $ref: '#/components/schemas/WorkflowDefinition' }
        removedStatuses:
          type: array
          items: { $ref: '#/components/schemas/RemovedStatus' }
    ValidationError:
      type: object
      required: [message]
      properties:
        path: { type: string }
        message: { type: string }
    RemovedStatus:
      type: object
      required: [name, issueCount]
      properties:
        name: { type: string }
        issueCount: { type: integer }
    WorkflowApplyResult:
      type: object
      required: [action, workflowId, version]
      properties:
        action: { type: string, enum: [created, published, unchanged] }
        workflowId: { type: string, format: uuid }
        version: { type: integer }
        migrationQueued: { type: boolean }

    Board:
      type: object
      required: [id, projectKey, name, type, filterJql, swimlanes, doneRetentionDays, columns, cardFields]
      properties:
        id: { type: string, format: uuid }
        projectKey: { type: string }
        name: { type: string }
        type: { $ref: '#/components/schemas/BoardType' }
        filterJql: { type: string }
        swimlanes: { $ref: '#/components/schemas/Swimlanes' }
        doneRetentionDays: { type: integer }
        columns: { type: array, items: { $ref: '#/components/schemas/BoardColumn' } }
        cardFields: { type: array, items: { $ref: '#/components/schemas/CardField' }, description: What cards show besides the summary, type and key, in this order }
    BoardType: { type: string, enum: [scrum, kanban] }
    CardField: { type: string, enum: [epic, labels, priority, dev, points, assignee, dueDate, daysInColumn, timeTracking] }
    Swimlanes: { type: string, enum: [none, assignee, epic, priority, type] }
    BoardColumn:
      type: object
      required: [name, statusIds]
      properties:
        id: { type: string, format: uuid }
        name: { type: string, minLength: 1, maxLength: 60 }
        statusIds: { type: array, items: { type: string, format: uuid } }
        wipLimit: { type: integer, minimum: 1 }
    BoardInput:
      type: object
      required: [name, type]
      properties:
        name: { type: string, minLength: 1, maxLength: 80 }
        type: { $ref: '#/components/schemas/BoardType' }
        filterJql: { type: string }
        swimlanes: { $ref: '#/components/schemas/Swimlanes' }
        doneRetentionDays: { type: integer, minimum: 1, maximum: 365 }
        columns:
          description: Omit to derive one column per status from the project's workflows.
          type: array
          items: { $ref: '#/components/schemas/BoardColumn' }
        cardFields:
          description: Omit to keep the current fields (new boards get epic, labels, priority, dev, points and assignee).
          type: array
          maxItems: 8
          items: { $ref: '#/components/schemas/CardField' }
    BoardData:
      type: object
      required: [board, columns, unmappedStatusIds, total]
      properties:
        board: { $ref: '#/components/schemas/Board' }
        total: { type: integer, description: 'Issues on the board; the columns hold at most 2,000 of them, in rank order' }
        sprint: { $ref: '#/components/schemas/Sprint' }
        columns:
          type: array
          items: { $ref: '#/components/schemas/BoardColumnData' }
        unmappedStatusIds:
          description: Statuses used by the project but not shown on any column.
          type: array
          items: { type: string, format: uuid }
    BoardColumnData:
      type: object
      required: [id, name, statusIds, issues, overLimit]
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        statusIds: { type: array, items: { type: string, format: uuid } }
        wipLimit: { type: integer }
        overLimit: { type: boolean }
        issues: { type: array, items: { $ref: '#/components/schemas/Issue' } }
    Backlog:
      type: object
      required: [sprints, backlog, backlogTotal]
      properties:
        sprints:
          type: array
          items: { $ref: '#/components/schemas/BacklogSprint' }
        backlog: { type: array, items: { $ref: '#/components/schemas/Issue' } }
        backlogTotal: { type: integer, description: 'Issues in the backlog; `backlog` holds at most 2,000 of them, in rank order' }
    BacklogSprint:
      type: object
      required: [sprint, issues, total]
      properties:
        sprint: { $ref: '#/components/schemas/Sprint' }
        issues: { type: array, items: { $ref: '#/components/schemas/Issue' } }
        total: { type: integer, description: 'Issues in the sprint; `issues` holds at most 2,000 of them, in rank order' }
    Sprint:
      type: object
      required: [id, name, goal, state, position]
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        goal: { type: string }
        state: { $ref: '#/components/schemas/SprintState' }
        startDate: { type: string, format: date }
        endDate: { type: string, format: date }
        completedAt: { type: string, format: date-time }
        position: { type: integer }
    SprintState: { type: string, enum: [future, active, closed] }
    SprintInput:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 80
          description: |
            Creating a sprint without a name continues the project's numbering: the latest
            sprint's name with its trailing number plus one ("Sprint 24" → "Sprint 25"), else
            "<KEY> Sprint <n>".
        goal: { type: string }
        startDate: { type: [string, 'null'], format: date }
        endDate: { type: [string, 'null'], format: date }
    SprintReport:
      type: object
      description: |
        Issue counts and story points are of standard issues: sub-tasks count as part of their
        parent. Seconds include sub-tasks. For a future sprint, committed is what it holds now.
      required: [sprint, committedPoints, completedPoints, committedIssues, completedIssues, addedPoints, removedPoints, addedIssues, removedIssues, burndown, committedSeconds, completedSeconds, addedSeconds, removedSeconds]
      properties:
        sprint: { $ref: '#/components/schemas/Sprint' }
        committedPoints: { type: number, format: double, description: Story points in the sprint when it started }
        completedPoints: { type: number, format: double, description: 'Points done in the sprint, including work added after it started' }
        committedIssues: { type: integer }
        completedIssues: { type: integer }
        addedPoints: { type: number, format: double, description: Points of issues added after the start }
        removedPoints: { type: number, format: double, description: Points of issues taken out after the start }
        addedIssues: { type: integer }
        removedIssues: { type: integer }
        committedSeconds: { type: integer, format: int64, description: Remaining estimate of the open issues in the sprint when it started }
        completedSeconds: { type: integer, format: int64, description: Time logged on the sprint's issues during the sprint (counted when it was logged) }
        addedSeconds: { type: integer, format: int64, description: 'Remaining estimate of issues added after the start, when they were added' }
        removedSeconds: { type: integer, format: int64, description: 'Remaining estimate of issues taken out after the start, when they left' }
        burndown:
          type: array
          items: { $ref: '#/components/schemas/BurndownPoint' }
    BurndownPoint:
      type: object
      description: The sprint at the end of each day (today so far for an active sprint).
      required: [date, remainingPoints, idealPoints, scopePoints, completedPoints, remainingIssues, scopeIssues, completedIssues, remainingSeconds, idealSeconds, scopeSeconds, completedSeconds]
      properties:
        date: { type: string, format: date }
        remainingPoints: { type: number, format: double, description: Scope minus completed }
        idealPoints: { type: number, format: double, description: Committed points falling linearly to zero at the end date }
        scopePoints: { type: number, format: double }
        completedPoints: { type: number, format: double }
        remainingIssues: { type: integer }
        scopeIssues: { type: integer }
        completedIssues: { type: integer }
        remainingSeconds: { type: integer, format: int64, description: Remaining estimate of the issues in the sprint that are not done }
        idealSeconds: { type: integer, format: int64, description: Committed hours falling linearly to zero at the end date }
        scopeSeconds: { type: integer, format: int64, description: Time logged in the sprint so far plus the remaining estimate }
        completedSeconds: { type: integer, format: int64, description: Time logged on the sprint's issues since it started }
    Release:
      type: object
      required: [id, name, description, state, position, issueCount, doneCount, notesPublished]
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        description: { type: string }
        startDate: { type: string, format: date }
        releaseDate: { type: string, format: date }
        state: { $ref: '#/components/schemas/ReleaseState' }
        releasedAt: { type: string, format: date-time }
        position: { type: integer }
        issueCount: { type: integer }
        doneCount: { type: integer }
        notesPublished: { type: boolean, description: 'The release notes are shown on the What''s new page of the help centres that chose this project' }
        notesPublishedAt: { type: string, format: date-time }
        tagName: { type: string, description: The Git tag the release ships as }
        tagRepositoryId: { type: string, format: uuid }
    ReleaseState: { type: string, enum: [unreleased, released, archived] }
    ReleaseInput:
      type: object
      properties:
        name: { type: string, minLength: 1, maxLength: 80 }
        description: { type: string }
        startDate: { type: [string, 'null'], format: date }
        releaseDate: { type: [string, 'null'], format: date }
        state: { $ref: '#/components/schemas/ReleaseState' }
        notes: { type: string, maxLength: 100000, description: Release notes (Markdown) }
        notesPublished: { type: boolean, description: 'Publish the notes to the What''s new page of the help centres that chose this project' }
        tagRepositoryId: { type: [string, 'null'], format: uuid, description: 'The repository of the release''s Git tag (one connected to the project); null, with tagName null, unlinks the tag' }
        tagName: { type: [string, 'null'], maxLength: 250, description: The Git tag the release ships as }
    ReleaseDetail:
      allOf:
        - $ref: '#/components/schemas/Release'
        - type: object
          required: [projectKey, notes, progress, environments, canManage]
          properties:
            projectKey: { type: string }
            notes: { type: string, description: Release notes (Markdown) }
            tag: { $ref: '#/components/schemas/ReleaseTag' }
            progress: { $ref: '#/components/schemas/ReleaseProgress' }
            environments: { type: array, items: { $ref: '#/components/schemas/ReleaseEnvironment' } }
            canManage: { type: boolean, description: The caller holds release.manage in the project }
    ReleaseTag:
      type: object
      required: [name, repository, url]
      properties:
        name: { type: string }
        sha: { type: string }
        repository: { $ref: '#/components/schemas/RepositoryRef' }
        url: { type: string, description: The tag's page at the provider }
        previousTag: { type: string, description: The tag of the previous release with one in the same repository }
        compareUrl: { type: string, description: The changes between the previous release's tag and this one }
        linkedAt: { type: string, format: date-time }
    ReleaseProgress:
      type: object
      required: [issues, points, unestimated]
      properties:
        issues: { $ref: '#/components/schemas/CategoryCounts' }
        points: { $ref: '#/components/schemas/CategoryCounts' }
        unestimated: { type: integer, description: Issues without story points }
    CategoryCounts:
      type: object
      required: [todo, inProgress, done, total]
      properties:
        todo: { type: number, format: double }
        inProgress: { type: number, format: double }
        done: { type: number, format: double }
        total: { type: number, format: double }
    ReleaseEnvironment:
      type: object
      required: [environment, reachedAt, via]
      properties:
        environment: { type: string }
        reachedAt: { type: string, format: date-time }
        via:
          type: string
          enum: [tag, issues]
          description: 'tag: the release''s tag was deployed there; issues: every done issue of the release was'
        url: { type: string, description: 'The deployment (tag only)' }
    ReleaseBurnup:
      type: object
      required: [from, to, scopeSource, days]
      properties:
        from: { type: string, format: date }
        to: { type: string, format: date, description: Today, or the day the release was released }
        releaseDate: { type: string, format: date }
        scopeSource:
          type: string
          enum: [history, current]
          description: 'history: scope follows the fix version changes recorded in issue history; current: there are none, so scope is the release''s current issues, each from its creation'
        days: { type: array, items: { $ref: '#/components/schemas/ReleaseBurnupDay' } }
    ReleaseBurnupDay:
      type: object
      description: The release at the end of a day.
      required: [date, scopeIssues, doneIssues, scopePoints, donePoints]
      properties:
        date: { type: string, format: date }
        scopeIssues: { type: integer }
        doneIssues: { type: integer }
        scopePoints: { type: number, format: double }
        donePoints: { type: number, format: double }
    ReleaseReadiness:
      type: object
      required: [ready, warnings]
      properties:
        ready: { type: boolean, description: There are no warnings }
        warnings: { type: array, items: { $ref: '#/components/schemas/ReadinessWarning' } }
    ReadinessWarning:
      type: object
      required: [kind, message, issues]
      properties:
        kind: { type: string, enum: [not_done, open_pull_requests, failed_pipelines, overdue] }
        message: { type: string }
        issues: { type: array, items: { $ref: '#/components/schemas/ReadinessIssue' } }
    ReadinessIssue:
      type: object
      required: [issue, links]
      properties:
        issue: { $ref: '#/components/schemas/IssueRef' }
        links: { type: array, items: { $ref: '#/components/schemas/ReadinessLink' }, description: The open pull requests or the failed pipeline }
    ReadinessLink:
      type: object
      required: [title, url, state, repository]
      properties:
        title: { type: string }
        url: { type: string }
        state: { type: string }
        repository: { type: string }
    GeneratedReleaseNotes:
      type: object
      required: [notes, issueCount]
      properties:
        notes: { type: string, description: Markdown }
        issueCount: { type: integer, description: The done issues listed }
    ReleaseSettings:
      type: object
      required: [createFromTags, releaseOnTag]
      properties:
        createFromTags: { type: boolean, description: A new tag in a connected repository without a release of its name creates one }
        releaseOnTag: { type: boolean, description: Pushing a release's tag marks the release released }
    ReleaseSettingsPatch:
      type: object
      properties:
        createFromTags: { type: boolean }
        releaseOnTag: { type: boolean }

    IssueTypeRef:
      type: object
      required: [id, key, name, icon, color, level, kind]
      properties:
        id: { type: string, format: uuid }
        key: { type: string }
        name: { type: string }
        icon: { type: string }
        color: { type: string }
        level: { type: integer }
        kind: { $ref: '#/components/schemas/IssueTypeKind' }
    IssueRef:
      type: object
      required: [id, key, summary, type, status]
      properties:
        id: { type: string, format: uuid }
        key: { type: string }
        summary: { type: string }
        type: { $ref: '#/components/schemas/IssueTypeRef' }
        status: { $ref: '#/components/schemas/Status' }
    DevSummary:
      type: object
      required: [branches, commits, pullRequests, openPullRequests, mergedPullRequests]
      properties:
        branches: { type: integer }
        commits: { type: integer }
        pullRequests: { type: integer }
        openPullRequests: { type: integer }
        mergedPullRequests: { type: integer }
        pipelineState: { type: string }
        deployedTo: { type: array, items: { type: string }, description: Environments a successful deployment containing the issue reached }
    Issue:
      type: object
      required: [id, key, projectKey, type, summary, status, priority, labels, rank, version, fields, createdAt, updatedAt]
      properties:
        id: { type: string, format: uuid }
        key: { type: string }
        projectKey: { type: string }
        type: { $ref: '#/components/schemas/IssueTypeRef' }
        summary: { type: string }
        status: { $ref: '#/components/schemas/Status' }
        priority: { $ref: '#/components/schemas/Priority' }
        assignee: { $ref: '#/components/schemas/User' }
        reporter: { $ref: '#/components/schemas/User' }
        parent: { $ref: '#/components/schemas/IssueRef' }
        sprintId: { type: string, format: uuid }
        storyPoints: { type: number, format: double }
        startDate: { type: string, format: date }
        dueDate: { type: string, format: date }
        labels: { type: array, items: { type: string } }
        resolution: { type: string }
        resolvedAt: { type: string, format: date-time }
        rank: { type: string }
        version: { type: integer }
        fields: { type: object, additionalProperties: true }
        dev: { $ref: '#/components/schemas/DevSummary' }
        statusChangedAt: { type: string, format: date-time, description: When the issue entered its current status }
        timeTracking: { $ref: '#/components/schemas/TimeTracking', description: Always present }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
    IssueDetail:
      allOf:
        - $ref: '#/components/schemas/Issue'
        - type: object
          required: [description, project, children, links, development, deployments, attachments, releases, watching, watcherCount, permissions]
          properties:
            description: { type: string }
            project: { $ref: '#/components/schemas/ProjectRef' }
            sprint: { $ref: '#/components/schemas/Sprint' }
            releases: { type: array, items: { $ref: '#/components/schemas/Release' } }
            children: { type: array, items: { $ref: '#/components/schemas/Issue' } }
            progress: { $ref: '#/components/schemas/IssueProgress' }
            links: { type: array, items: { $ref: '#/components/schemas/IssueLink' } }
            development: { type: array, items: { $ref: '#/components/schemas/DevLink' } }
            deployments:
              type: array
              items: { $ref: '#/components/schemas/IssueDeployment' }
              description: One per environment the issue was deployed to - the latest successful deployment containing it, else the latest one that failed
            attachments: { type: array, items: { $ref: '#/components/schemas/Attachment' } }
            watching: { type: boolean }
            watcherCount: { type: integer }
            suggestedBranch: { type: string }
            parentHidden: { type: boolean, description: The issue has a parent you can't browse (an initiative in a programme you can't see), so parent is left out }
            permissions: { $ref: '#/components/schemas/IssuePermissions' }
    ProjectRef:
      type: object
      required: [id, key, name]
      properties:
        id: { type: string, format: uuid }
        key: { type: string }
        name: { type: string }
    IssueProgress:
      type: object
      required: [total, done, inProgress, points, donePoints]
      properties:
        total: { type: integer }
        done: { type: integer }
        inProgress: { type: integer }
        points: { type: number, format: double }
        donePoints: { type: number, format: double }
    IssuePermissions:
      type: object
      required: [edit, delete, transition, comment, assign]
      properties:
        edit: { type: boolean }
        delete: { type: boolean }
        transition: { type: boolean }
        comment: { type: boolean }
        assign: { type: boolean }
    IssueCreate:
      type: object
      required: [projectKey, typeKey, summary]
      properties:
        projectKey: { type: string }
        typeKey: { type: string }
        summary: { type: string, minLength: 1, maxLength: 255 }
        description: { type: string }
        priority: { $ref: '#/components/schemas/Priority' }
        assigneeId: { type: string, format: uuid }
        parentKey: { type: string }
        sprintId: { type: string, format: uuid }
        storyPoints: { type: number, format: double }
        startDate: { type: string, format: date }
        dueDate: { type: string, format: date }
        labels: { type: array, items: { type: string } }
        releaseIds: { type: array, items: { type: string, format: uuid } }
        fields: { type: object, additionalProperties: true }
        originalEstimate: { type: integer, format: int64, minimum: 0, description: Seconds; the remaining estimate starts equal to it unless given }
        remainingEstimate: { type: integer, format: int64, minimum: 0, description: Seconds }
    IssuePatch:
      type: object
      properties:
        version: { type: integer, description: Expected current version (optimistic locking) }
        typeKey: { type: string }
        summary: { type: string, minLength: 1, maxLength: 255 }
        description: { type: string }
        priority: { $ref: '#/components/schemas/Priority' }
        assigneeId: { type: [string, 'null'], format: uuid }
        reporterId: { type: [string, 'null'], format: uuid }
        parentKey: { type: [string, 'null'] }
        sprintId: { type: [string, 'null'], format: uuid }
        storyPoints: { type: [number, 'null'], format: double }
        startDate: { type: [string, 'null'], format: date }
        dueDate: { type: [string, 'null'], format: date }
        labels: { type: array, items: { type: string } }
        releaseIds: { type: array, items: { type: string, format: uuid } }
        fields:
          description: Custom field values to set; a null value clears the field.
          type: object
          additionalProperties: true
        originalEstimate: { type: [integer, 'null'], format: int64, minimum: 0, description: 'Seconds; setting it on an issue without a remaining estimate sets that too' }
        remainingEstimate: { type: [integer, 'null'], format: int64, minimum: 0, description: Seconds }
    AvailableTransition:
      type: object
      required: [id, name, to, screen]
      properties:
        id: { type: string }
        name: { type: string }
        to: { $ref: '#/components/schemas/Status' }
        screen:
          type: array
          items: { $ref: '#/components/schemas/ScreenFieldInfo' }
    ScreenFieldInfo:
      type: object
      required: [field, name, type, required]
      properties:
        field: { type: string }
        name: { type: string }
        type: { type: string }
        required: { type: boolean }
        options: { type: array, items: { type: string } }
    TransitionRequest:
      type: object
      required: [transitionId]
      properties:
        transitionId: { type: string }
        version: { type: integer }
        fields: { type: object, additionalProperties: true, description: Values for the transition screen }
        comment: { type: string }
        commentVisibility: { $ref: '#/components/schemas/Visibility', description: 'Service projects: the comment is internal unless public' }
    MoveRequest:
      type: object
      properties:
        statusId: { type: string, format: uuid }
        transitionId: { type: string }
        sprintId:
          description: null moves the issue to the backlog; omit to leave the sprint unchanged.
          type: [string, 'null']
          format: uuid
        afterIssueId: { type: string, format: uuid }
        beforeIssueId: { type: string, format: uuid }
    Comment:
      type: object
      required: [id, body, source, createdAt, canEdit]
      properties:
        id: { type: string, format: uuid }
        author: { $ref: '#/components/schemas/User' }
        body: { type: string }
        source: { type: string }
        via: { type: string, description: 'What the author used, e.g. an AI assistant' }
        createdAt: { type: string, format: date-time }
        editedAt: { type: string, format: date-time }
        canEdit: { type: boolean }
        visibility: { $ref: '#/components/schemas/Visibility', description: Service projects only }
    CommentInput:
      type: object
      required: [body]
      properties:
        body: { type: string, minLength: 1, maxLength: 50000 }
        visibility: { $ref: '#/components/schemas/Visibility', description: 'Service projects: public replies to the customer; internal (the default for agents) is a note' }
    HistoryEntry:
      type: object
      required: [id, actorType, field, createdAt]
      properties:
        id: { type: integer, format: int64 }
        actor: { $ref: '#/components/schemas/User' }
        actorType: { type: string }
        via: { type: string, description: 'What the person used, e.g. an AI assistant ("Ada via Claude")' }
        field: { type: string }
        from: { type: string }
        to: { type: string }
        createdAt: { type: string, format: date-time }
    TimeTracking:
      type: object
      description: Estimates and time logged, in seconds. A missing estimate was never set.
      required: [spentSeconds]
      properties:
        originalSeconds: { type: integer, format: int64, description: Original estimate }
        remainingSeconds: { type: integer, format: int64, description: Remaining estimate }
        spentSeconds: { type: integer, format: int64, description: "Time spent: the sum of the issue's worklogs" }
    TimeTrackingSettings:
      type: object
      required: [hoursPerDay, daysPerWeek]
      properties:
        hoursPerDay: { type: number, format: double, description: Hours in a working day (1d) }
        daysPerWeek: { type: number, format: double, description: Working days in a week (1w) }
    AdjustEstimate:
      type: string
      enum: [auto, leave]
      description: 'auto (the default) moves the remaining estimate by the time logged, never below zero; leave keeps it'
    Worklog:
      type: object
      required: [id, issueKey, startedAt, timeSpentSeconds, comment, createdAt, updatedAt, canEdit]
      properties:
        id: { type: string, format: uuid }
        issueKey: { type: string }
        author: { $ref: '#/components/schemas/User' }
        startedAt: { type: string, format: date-time, description: When the work was done }
        timeSpentSeconds: { type: integer, format: int64 }
        comment: { type: string }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        canEdit: { type: boolean, description: The caller may change or delete it }
    WorklogInput:
      type: object
      required: [timeSpentSeconds]
      properties:
        timeSpentSeconds: { type: integer, format: int64, minimum: 60 }
        startedAt: { type: string, format: date-time, description: Defaults to now }
        comment: { type: string, maxLength: 10000 }
        adjustEstimate: { $ref: '#/components/schemas/AdjustEstimate' }
        remainingEstimateSeconds: { type: integer, format: int64, minimum: 0, description: Set the remaining estimate to this instead of adjusting it }
    WorklogPatch:
      type: object
      properties:
        timeSpentSeconds: { type: integer, format: int64, minimum: 60 }
        startedAt: { type: string, format: date-time }
        comment: { type: string, maxLength: 10000 }
        adjustEstimate: { $ref: '#/components/schemas/AdjustEstimate' }
        remainingEstimateSeconds: { type: integer, format: int64, minimum: 0 }
    Timesheet:
      type: object
      required: [from, to, timeZone, dates, dayTotals, total, people]
      properties:
        from: { type: string, format: date }
        to: { type: string, format: date }
        timeZone: { type: string }
        dates: { type: array, items: { type: string, format: date }, description: Every day from..to }
        dayTotals: { type: array, items: { type: integer, format: int64 }, description: Seconds logged each day }
        total: { type: integer, format: int64 }
        people:
          type: array
          description: People who logged time, most first
          items: { $ref: '#/components/schemas/TimesheetPerson' }
    TimesheetPerson:
      type: object
      required: [days, total, issues]
      properties:
        user: { $ref: '#/components/schemas/User', description: Missing when the author's account is gone }
        days: { type: array, items: { type: integer, format: int64 }, description: Seconds per date }
        total: { type: integer, format: int64 }
        issues:
          type: array
          items: { $ref: '#/components/schemas/TimesheetIssue' }
    TimesheetIssue:
      type: object
      required: [issue, days, total]
      properties:
        issue: { $ref: '#/components/schemas/IssueRef' }
        days: { type: array, items: { type: integer, format: int64 } }
        total: { type: integer, format: int64 }
    IssueLink:
      type: object
      required: [id, type, direction, label, issue]
      properties:
        id: { type: string, format: uuid }
        type: { $ref: '#/components/schemas/LinkType' }
        direction: { $ref: '#/components/schemas/LinkDirection' }
        label: { type: string }
        issue: { $ref: '#/components/schemas/IssueRef' }
    DevLink:
      type: object
      required: [id, kind, repository, externalId, title, url, state, authorName, data, updatedAt]
      properties:
        id: { type: string, format: uuid }
        kind: { $ref: '#/components/schemas/DevLinkKind' }
        repository: { $ref: '#/components/schemas/RepositoryRef' }
        externalId: { type: string }
        title: { type: string }
        url: { type: string }
        state: { type: string }
        authorName: { type: string }
        data: { type: object, additionalProperties: true }
        updatedAt: { type: string, format: date-time }
    DevLinkKind: { type: string, enum: [branch, commit, pull_request, pipeline] }
    IssueDeployment:
      type: object
      required: [id, environment, status, deployedAt, url, ref, sha, deployer, repository]
      properties:
        id: { type: string, format: uuid }
        environment: { type: string }
        status: { $ref: '#/components/schemas/DeploymentStatus' }
        deployedAt: { type: string, format: date-time, description: When the provider reported the status }
        url: { type: string }
        ref: { type: string }
        sha: { type: string }
        deployer: { type: string }
        repository: { $ref: '#/components/schemas/RepositoryRef' }
    DeploymentStatus: { type: string, enum: [pending, running, success, failed, canceled] }
    ScmProvider: { type: string, enum: [github, gitlab] }
    RepositoryRef:
      type: object
      required: [id, provider, fullPath, webUrl]
      properties:
        id: { type: string, format: uuid }
        provider: { $ref: '#/components/schemas/ScmProvider' }
        fullPath: { type: string }
        webUrl: { type: string }
    Attachment:
      type: object
      required: [id, filename, contentType, size, createdAt, url]
      properties:
        id: { type: string, format: uuid }
        filename: { type: string }
        contentType: { type: string }
        size: { type: integer, format: int64 }
        uploader: { $ref: '#/components/schemas/User' }
        createdAt: { type: string, format: date-time }
        url: { type: string }
        visibility: { $ref: '#/components/schemas/Visibility', description: 'Service projects: public = added by a customer in the portal (the only files the portal shows)' }

    RiskRating:
      type: string
      enum: [low, medium, high, critical]
      description: 'From the score (likelihood × impact): 1–4 low, 5–9 medium, 10–16 high, 20–25 critical'
    LinkedWork:
      type: object
      required: [total, done]
      properties:
        total: { type: integer, description: Issues linked to this one (mitigating or implementing work) }
        done: { type: integer, description: Of those, how many are in a done status }
    RiskEntry:
      type: object
      required: [issue, mitigation]
      properties:
        issue: { $ref: '#/components/schemas/Issue' }
        likelihood: { type: integer, minimum: 1, maximum: 5, description: '1 Rare, 2 Unlikely, 3 Possible, 4 Likely, 5 Almost certain' }
        impact: { type: integer, minimum: 1, maximum: 5, description: '1 Negligible, 2 Minor, 3 Moderate, 4 Major, 5 Severe' }
        score: { type: integer, minimum: 1, maximum: 25, description: Likelihood × impact, when both are set }
        rating: { $ref: '#/components/schemas/RiskRating' }
        response: { type: string, enum: [Avoid, Reduce, Transfer, Accept] }
        mitigation: { $ref: '#/components/schemas/LinkedWork' }
    RiskCell:
      type: object
      required: [likelihood, impact, score, rating, count]
      properties:
        likelihood: { type: integer }
        impact: { type: integer }
        score: { type: integer }
        rating: { $ref: '#/components/schemas/RiskRating' }
        count: { type: integer, description: Open risks with this likelihood and impact }
    RiskSummary:
      type: object
      required: [open, highOrCritical, reviewsOverdue, unassessed]
      properties:
        open: { type: integer, description: Risks not in a done status }
        highOrCritical: { type: integer, description: Open risks rated high or critical }
        reviewsOverdue: { type: integer, description: Open risks whose review date (due date) has passed }
        unassessed: { type: integer, description: Open risks without a likelihood or an impact }
    RiskRegister:
      type: object
      required: [summary, matrix, risks, truncated]
      properties:
        summary: { $ref: '#/components/schemas/RiskSummary' }
        matrix: { type: array, items: { $ref: '#/components/schemas/RiskCell' }, description: All 25 cells, likelihood 5 to 1 then impact 1 to 5 }
        risks: { type: array, items: { $ref: '#/components/schemas/RiskEntry' } }
        truncated: { type: boolean, description: The project has more risks than the register returns (2000) }
    DecisionEntry:
      type: object
      required: [issue, supersedes, supersededBy, implementation]
      properties:
        issue: { $ref: '#/components/schemas/Issue' }
        decidedOn: { type: string, format: date }
        supersedes: { type: array, items: { $ref: '#/components/schemas/IssueRef' }, description: Earlier decisions this one replaces }
        supersededBy: { type: array, items: { $ref: '#/components/schemas/IssueRef' }, description: Later decisions that replace this one }
        implementation: { $ref: '#/components/schemas/LinkedWork' }
    DecisionLog:
      type: object
      required: [decisions, truncated]
      properties:
        decisions: { type: array, items: { $ref: '#/components/schemas/DecisionEntry' } }
        truncated: { type: boolean, description: The project has more decisions than the log returns (2000) }
    SupersedeRequest:
      type: object
      required: [supersededByKey]
      properties:
        supersededByKey: { type: string, description: The newer decision }
        version: { type: integer, description: Expected current version of the superseded decision (optimistic locking) }
        comment: { type: string, description: Optional comment added with the transition }

    Timeline:
      type: object
      required: [projects, issues, links, sprints, releases, truncated]
      properties:
        projects: { type: array, items: { $ref: '#/components/schemas/ProjectRef' } }
        issues: { type: array, items: { $ref: '#/components/schemas/Issue' }, description: 'Epics and standard issues in rank order (children name their parent)' }
        links: { type: array, items: { $ref: '#/components/schemas/TimelineLink' } }
        sprints: { type: array, items: { $ref: '#/components/schemas/TimelineSprint' } }
        releases: { type: array, items: { $ref: '#/components/schemas/TimelineRelease' } }
        externalLinks:
          type: array
          items: { $ref: '#/components/schemas/ExternalDependency' }
          description: Blocks links between a shown issue and an unfinished one that isn't shown (another project, filtered out or past the limit)
        truncated: { type: boolean, description: More issues matched than the timeline shows (2000) }
    ExternalDependency:
      type: object
      required: [issueKey, direction, open]
      description: A blocks link from a shown issue to one outside the view. The other issue is named only when you can browse it.
      properties:
        issueKey: { type: string, description: The shown issue }
        direction: { type: string, enum: [blockedBy, blocks], description: 'blockedBy: the other issue blocks the shown one' }
        open: { type: boolean, description: The other issue is not done (always true; done ones are left out) }
        linkId: { type: string, format: uuid, description: Present when you can browse the other issue }
        other: { $ref: '#/components/schemas/Issue' }
    ProgrammeTeams:
      type: object
      required: [teams, hiddenCount]
      properties:
        teams: { type: array, items: { $ref: '#/components/schemas/ProjectRef' } }
        hiddenCount: { type: integer, description: Teams in the programme that you can't browse }
    ProgrammeOverview:
      type: object
      required: [programme, teams, hiddenTeamCount, initiatives, unlinkedEpics, unlinkedTotal, truncated]
      properties:
        programme: { $ref: '#/components/schemas/ProjectRef' }
        teams: { type: array, items: { $ref: '#/components/schemas/ProjectRef' } }
        hiddenTeamCount: { type: integer, description: Teams you can't browse; their epics and work are left out }
        initiatives: { type: array, items: { $ref: '#/components/schemas/ProgrammeInitiative' } }
        unlinkedEpics: { type: array, items: { $ref: '#/components/schemas/Issue' }, description: Open epics in the teams that belong to no initiative (up to 50) }
        unlinkedTotal: { type: integer }
        truncated: { type: boolean, description: More initiatives or epics than the overview shows }
    ProgrammeInitiative:
      type: object
      required: [issue, progress, epics, flags]
      properties:
        issue: { $ref: '#/components/schemas/Issue' }
        progress: { $ref: '#/components/schemas/IssueProgress', description: The work under its epics (in teams you can browse) }
        forecastDate: { type: string, format: date, description: The latest forecast of its epics }
        epics: { type: array, items: { $ref: '#/components/schemas/ProgrammeEpic' } }
        flags: { type: array, items: { $ref: '#/components/schemas/PlanningFlag' } }
    ProgrammeEpic:
      type: object
      required: [issue, progress, flags]
      properties:
        issue: { $ref: '#/components/schemas/Issue' }
        progress: { $ref: '#/components/schemas/IssueProgress' }
        forecastDate: { type: string, format: date, description: The latest due date of the epic and the issues under it }
        flags: { type: array, items: { $ref: '#/components/schemas/PlanningFlag' } }
    PlanningFlag:
      type: object
      required: [kind, message, issueKeys, hiddenCount]
      properties:
        kind: { type: string, enum: [late, overdue, unscheduled, blocked] }
        message: { type: string }
        issueKeys: { type: array, items: { type: string }, description: 'blocked: the unfinished issues in other projects doing the blocking, those you can browse' }
        hiddenCount: { type: integer, description: 'blocked: blocking issues you can''t browse' }
    TimelineLink:
      type: object
      required: [id, sourceKey, targetKey, type]
      properties:
        id: { type: string, format: uuid }
        sourceKey: { type: string, description: The blocking issue }
        targetKey: { type: string, description: The blocked issue }
        type: { type: string, enum: [blocks] }
    TimelineSprint:
      type: object
      required: [projectKey, sprint]
      properties:
        projectKey: { type: string }
        sprint: { $ref: '#/components/schemas/Sprint' }
    TimelineRelease:
      type: object
      required: [projectKey, release]
      properties:
        projectKey: { type: string }
        release: { $ref: '#/components/schemas/Release' }
    SearchResult:
      type: object
      required: [issues, total, limit, offset]
      properties:
        issues: { type: array, items: { $ref: '#/components/schemas/Issue' } }
        total: { type: integer }
        limit: { type: integer }
        offset: { type: integer }
    SearchMetadata:
      type: object
      required: [fields, functions]
      properties:
        fields:
          type: array
          items: { $ref: '#/components/schemas/SearchField' }
        functions: { type: array, items: { type: string } }
    SearchField:
      type: object
      required: [name, type, operators]
      properties:
        name: { type: string }
        type: { type: string }
        operators: { type: array, items: { type: string } }
        values: { type: array, items: { type: string } }
    SavedFilter:
      type: object
      required: [id, name, description, jql, shared, owner, mine]
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        description: { type: string }
        jql: { type: string }
        shared: { type: boolean }
        owner: { $ref: '#/components/schemas/User' }
        mine: { type: boolean }
    SavedFilterInput:
      type: object
      required: [name, jql]
      properties:
        name: { type: string, minLength: 1, maxLength: 120 }
        description: { type: string }
        jql: { type: string }
        shared: { type: boolean }

    FieldDefinition:
      type: object
      required: [id, key, name, description, type, options, reserved]
      properties:
        id: { type: string, format: uuid }
        key: { type: string }
        name: { type: string }
        description: { type: string }
        type: { $ref: '#/components/schemas/FieldType' }
        options: { type: array, items: { type: string } }
        reserved: { type: boolean, description: 'A field of risks and decisions (risk_*, decision_*): set up by the project setting and not changed or deleted here' }
        projectKeys: { type: array, items: { type: string }, description: Omitted = all projects }
        issueTypeKeys: { type: array, items: { type: string }, description: Omitted = all issue types }
    FieldType: { type: string, enum: [text, number, select, multiselect, user, date, version, checkbox, url] }
    FieldDefinitionInput:
      type: object
      required: [key, name, type]
      properties:
        key: { type: string, pattern: '^[a-z][a-z0-9_]{0,40}$' }
        name: { type: string, minLength: 1, maxLength: 80 }
        description: { type: string }
        type: { $ref: '#/components/schemas/FieldType' }
        options: { type: array, items: { type: string } }
        projectKeys: { type: array, items: { type: string } }
        issueTypeKeys: { type: array, items: { type: string } }

    Notification:
      type: object
      required: [id, kind, title, body, createdAt]
      properties:
        id: { type: integer, format: int64 }
        kind: { type: string }
        title: { type: string }
        body: { type: string }
        issueKey: { type: string }
        actor: { $ref: '#/components/schemas/User' }
        readAt: { type: string, format: date-time }
        createdAt: { type: string, format: date-time }
    NotificationList:
      type: object
      required: [items, unread]
      properties:
        items: { type: array, items: { $ref: '#/components/schemas/Notification' } }
        unread: { type: integer }

    EmailDelivery:
      type: string
      enum: [immediate, digest, "off"]
      description: immediate = within a couple of minutes (changes to one issue are combined); digest = in the daily digest; off = no email.
    EmailNotificationPreference:
      type: object
      required: [kind, label, description, delivery, defaultDelivery]
      properties:
        kind: { type: string, description: 'Notification kind: assigned, commented, transitioned or automation' }
        label: { type: string }
        description: { type: string }
        delivery: { $ref: '#/components/schemas/EmailDelivery' }
        defaultDelivery: { $ref: '#/components/schemas/EmailDelivery' }
        deliveries: { type: array, items: { $ref: '#/components/schemas/EmailDelivery' }, description: The choices this kind offers }
    EmailNotificationSettings:
      type: object
      required: [address, preferences, digestHour, timeZone, paused, nextDigestAt, emailConfigured]
      properties:
        address: { type: string, description: Where the emails go (your account's address) }
        preferences: { type: array, items: { $ref: '#/components/schemas/EmailNotificationPreference' } }
        digestHour: { type: integer, minimum: 0, maximum: 23, description: Hour of the day (in timeZone) the digest is sent }
        timeZone: { type: string, description: 'IANA time zone, e.g. Europe/London' }
        paused: { type: boolean, description: No notification emails at all while paused }
        lastDigestAt: { type: string, format: date-time }
        nextDigestAt: { type: string, format: date-time, description: When the next digest goes out if there is anything to send }
        emailConfigured: { type: boolean, description: False when the server has no SMTP server and only keeps mail in memory }
    EmailNotificationSettingsPatch:
      type: object
      properties:
        preferences:
          type: array
          items:
            type: object
            required: [kind, delivery]
            properties:
              kind: { type: string }
              delivery: { $ref: '#/components/schemas/EmailDelivery' }
        digestHour: { type: integer, minimum: 0, maximum: 23 }
        timeZone: { type: string }
        paused: { type: boolean }
    EmailTransport: { type: string, enum: [smtp, memory] }
    Emailed:
      type: boolean
      description: |
        Whether the email was queued for delivery through the SMTP server. False when the server
        has none (WFM_SMTP_HOST unset): the message is only logged and kept in memory.
    EmailStatus:
      type: object
      required: [transport, from, publicUrl, unsubscribeUrl, batchWindowSeconds]
      properties:
        transport: { $ref: '#/components/schemas/EmailTransport' }
        host: { type: string }
        port: { type: integer }
        tls: { type: string, enum: [starttls, tls, none] }
        username: { type: string, description: SMTP user name if set (the password is never shown) }
        from: { type: string }
        publicUrl: { type: string, description: Links in emails point here (WFM_PUBLIC_URL) }
        unsubscribeUrl: { type: string, description: One-click unsubscribe endpoint (on WFM_HOOKS_PUBLIC_URL) }
        batchWindowSeconds: { type: integer, description: Immediate emails wait this long so changes to one issue arrive as one email }
    EmailTestResult:
      type: object
      required: [to, transport]
      properties:
        to: { type: string }
        transport: { $ref: '#/components/schemas/EmailTransport' }

    ScmConnection:
      type: object
      required: [id, provider, name, apiUrl, webUrl, manageHooks, writeBack, webhookUrl, createdAt, repositoryCount]
      properties:
        id: { type: string, format: uuid }
        provider: { $ref: '#/components/schemas/ScmProvider' }
        name: { type: string }
        apiUrl: { type: string }
        webUrl: { type: string }
        appId: { type: integer, format: int64 }
        appSlug: { type: string }
        manageHooks: { type: boolean }
        writeBack: { type: boolean }
        webhookUrl: { type: string, description: URL to configure as the App / hook delivery URL }
        createdAt: { type: string, format: date-time }
        lastEventAt: { type: string, format: date-time }
        repositoryCount: { type: integer }
    ScmConnectionCreated:
      allOf:
        - $ref: '#/components/schemas/ScmConnection'
        - type: object
          required: [webhookSecret]
          properties:
            webhookSecret: { type: string, description: The webhook secret (generated when not supplied); only returned on creation }
    ScmConnectionInput:
      type: object
      required: [provider, name]
      properties:
        provider: { $ref: '#/components/schemas/ScmProvider' }
        name: { type: string, minLength: 1, maxLength: 100 }
        apiUrl: { type: string, description: Defaults to https://api.github.com or https://gitlab.com }
        webUrl: { type: string }
        appId: { type: integer, format: int64, description: GitHub App id }
        appSlug: { type: string }
        privateKey: { type: string, description: GitHub App private key (PEM) }
        token: { type: string, description: GitLab group or project access token }
        webhookSecret: { type: string, description: Generated when omitted }
        manageHooks: { type: boolean, default: false }
        writeBack: { type: boolean, default: true }
    ScmConnectionPatch:
      type: object
      properties:
        name: { type: string, minLength: 1, maxLength: 100 }
        privateKey: { type: string }
        token: { type: string }
        webhookSecret: { type: string }
        manageHooks: { type: boolean }
        writeBack: { type: boolean }
    ScmRepository:
      type: object
      required: [id, connectionId, provider, fullPath, webUrl, defaultBranch, projectKeys]
      properties:
        id: { type: string, format: uuid }
        connectionId: { type: string, format: uuid }
        provider: { $ref: '#/components/schemas/ScmProvider' }
        fullPath: { type: string }
        webUrl: { type: string }
        defaultBranch: { type: string }
        projectKeys: { type: array, items: { type: string } }
    WebhookEvent:
      type: object
      required: [id, eventType, deliveryId, status, attempts, receivedAt]
      properties:
        id: { type: string, format: uuid }
        eventType: { type: string }
        deliveryId: { type: string }
        status: { type: string, enum: [pending, processed, ignored, failed] }
        error: { type: string }
        attempts: { type: integer }
        receivedAt: { type: string, format: date-time }
        processedAt: { type: string, format: date-time }

    # Import from Jira
    JiraFlavor:
      type: string
      enum: [cloud, datacenter]
      description: Jira Cloud (REST v3, email + API token) or Jira Data Center / Server (REST v2, personal access token)
    JiraConnectionInput:
      type: object
      required: [flavor, baseUrl, token]
      properties:
        flavor: { $ref: '#/components/schemas/JiraFlavor' }
        baseUrl: { type: string, description: 'Site URL, e.g. https://acme.atlassian.net or https://jira.example.com' }
        email: { type: string, description: Email of the Atlassian account that owns the API token (Cloud only) }
        token: { type: string, writeOnly: true, description: API token (Cloud) or personal access token (Data Center) }
    JiraConnectionTest:
      type: object
      required: [serverTitle, version, deploymentType, user]
      properties:
        serverTitle: { type: string }
        version: { type: string }
        deploymentType: { type: string }
        user:
          type: object
          required: [displayName]
          properties:
            displayName: { type: string }
            email: { type: string }
    JiraImportStatus:
      type: string
      enum: [draft, scanning, scanned, running, succeeded, failed, cancelled]
    JiraImport:
      type: object
      required: [id, flavor, baseUrl, email, serverTitle, hasCredentials, status, stalled, scanStale, config, targets, people, createdAt, updatedAt]
      properties:
        id: { type: string, format: uuid }
        flavor: { $ref: '#/components/schemas/JiraFlavor' }
        baseUrl: { type: string }
        email: { type: string }
        serverTitle: { type: string }
        hasCredentials: { type: boolean }
        status: { $ref: '#/components/schemas/JiraImportStatus' }
        mode: { type: string, enum: [scan, import], description: The kind of the current or last run }
        stalled: { type: boolean, description: A run stopped checkpointing (worker restarted); it can be resumed or cancelled }
        scanStale: { type: boolean, description: The scan is missing or does not cover the selected projects }
        config: { $ref: '#/components/schemas/JiraImportConfig' }
        targets: { type: array, items: { $ref: '#/components/schemas/JiraImportTarget' } }
        scan: { $ref: '#/components/schemas/JiraScan' }
        people: { type: array, items: { $ref: '#/components/schemas/JiraPerson' } }
        progress: { $ref: '#/components/schemas/JiraImportProgress' }
        error: { type: string }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        startedAt: { type: string, format: date-time }
        finishedAt: { type: string, format: date-time }
    JiraImportPatch:
      type: object
      properties:
        config: { $ref: '#/components/schemas/JiraImportConfig' }
        email: { type: string, description: New account email (Cloud), with token }
        token: { type: string, writeOnly: true, description: New API token or personal access token }
    JiraImportConfig:
      type: object
      required: [projects, priorities, storyPointsField, people, componentsAsLabels, attachments]
      properties:
        projects: { type: array, items: { $ref: '#/components/schemas/JiraProjectTarget' } }
        priorities:
          type: object
          description: Jira priority id → priority here
          additionalProperties: { $ref: '#/components/schemas/Priority' }
        storyPointsField: { type: string, description: 'Jira field id holding story points; "none" to skip' }
        people:
          type: object
          description: Jira person id (account id or user key) → how they are represented here
          additionalProperties: { $ref: '#/components/schemas/JiraPersonMapping' }
        componentsAsLabels: { type: boolean, description: 'Import components as labels named "component:<name>"' }
        attachments: { type: boolean, description: Download attachments (up to the attachment size limit) }
    JiraProjectTarget:
      type: object
      required: [jiraId, jiraKey, jiraName, key, name, template, open, statuses, types]
      properties:
        jiraId: { type: string }
        jiraKey: { type: string }
        jiraName: { type: string }
        key: { type: string, description: Project key here (issues keep their numbers) }
        name: { type: string }
        template: { type: string, description: 'scrum, kanban or bug_tracking; empty = from the Jira boards' }
        open: { type: boolean, description: Open to every organisation member; otherwise private }
        statuses:
          type: object
          description: Jira status id → status name here
          additionalProperties: { type: string }
        types:
          type: object
          description: Jira issue type id → issue type key here
          additionalProperties: { type: string }
    JiraPersonMapping:
      type: object
      required: [action]
      properties:
        action:
          type: string
          enum: [auto, user, placeholder, skip]
          description: 'auto: the account with the same email, else a placeholder; user: a chosen account; placeholder: always a placeholder; skip: leave empty'
        userId: { type: string, format: uuid }
        email: { type: string, description: Email for the placeholder (lets the person claim it by signing in) }
    JiraImportTarget:
      type: object
      required: [jiraId, statuses, types]
      properties:
        jiraId: { type: string }
        existingKey: { type: string, description: Key of the project an earlier run created }
        statuses:
          type: array
          items:
            type: object
            required: [name, category]
            properties:
              name: { type: string }
              category: { $ref: '#/components/schemas/StatusCategory' }
        types:
          type: array
          items:
            type: object
            required: [key, name, level, statuses]
            properties:
              key: { type: string }
              name: { type: string }
              level: { type: integer }
              statuses: { type: array, items: { type: string } }
    JiraProject:
      type: object
      required: [id, key, name, type, teamManaged]
      properties:
        id: { type: string }
        key: { type: string }
        name: { type: string }
        type: { type: string }
        teamManaged: { type: boolean }
        lead: { type: string }
        importedAs: { type: string, description: Key here of the project an earlier import of this site created }
    JiraValueCount:
      type: object
      required: [id, name, count]
      properties:
        id: { type: string }
        name: { type: string }
        count: { type: integer }
    JiraScan:
      type: object
      required: [complete, projects, priorities, people, storyPointFields, oversizeAttachments, oversizeCount]
      properties:
        complete: { type: boolean }
        scannedAt: { type: string, format: date-time }
        projects: { type: array, items: { $ref: '#/components/schemas/JiraScanProject' } }
        priorities: { type: array, items: { $ref: '#/components/schemas/JiraValueCount' } }
        people:
          type: array
          items: { $ref: '#/components/schemas/JiraScanPerson' }
        storyPointFields: { type: array, items: { $ref: '#/components/schemas/JiraValueCount' } }
        oversizeAttachments:
          type: array
          items:
            type: object
            required: [issueKey, filename, size]
            properties:
              issueKey: { type: string }
              filename: { type: string }
              size: { type: integer, format: int64 }
        oversizeCount: { type: integer }
    JiraScanProject:
      type: object
      required: [jiraId, key, name, boardType, issues, maxNumber, comments, attachments, attachmentBytes, sprints, versions, statuses, types, pairs]
      properties:
        jiraId: { type: string }
        key: { type: string }
        name: { type: string }
        boardType: { type: string }
        issues: { type: integer }
        maxNumber: { type: integer }
        comments: { type: integer }
        attachments: { type: integer }
        attachmentBytes: { type: integer, format: int64 }
        sprints: { type: integer }
        versions: { type: integer }
        statuses:
          type: array
          items:
            type: object
            required: [id, name, category, count]
            properties:
              id: { type: string }
              name: { type: string }
              category: { $ref: '#/components/schemas/StatusCategory' }
              count: { type: integer }
        types:
          type: array
          items:
            type: object
            required: [id, name, level, count]
            properties:
              id: { type: string }
              name: { type: string }
              level: { type: integer }
              count: { type: integer }
        pairs:
          type: array
          description: Issues per Jira issue type and status
          items:
            type: object
            required: [type, status, count]
            properties:
              type: { type: string }
              status: { type: string }
              count: { type: integer }
    JiraScanPerson:
      type: object
      required: [id, displayName, email, active, app, count]
      properties:
        id: { type: string }
        displayName: { type: string }
        email: { type: string }
        active: { type: boolean }
        app: { type: boolean }
        count: { type: integer }
    JiraPerson:
      allOf:
        - $ref: '#/components/schemas/JiraScanPerson'
        - type: object
          required: [placeholder]
          properties:
            match:
              type: object
              description: The account with the same email, or the one an earlier run mapped the person to
              required: [id, displayName, email, active]
              properties:
                id: { type: string, format: uuid }
                displayName: { type: string }
                email: { type: string }
                active: { type: boolean }
            placeholder: { type: boolean, description: The match is an unclaimed placeholder from an earlier import }
    JiraImportProblem:
      type: object
      required: [severity, kind, message, count]
      properties:
        severity: { type: string, enum: [error, warning, info] }
        kind: { type: string }
        project: { type: string }
        message: { type: string }
        count: { type: integer }
        examples: { type: array, items: { type: string } }
    JiraImportProgress:
      type: object
      required: [phases, counters, problems]
      properties:
        phases:
          type: array
          items:
            type: object
            required: [key, label, status, done, total]
            properties:
              key: { type: string }
              label: { type: string }
              status: { type: string, enum: [pending, running, done] }
              done: { type: integer }
              total: { type: integer }
        counters: { type: object, additionalProperties: { type: integer } }
        problems: { type: array, items: { $ref: '#/components/schemas/JiraImportProblem' } }
    JiraDryRun:
      type: object
      required: [ready, projects, people, problems]
      properties:
        ready: { type: boolean, description: No problem blocks the import }
        projects:
          type: array
          items:
            type: object
            required: [jiraKey, key, name, template, existing, issues, comments, attachments, attachmentBytes, skippedAttachments, sprints, versions]
            properties:
              jiraKey: { type: string }
              key: { type: string }
              name: { type: string }
              template: { type: string }
              existing: { type: boolean, description: The project exists from an earlier run and is updated }
              issues: { type: integer }
              comments: { type: integer }
              attachments: { type: integer }
              attachmentBytes: { type: integer, format: int64 }
              skippedAttachments: { type: integer }
              sprints: { type: integer }
              versions: { type: integer }
        people:
          type: object
          required: [matched, placeholders, unclaimable, skipped]
          properties:
            matched: { type: integer }
            placeholders: { type: integer }
            unclaimable: { type: integer }
            skipped: { type: integer }
        problems: { type: array, items: { $ref: '#/components/schemas/JiraImportProblem' } }

    AutomationTrigger:
      type: object
      required: [type]
      properties:
        type:
          type: string
          enum: [issue.created, issue.transitioned, issue.field_changed, pr.opened, pr.merged, commit.pushed, branch.created, pipeline.completed, deployment.completed, schedule, sla.breached]
        toStatus: { type: string, description: 'issue.transitioned: only when entering this status (name)' }
        toCategory: { $ref: '#/components/schemas/StatusCategory', description: 'issue.transitioned: only when entering a status of this category' }
        field: { type: string, description: 'issue.field_changed: the field(s) to watch, comma-separated (e.g. "startDate, dueDate")' }
        cron: { type: string, description: 'schedule: 5-field cron expression (UTC)' }
        jql: { type: string, description: 'schedule: issues to run the actions against' }
        environment: { type: string, description: 'deployment.completed: only deployments to this environment (case-insensitive)' }
        status:
          type: string
          enum: [success, failed]
          description: 'deployment.completed: only deployments that succeeded, or that failed'
    AutomationAction:
      type: object
      required: [type]
      properties:
        type:
          type: string
          enum: [transition, assign, set_field, comment, add_label, webhook, notify, rollup_dates, chat]
          description: 'rollup_dates sets the target issue''s start and due dates to span its children'
        target:
          type: string
          enum: [issue, parent, children]
          description: 'Which issue(s) the action applies to: the triggering issue (default), its parent (e.g. the epic of a story) or its children'
        status: { type: string, description: 'transition: target status name' }
        category: { $ref: '#/components/schemas/StatusCategory', description: 'transition: move to a status of this category instead of a named status (the first one the workflow allows)' }
        assignee: { type: string, description: 'assign: actor | reporter | unassigned | <email>' }
        field: { type: string }
        value: {}
        body: { type: string, description: 'comment: Markdown body' }
        label: { type: string }
        url: { type: string }
        recipients: { type: string, description: 'notify: assignee | reporter | watchers' }
        channelId: { type: string, format: uuid, description: 'chat: one of the project''s chat channels (body is the message, Markdown with {{issue.*}} / {{trigger.*}} / {{event.*}})' }
        secret:
          type: string
          writeOnly: true
          description: 'webhook: signs the body as `X-WFM-Signature-256: sha256=<hex HMAC-SHA256>`, like outbound webhooks. Stored encrypted and never returned. Omit to keep the stored secret (dropped if `url` changes); `""` removes it'
        authHeader: { type: string, description: 'webhook: header carrying `authValue` (default `Authorization`); not `Host`, `Content-Type`, `Content-Length`, `User-Agent` or `X-WFM-*`' }
        authValue:
          type: string
          writeOnly: true
          description: 'webhook: sent in `authHeader`, e.g. `Bearer <token>`. Stored encrypted and never returned. Omit to keep the stored value (dropped if `url` changes); `""` removes it'
        secretSet: { type: boolean, readOnly: true, description: 'webhook: a signing secret is stored' }
        authSet: { type: boolean, readOnly: true, description: 'webhook: an auth header value is stored' }
    AutomationRule:
      type: object
      required: [id, name, description, enabled, trigger, condition, actions, createdAt]
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        description: { type: string }
        enabled: { type: boolean }
        trigger: { $ref: '#/components/schemas/AutomationTrigger' }
        condition: { type: string, description: Optional CEL expression }
        actions: { type: array, items: { $ref: '#/components/schemas/AutomationAction' } }
        lastRunAt: { type: string, format: date-time }
        nextRunAt: { type: string, format: date-time }
        createdAt: { type: string, format: date-time }
    AutomationRuleInput:
      type: object
      required: [name, trigger, actions]
      properties:
        name: { type: string, minLength: 1, maxLength: 120 }
        description: { type: string }
        enabled: { type: boolean, default: true }
        trigger: { $ref: '#/components/schemas/AutomationTrigger' }
        condition: { type: string }
        actions: { type: array, minItems: 1, items: { $ref: '#/components/schemas/AutomationAction' } }
    AutomationRun:
      type: object
      required: [id, status, message, depth, createdAt]
      properties:
        id: { type: integer, format: int64 }
        issueKey: { type: string }
        status: { type: string, enum: [success, skipped, failed] }
        message: { type: string }
        depth: { type: integer }
        createdAt: { type: string, format: date-time }

    WebhookSubscription:
      type: object
      required: [id, name, url, events, active, createdAt]
      properties:
        id: { type: string, format: uuid }
        projectKey: { type: string }
        name: { type: string }
        url: { type: string }
        events: { type: array, items: { type: string } }
        active: { type: boolean }
        createdAt: { type: string, format: date-time }
    WebhookSubscriptionInput:
      type: object
      required: [name, url, events]
      properties:
        projectKey: { type: string }
        name: { type: string, minLength: 1, maxLength: 100 }
        url: { type: string, format: uri }
        events:
          type: array
          minItems: 1
          items:
            type: string
            enum: ['*', issue.created, issue.updated, issue.transitioned, issue.deleted, comment.created, sprint.started, sprint.completed, release.released, sla.breached, deployment.completed]
        active: { type: boolean, default: true }
    WebhookSubscriptionCreated:
      allOf:
        - $ref: '#/components/schemas/WebhookSubscription'
        - type: object
          required: [secret]
          properties:
            secret: { type: string }
    ChatProvider: { type: string, enum: [slack, mattermost] }
    ChatEvent:
      type: string
      enum: ['*', issue.created, issue.transitioned, issue.assigned, comment.created, sprint.started, sprint.completed, release.released, pr.opened, pr.merged, pipeline.failed]
    ChatChannel:
      type: object
      required: [id, provider, name, events, jql, active, urlHint, createdAt]
      properties:
        id: { type: string, format: uuid }
        provider: { $ref: '#/components/schemas/ChatProvider' }
        name: { type: string, description: 'Label for people, e.g. #team-payments' }
        events: { type: array, items: { $ref: '#/components/schemas/ChatEvent' } }
        jql: { type: string, description: Only issues matching this query notify (issue events) }
        active: { type: boolean }
        urlHint: { type: string, description: 'Host and last characters of the webhook URL (the URL itself is never returned)' }
        createdAt: { type: string, format: date-time }
    ChatChannelInput:
      type: object
      required: [provider, name, webhookUrl, events]
      properties:
        provider: { $ref: '#/components/schemas/ChatProvider' }
        name: { type: string, minLength: 1, maxLength: 100 }
        webhookUrl: { type: string, description: The channel's incoming webhook URL }
        events: { type: array, minItems: 1, items: { $ref: '#/components/schemas/ChatEvent' } }
        jql: { type: string }
        active: { type: boolean, default: true }
    ChatChannelPatch:
      type: object
      properties:
        name: { type: string, minLength: 1, maxLength: 100 }
        webhookUrl: { type: string, description: Omit to keep the current URL }
        events: { type: array, minItems: 1, items: { $ref: '#/components/schemas/ChatEvent' } }
        jql: { type: string }
        active: { type: boolean }
    ChatDelivery:
      type: object
      required: [id, event, status, attempts, createdAt]
      properties:
        id: { type: string, format: uuid }
        event: { type: string }
        issueKey: { type: string }
        status: { type: string, enum: [pending, delivered, failed] }
        attempts: { type: integer }
        responseStatus: { type: integer }
        lastError: { type: string }
        createdAt: { type: string, format: date-time }
        deliveredAt: { type: string, format: date-time }
    WebhookDelivery:
      type: object
      required: [id, event, status, attempts, createdAt]
      properties:
        id: { type: string, format: uuid }
        event: { type: string }
        status: { type: string, enum: [pending, delivered, failed] }
        attempts: { type: integer }
        responseStatus: { type: integer }
        lastError: { type: string }
        createdAt: { type: string, format: date-time }
        deliveredAt: { type: string, format: date-time }

    # AI assistants (OAuth)
    OAuthScope:
      type: string
      enum: ['mcp:read', 'mcp:write']
      description: mcp:read = the MCP read tools; mcp:write = also the tools that create, edit, comment on and move issues.
    OAuthClient:
      type: object
      required: [id, name, kind, host]
      properties:
        id: { type: string, description: Generated for apps that registered themselves; the metadata document URL otherwise }
        name: { type: string, description: The name the app gives itself }
        kind:
          type: string
          enum: [dynamic, metadata]
          description: dynamic = registered itself (RFC 7591); metadata = identified by a client ID metadata document served from its own https URL
        host: { type: string, description: The host serving a metadata document (verified), or the host of a registered app's first redirect URI }
        clientUri: { type: string }
        logoUri: { type: string }
    OAuthConsent:
      type: object
      required: [client, redirectUri, redirectHost, nativeRedirect, requestedScopes, alreadyGranted]
      properties:
        client: { $ref: '#/components/schemas/OAuthClient' }
        redirectUri: { type: string }
        redirectHost: { type: string, description: Host (or app scheme) the browser returns to }
        nativeRedirect: { type: boolean, description: The browser returns to an app on this computer (a loopback address or an app link) }
        requestedScopes: { type: array, items: { $ref: '#/components/schemas/OAuthScope' } }
        grantedScopes: { type: array, items: { $ref: '#/components/schemas/OAuthScope' }, description: What you allowed this app before, if anything }
        alreadyGranted: { type: boolean, description: An earlier approval covers this request }
    OAuthConsentDecision:
      type: object
      required: [request, approve]
      properties:
        request: { type: string }
        approve: { type: boolean }
        scopes: { type: array, items: { $ref: '#/components/schemas/OAuthScope' }, description: Required when approving; among those requested }
    OAuthRedirect:
      type: object
      required: [redirectUrl]
      properties:
        redirectUrl: { type: string }
    ConnectedApp:
      type: object
      required: [id, client, scopes, createdAt]
      properties:
        id: { type: string, format: uuid }
        client: { $ref: '#/components/schemas/OAuthClient' }
        scopes: { type: array, items: { $ref: '#/components/schemas/OAuthScope' } }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        lastUsedAt: { type: string, format: date-time }
        user: { $ref: '#/components/schemas/User', description: The person who connected it (administration listing) }

    # Service desk -------------------------------------------------------------
    Visibility:
      type: string
      enum: [public, internal]
      description: 'Service requests only: a public reply reaches the customer (portal and email); an internal note only agents see.'
    PortalAccess:
      type: string
      enum: [customers, organisation]
      description: 'Who may raise requests: customers = people on the customer list; organisation = also every organisation member (not guests)'
    ServiceDesk:
      type: object
      required: [projectKey, portalName, portalDescription, portalAccess, selfSignup, portalUrl, customerCount, openProject, releaseNotesProjects]
      properties:
        projectKey: { type: string }
        portalName: { type: string }
        portalDescription: { type: string }
        portalAccess: { $ref: '#/components/schemas/PortalAccess' }
        selfSignup: { type: boolean, description: Anyone may create a customer account from the portal (confirmed by email) }
        portalUrl: { type: string }
        customerCount: { type: integer }
        openProject: { type: boolean, description: 'The project is open: every organisation member can browse it as an agent, internal notes included' }
        releaseNotesProjects:
          type: array
          items: { $ref: '#/components/schemas/ProjectRef' }
          description: Software projects whose published release notes the help centre shows on its What's new page
    ServiceDeskPatch:
      type: object
      properties:
        portalName: { type: string, minLength: 1, maxLength: 100 }
        portalDescription: { type: string, maxLength: 2000 }
        portalAccess: { $ref: '#/components/schemas/PortalAccess' }
        selfSignup: { type: boolean }
        releaseNotesProjectKeys:
          type: array
          maxItems: 50
          items: { type: string }
          description: Software projects (ones you can browse) whose published release notes the help centre shows; replaces the list
    FormFieldType:
      type: string
      enum: [text, textarea, select, multiselect, number, date, checkbox, url]
    RequestFormField:
      type: object
      required: [field, label, required]
      properties:
        field: { type: string, description: 'summary, description, priority, dueDate or a custom field key (not user or version fields)' }
        label: { type: string }
        help: { type: string }
        required: { type: boolean }
        type: { $ref: '#/components/schemas/FormFieldType', description: Derived from the field (ignored on input) }
        options: { type: array, items: { type: string }, description: Choices of select fields (ignored on input) }
        showWhen: { $ref: '#/components/schemas/FormCondition' }
    FormCondition:
      type: object
      description: >-
        Show the field only when an earlier select, multiselect, checkbox or priority field's answer is one of
        `values` (any of them for a multiselect; `true` or `false` for a checkbox). `required` applies only while the
        field is shown, and answers to hidden fields are ignored.
      required: [field, values]
      properties:
        field: { type: string, description: The earlier field's key }
        values: { type: array, minItems: 1, items: { type: string } }
    RequestApprovalConfig:
      type: object
      required: [mode, approvedStatus, declinedStatus]
      properties:
        mode: { type: string, enum: [any, all], description: 'any: one approval is enough; all: every approver must approve. One decline declines.' }
        approverIds: { type: array, items: { type: string, format: uuid } }
        groupId: { type: string, format: uuid, description: The group's members when the request is raised are asked too }
        approvedStatus: { type: string, description: Status the request moves to when approved }
        declinedStatus: { type: string, description: Status the request moves to when declined }
        approvers: { type: array, items: { $ref: '#/components/schemas/PortalPerson' }, description: The named approvers (output) }
        groupName: { type: string, description: Output }
    RequestTypeRef:
      type: object
      required: [id, name, icon]
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        icon: { type: string }
    RequestType:
      type: object
      required: [id, name, description, icon, issueType, group, position, fields, requestCount]
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        description: { type: string }
        icon: { type: string }
        issueType: { $ref: '#/components/schemas/IssueTypeRef' }
        group: { type: string, description: Portal group heading ("" for none) }
        position: { type: integer }
        fields: { type: array, items: { $ref: '#/components/schemas/RequestFormField' } }
        approval: { $ref: '#/components/schemas/RequestApprovalConfig' }
        requestCount: { type: integer }
    RequestTypeInput:
      type: object
      required: [name, issueTypeKey]
      properties:
        name: { type: string, minLength: 1, maxLength: 120 }
        description: { type: string, maxLength: 2000 }
        icon: { type: string, description: 'help, bug, laptop, key, access, question, change or document' }
        issueTypeKey: { type: string }
        group: { type: string, maxLength: 60 }
        position: { type: integer }
        fields: { type: array, maxItems: 30, items: { $ref: '#/components/schemas/RequestFormField' } }
        approval: { $ref: '#/components/schemas/RequestApprovalConfig' }
    ServiceCustomer:
      type: object
      required: [user, addedAt, account]
      properties:
        user: { $ref: '#/components/schemas/User' }
        addedAt: { type: string, format: date-time }
        account: { type: string, enum: [customer, member], description: 'customer: a portal-only account; member: an organisation account' }
        invited: { type: boolean, description: Has not chosen a password yet }
        active: { type: boolean }
    ServiceCustomerInput:
      type: object
      required: [email]
      properties:
        email: { type: string, minLength: 3, maxLength: 254 }
        displayName: { type: string, maxLength: 120 }
    ServiceCustomerAdded:
      type: object
      required: [customer, invited, emailed]
      properties:
        customer: { $ref: '#/components/schemas/ServiceCustomer' }
        invited: { type: boolean, description: A new customer account was created with an invitation to choose a password }
        emailed: { $ref: '#/components/schemas/Emailed' }
    Queue:
      type: object
      required: [id, name, jql, position, count]
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        jql: { type: string, description: 'Within the project; e.g. assignee is EMPTY AND resolution = Unresolved ORDER BY "Time to resolution"' }
        position: { type: integer }
        count: { type: integer }
    QueueInput:
      type: object
      required: [name, jql]
      properties:
        name: { type: string, minLength: 1, maxLength: 80 }
        jql: { type: string, maxLength: 4000 }
        position: { type: integer }
    QueueItem:
      type: object
      required: [issue, slas]
      properties:
        issue: { $ref: '#/components/schemas/Issue' }
        requestType: { $ref: '#/components/schemas/RequestTypeRef' }
        slas: { type: array, items: { $ref: '#/components/schemas/Sla' } }
    QueueIssues:
      type: object
      required: [queue, total, items]
      properties:
        queue: { $ref: '#/components/schemas/Queue' }
        total: { type: integer }
        items: { type: array, items: { $ref: '#/components/schemas/QueueItem' } }
    SlaMetric: { type: string, enum: [first_response, resolution] }
    Sla:
      type: object
      required: [metric, name, state, targetSeconds, elapsedSeconds, remainingSeconds, breached]
      properties:
        metric: { $ref: '#/components/schemas/SlaMetric' }
        name: { type: string }
        state: { type: string, enum: [running, paused, stopped] }
        targetSeconds: { type: integer, format: int64 }
        elapsedSeconds: { type: integer, format: int64, description: Working time used (in the goal's calendar) }
        remainingSeconds: { type: integer, format: int64, description: Working time left; negative once breached }
        breached: { type: boolean }
        breachAt: { type: string, format: date-time, description: 'Running clocks: when the goal is (or was) missed' }
        stoppedAt: { type: string, format: date-time }
        calendar: { type: string, description: Calendar name; omitted = around the clock }
    SlaHours:
      type: object
      required: [day, start, end]
      properties:
        day: { type: integer, minimum: 0, maximum: 6, description: 0 = Sunday }
        start: { type: string, description: 'HH:MM' }
        end: { type: string, description: 'HH:MM (24:00 for midnight)' }
    SlaCalendar:
      type: object
      required: [name, timeZone, hours, holidays]
      properties:
        id: { type: string, format: uuid }
        name: { type: string, minLength: 1, maxLength: 80 }
        timeZone: { type: string, description: 'IANA time zone, e.g. Europe/London' }
        hours: { type: array, items: { $ref: '#/components/schemas/SlaHours' }, description: No hours = around the clock }
        holidays: { type: array, items: { type: string }, description: 'Days off, YYYY-MM-DD' }
    SlaGoal:
      type: object
      required: [targetMinutes]
      properties:
        priorities: { type: array, items: { $ref: '#/components/schemas/Priority' }, description: Only requests of these priorities (empty = any) }
        requestTypeIds: { type: array, items: { type: string, format: uuid }, description: Only requests of these types (empty = any) }
        jql: { type: string, description: Only requests matching this query (empty = any) }
        targetMinutes: { type: integer, minimum: 1, maximum: 5256000 }
        calendar: { type: string, description: Calendar name; omitted = around the clock }
    SlaMetricConfig:
      type: object
      required: [metric, name, pauseStatuses, goals]
      properties:
        metric: { $ref: '#/components/schemas/SlaMetric' }
        name: { type: string }
        pauseStatuses: { type: array, items: { type: string }, description: Status names in which the clock pauses }
        goals: { type: array, maxItems: 50, items: { $ref: '#/components/schemas/SlaGoal' }, description: The first goal whose conditions match a request applies }
    SlaConfig:
      type: object
      required: [calendars, metrics]
      properties:
        calendars: { type: array, maxItems: 20, items: { $ref: '#/components/schemas/SlaCalendar' } }
        metrics: { type: array, items: { $ref: '#/components/schemas/SlaMetricConfig' } }
    PortalPerson:
      type: object
      required: [id, displayName]
      properties:
        id: { type: string, format: uuid }
        displayName: { type: string }
        avatarUrl: { type: string }
    Approver:
      type: object
      required: [person]
      properties:
        person: { $ref: '#/components/schemas/PortalPerson' }
        decision: { type: string, enum: [approved, declined] }
        comment: { type: string }
        decidedAt: { type: string, format: date-time }
    Approval:
      type: object
      required: [id, status, mode, approvers, createdAt, canDecide]
      properties:
        id: { type: string, format: uuid }
        status: { type: string, enum: [pending, approved, declined] }
        mode: { type: string, enum: [any, all] }
        approvers: { type: array, items: { $ref: '#/components/schemas/Approver' } }
        createdAt: { type: string, format: date-time }
        decidedAt: { type: string, format: date-time }
        canDecide: { type: boolean, description: The caller is an approver who has not decided yet }
    ApprovalDecision:
      type: object
      required: [decision]
      properties:
        decision: { type: string, enum: [approve, decline] }
        comment: { type: string, maxLength: 5000 }
    ServiceRequest:
      type: object
      required: [issueKey, slas, approvals, participants, portalUrl, customerVisible]
      properties:
        issueKey: { type: string }
        requestType: { $ref: '#/components/schemas/RequestTypeRef' }
        channel: { type: string, enum: [portal, agent] }
        slas: { type: array, items: { $ref: '#/components/schemas/Sla' } }
        approvals: { type: array, items: { $ref: '#/components/schemas/Approval' } }
        participants: { type: array, items: { $ref: '#/components/schemas/User' } }
        portalUrl: { type: string }
        customerVisible: { type: boolean, description: The reporter and participants see it in the portal (it has a request type) }
    ServiceRequestPatch:
      type: object
      required: [requestTypeId]
      properties:
        requestTypeId: { type: string, format: uuid }
    ParticipantInput:
      type: object
      required: [email]
      properties:
        email: { type: string }
    PortalDeskRef:
      type: object
      required: [projectKey, name]
      properties:
        projectKey: { type: string }
        name: { type: string }
    PortalDesk:
      type: object
      required: [projectKey, name, description, requestTypeCount]
      properties:
        projectKey: { type: string }
        name: { type: string }
        description: { type: string }
        requestTypeCount: { type: integer }
    PortalFormField:
      type: object
      required: [key, label, type, required]
      properties:
        key: { type: string }
        label: { type: string }
        help: { type: string }
        type: { $ref: '#/components/schemas/FormFieldType' }
        required: { type: boolean, description: Required while the field is shown }
        options: { type: array, items: { type: string } }
        showWhen: { $ref: '#/components/schemas/FormCondition' }
    PortalRequestType:
      type: object
      required: [id, name, description, icon, group, fields, needsApproval]
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        description: { type: string }
        icon: { type: string }
        group: { type: string }
        fields: { type: array, items: { $ref: '#/components/schemas/PortalFormField' } }
        needsApproval: { type: boolean }
    PortalDeskDetail:
      type: object
      required: [projectKey, name, description, requestTypes, whatsNew]
      properties:
        projectKey: { type: string }
        name: { type: string }
        description: { type: string }
        requestTypes: { type: array, items: { $ref: '#/components/schemas/PortalRequestType' } }
        whatsNew: { type: boolean, description: 'The desk''s What''s new page has release notes' }
    PortalReleaseNote:
      type: object
      required: [id, product, name, notes]
      properties:
        id: { type: string, format: uuid }
        product: { type: string, description: The software project's name }
        name: { type: string, description: The release's name }
        date: { type: string, format: date, description: When it was released, else its planned release date }
        notes: { type: string, description: Markdown }
    PortalRequestCreate:
      type: object
      required: [requestTypeId, values]
      properties:
        requestTypeId: { type: string, format: uuid }
        values: { type: object, additionalProperties: true, description: Form values by field key }
    PortalRequest:
      type: object
      required: [key, summary, status, requestType, desk, reporter, createdAt, updatedAt, role]
      properties:
        key: { type: string }
        summary: { type: string }
        status: { $ref: '#/components/schemas/Status' }
        requestType: { $ref: '#/components/schemas/RequestTypeRef' }
        desk: { $ref: '#/components/schemas/PortalDeskRef' }
        reporter: { $ref: '#/components/schemas/PortalPerson' }
        resolution: { type: string }
        createdAt: { type: string, format: date-time }
        updatedAt:
          type: string
          format: date-time
          description: |
            The latest activity the caller can see: the request was raised, a reply they can see, a
            status change or a file added in the help centre. Internal notes and other changes
            agents make do not count.
        role: { type: string, enum: [reporter, participant, approver], description: Why the caller sees it }
    PortalFieldValue:
      type: object
      required: [label, value]
      properties:
        label: { type: string }
        value: { type: string }
        type: { $ref: '#/components/schemas/FormFieldType' }
    PortalComment:
      type: object
      required: [id, author, body, createdAt, fromSupport]
      properties:
        id: { type: string, format: uuid }
        author: { $ref: '#/components/schemas/PortalPerson' }
        body: { type: string }
        createdAt: { type: string, format: date-time }
        fromSupport: { type: boolean, description: An agent's reply (not written in the portal) }
    PortalAttachment:
      type: object
      required: [id, filename, contentType, size, createdAt, url]
      properties:
        id: { type: string, format: uuid }
        filename: { type: string }
        contentType: { type: string }
        size: { type: integer, format: int64 }
        createdAt: { type: string, format: date-time }
        url: { type: string }
    PortalRequestDetail:
      allOf:
        - $ref: '#/components/schemas/PortalRequest'
        - type: object
          required: [fields, comments, attachments, approvals, participants, canComment]
          properties:
            fields: { type: array, items: { $ref: '#/components/schemas/PortalFieldValue' } }
            comments: { type: array, items: { $ref: '#/components/schemas/PortalComment' } }
            attachments: { type: array, items: { $ref: '#/components/schemas/PortalAttachment' } }
            approvals: { type: array, items: { $ref: '#/components/schemas/Approval' } }
            participants: { type: array, items: { $ref: '#/components/schemas/PortalPerson' } }
            canComment: { type: boolean }
    PortalCommentInput:
      type: object
      required: [body]
      properties:
        body: { type: string, minLength: 1, maxLength: 50000 }
    PortalSignupInfo:
      type: object
      required: [projectKey, name, description]
      properties:
        projectKey: { type: string }
        name: { type: string }
        description: { type: string }
    PortalSignup:
      type: object
      required: [email, displayName]
      properties:
        email: { type: string, minLength: 3, maxLength: 254 }
        displayName: { type: string, minLength: 1, maxLength: 120 }
