datasource db {
  provider  = "postgresql"
  url       = env("DATABASE_URL")
  directUrl = env("DIRECT_URL")
}

generator client {
  provider        = "prisma-client-js"
  output          = "../generated/prisma"
  binaryTargets   = ["native", "debian-openssl-1.1.x"]
  previewFeatures = ["metrics"]
}

model User {
  id    String @id @default(cuid())
  email String @unique

  authenticationMethod      AuthenticationMethod
  authenticationProfile     Json?
  authenticationExtraParams Json?
  authIdentifier            String?              @unique

  displayName String?
  name        String?
  avatarUrl   String?

  admin Boolean @default(false)

  /// Preferences for the dashboard
  dashboardPreferences Json?

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  /// @deprecated
  isOnCloudWaitlist      Boolean @default(false)
  /// @deprecated
  featureCloud           Boolean @default(false)
  /// @deprecated
  isOnHostedRepoWaitlist Boolean @default(false)

  marketingEmails       Boolean @default(true)
  confirmedBasicDetails Boolean @default(false)

  referralSource String?
  onboardingData Json?

  orgMemberships OrgMember[]
  sentInvites    OrgMemberInvite[]

  mfaEnabledAt         DateTime?
  mfaSecretReference   SecretReference? @relation(fields: [mfaSecretReferenceId], references: [id])
  mfaSecretReferenceId String?
  /// Hash of the last used code to prevent replay attacks
  mfaLastUsedCode      String?

  /// Maximum session lifetime in seconds for this user. Default = 1 year (Gregorian).
  /// May be further restricted by Organization.maxSessionDuration on any of the user's orgs.
  sessionDuration Int @default(31556952)

  /// Absolute timestamp at which the user's current session must end. Written
  /// at login (and any time the effective duration is recomputed) and shortened
  /// when an admin lowers an org cap. Read on requireUser / getUser; null is
  /// treated as "no enforced deadline" (legacy sessions from before this
  /// shipped). Read-on-demand has zero per-request DB cost — the value
  /// piggybacks on the User fetch that requireUser already does.
  nextSessionEnd DateTime?

  invitationCode       InvitationCode?       @relation(fields: [invitationCodeId], references: [id])
  invitationCodeId     String?
  personalAccessTokens PersonalAccessToken[]
  deployments          WorkerDeployment[]
  backupCodes          MfaBackupCode[]
  bulkActions          BulkActionGroup[]

  impersonationsPerformed          ImpersonationAuditLog[]           @relation("ImpersonationAdmin")
  impersonationsReceived           ImpersonationAuditLog[]           @relation("ImpersonationTarget")
  customerQueries                  CustomerQuery[]
  metricsDashboards                MetricsDashboard[]
  platformNotifications            PlatformNotification[]
  platformNotificationInteractions PlatformNotificationInteraction[]
}

model MfaBackupCode {
  id   String @id @default(cuid())
  /// Hash of the actual code
  code String

  user   User   @relation(fields: [userId], references: [id])
  userId String

  usedAt DateTime?

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  @@unique([userId, code])
}

// @deprecated This model is no longer used as the Cloud is out of private beta
// Leaving it here for now for historical reasons
model InvitationCode {
  id   String @id @default(cuid())
  code String @unique

  users User[]

  createdAt DateTime @default(now())
}

enum AuthenticationMethod {
  GITHUB
  MAGIC_LINK
  GOOGLE
  SSO
}

/// Used to generate PersonalAccessTokens, they're one-time use
model AuthorizationCode {
  id String @id @default(cuid())

  code String @unique

  personalAccessToken   PersonalAccessToken? @relation(fields: [personalAccessTokenId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  personalAccessTokenId String?

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

// Used by User's to perform API actions
model PersonalAccessToken {
  id String @id @default(cuid())

  /// If generated by the CLI this will be "cli", otherwise user-provided
  name String

  /// This is the token encrypted using the ENCRYPTION_KEY
  encryptedToken Json

  /// This is shown in the UI, with ********
  obfuscatedToken String

  /// This is used to find the token in the database
  hashedToken String @unique

  user   User   @relation(fields: [userId], references: [id])
  userId String

  revokedAt      DateTime?
  lastAccessedAt DateTime?

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  authorizationCodes AuthorizationCode[]
}

enum OrganizationAccessTokenType {
  USER
  SYSTEM
}

model OrganizationAccessToken {
  id String @id @default(cuid())

  /// User-provided name for the token
  name String

  /// Used to differentiate between user-generated and system-generated tokens
  type OrganizationAccessTokenType @default(USER)

  /// This is used to find the token in the database
  hashedToken String @unique

  organization   Organization @relation(fields: [organizationId], references: [id])
  organizationId String

  /// Optional expiration date for the token
  expiresAt      DateTime?
  revokedAt      DateTime?
  lastAccessedAt DateTime?

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  @@index([organizationId, type, createdAt(sort: Desc)])
}

model Organization {
  id    String @id @default(cuid())
  slug  String @unique
  title String

  maximumExecutionTimePerRunInMs Int @default(900000) // 15 minutes
  maximumConcurrencyLimit        Int @default(10)
  /// This is deprecated and will be removed in the future
  maximumSchedulesLimit          Int @default(5)

  maximumDevQueueSize      Int?
  maximumDeployedQueueSize Int?

  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt
  deletedAt DateTime?

  companySize String?

  onboardingData Json?

  avatar Json?

  runsEnabled Boolean @default(true)

  isActivated Boolean @default(false) @map("v3Enabled")

  /// @deprecated
  v2Enabled      Boolean @default(false)
  /// @deprecated
  v2MarqsEnabled Boolean @default(false)
  /// @deprecated
  hasRequestedV3 Boolean @default(false)

  environments RuntimeEnvironment[]

  apiRateLimiterConfig      Json?
  realtimeRateLimiterConfig Json?

  batchRateLimitConfig        Json?
  batchQueueConcurrencyConfig Json?

  featureFlags Json?

  maximumProjectCount Int @default(25)

  /// Maximum session lifetime in seconds for members of this organization.
  /// When set, caps each member's User.sessionDuration. The most restrictive cap across
  /// all of a user's orgs (excluding nulls) wins.
  maxSessionDuration Int?

  projects                 Project[]
  members                  OrgMember[]
  invites                  OrgMemberInvite[]
  organizationIntegrations OrganizationIntegration[]
  organizationAccessTokens OrganizationAccessToken[]
  workerGroups             WorkerInstanceGroup[]
  workerInstances          WorkerInstance[]
  githubAppInstallations   GithubAppInstallation[]
  customerQueries          CustomerQuery[]
  metricsDashboards        MetricsDashboard[]
  prompts                  Prompt[]

  platformNotifications PlatformNotification[]
  errorGroupStates      ErrorGroupState[]

  /// S2 basin that holds this org's realtime streams. Null until the
  /// per-org basin has been provisioned (OSS / s2-lite installs leave
  /// it null forever; reads fall back to the global basin env var).
  /// Set once at provisioning time; retention is reconfigured in-place
  /// when the org's plan changes.
  streamBasinName String?
}

model OrgMember {
  id String @id @default(cuid())

  organization   Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  organizationId String

  user   User   @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  userId String

  role OrgMemberRole @default(MEMBER)

  createdAt    DateTime             @default(now())
  updatedAt    DateTime             @updatedAt
  environments RuntimeEnvironment[]

  @@unique([organizationId, userId])
}

enum OrgMemberRole {
  ADMIN
  MEMBER
}

model OrgMemberInvite {
  id    String        @id @default(cuid())
  token String        @unique @default(cuid())
  email String
  role  OrgMemberRole @default(MEMBER)

  /// Optional RBAC role to assign on invite acceptance. When set, the
  /// accept-invite flow calls the loaded RBAC plugin's setUserRole with
  /// this id after creating the OrgMember. Null = legacy behaviour, the
  /// runtime fallback derives the role from `role` above.
  ///
  /// Plain text (not an FK) — the RBAC plugin's RbacRole table lives on
  /// a separate schema (Drizzle, not Prisma) so we can't model the FK
  /// here. Validation happens at write time (action) and read time
  /// (acceptInvite).
  rbacRoleId     String?
  organization   Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  organizationId String

  inviter   User   @relation(fields: [inviterId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  inviterId String

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  @@unique([organizationId, email])
}

model RuntimeEnvironment {
  id     String @id @default(cuid())
  slug   String
  apiKey String @unique

  /// @deprecated was for v2
  pkApiKey String @unique

  type RuntimeEnvironmentType @default(DEVELOPMENT)

  // Preview branches
  /// If true, this environment has branches and is treated differently in the dashboard/API
  /// NB: this flag is NOT used for Development branches, instead (type, parentEnvironmentId) = (DEVELOPMENT, NULL) is used
  isBranchableEnvironment Boolean              @default(false)
  branchName              String?
  parentEnvironment       RuntimeEnvironment?  @relation("parentEnvironment", fields: [parentEnvironmentId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  parentEnvironmentId     String?
  childEnvironments       RuntimeEnvironment[] @relation("parentEnvironment")

  // This is GitMeta type
  git Json?

  /// When set API calls will fail
  archivedAt DateTime?

  ///A memorable code for the environment
  shortcode String

  maximumConcurrencyLimit     Int                     @default(5)
  concurrencyLimitBurstFactor Decimal                 @default("2.00") @db.Decimal(4, 2)
  paused                      Boolean                 @default(false)
  pauseSource                 EnvironmentPauseSource?

  autoEnableInternalSources Boolean @default(true)

  organization   Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  organizationId String

  project   Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  projectId String

  //when the org member is deleted, it will keep the environment but set it to null
  orgMember   OrgMember? @relation(fields: [orgMemberId], references: [id], onDelete: SetNull, onUpdate: Cascade)
  orgMemberId String?

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  /// Allows us to customize the built-in environment variables for a specific environment, like TRIGGER_OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT
  builtInEnvironmentVariableOverrides Json?

  tunnelId String?

  backgroundWorkers          BackgroundWorker[]
  backgroundWorkerTasks      BackgroundWorkerTask[]
  taskRuns                   TaskRun[]
  taskQueues                 TaskQueue[]
  environmentVariableValues  EnvironmentVariableValue[]
  checkpoints                Checkpoint[]
  workerDeployments          WorkerDeployment[]
  workerDeploymentPromotions WorkerDeploymentPromotion[]
  taskRunAttempts            TaskRunAttempt[]
  CheckpointRestoreEvent     CheckpointRestoreEvent[]
  taskScheduleInstances      TaskScheduleInstance[]
  alerts                     ProjectAlert[]

  sessions                RuntimeEnvironmentSession[]
  currentSession          RuntimeEnvironmentSession?  @relation("currentSession", fields: [currentSessionId], references: [id], onDelete: SetNull, onUpdate: Cascade)
  currentSessionId        String?
  taskRunNumberCounter    TaskRunNumberCounter[]
  taskRunCheckpoints      TaskRunCheckpoint[]
  waitpoints              Waitpoint[]
  workerInstances         WorkerInstance[]
  waitpointTags           WaitpointTag[]
  BulkActionGroup         BulkActionGroup[]
  customerQueries         CustomerQuery[]
  prompts                 Prompt[]
  playgroundConversations PlaygroundConversation[]
  errorGroupStates        ErrorGroupState[]
  taskIdentifiers         TaskIdentifier[]
  revokedApiKeys          RevokedApiKey[]

  @@unique([projectId, slug, orgMemberId])
  @@unique([projectId, shortcode])
  @@index([parentEnvironmentId])
  @@index([projectId])
  @@index([organizationId])
}

/// Records of previously-valid API keys that are still accepted for authentication
/// during a grace window after rotation. Extend or end the grace period by updating `expiresAt`.
model RevokedApiKey {
  id                   String             @id @default(cuid())
  apiKey               String
  runtimeEnvironment   RuntimeEnvironment @relation(fields: [runtimeEnvironmentId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  runtimeEnvironmentId String
  expiresAt            DateTime
  createdAt            DateTime           @default(now())

  @@index([apiKey])
  @@index([runtimeEnvironmentId])
}

enum RuntimeEnvironmentType {
  PRODUCTION
  STAGING
  DEVELOPMENT
  PREVIEW
}

enum EnvironmentPauseSource {
  BILLING_LIMIT
}

model Project {
  id   String @id @default(cuid())
  slug String @unique
  name String

  externalRef String @unique

  organization   Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  organizationId String

  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt
  deletedAt DateTime?

  version ProjectVersion   @default(V2)
  engine  RunEngineVersion @default(V1)

  builderProjectId String?

  workerGroups WorkerInstanceGroup[]
  workers      WorkerInstance[]

  defaultWorkerGroup   WorkerInstanceGroup? @relation("ProjectDefaultWorkerGroup", fields: [defaultWorkerGroupId], references: [id])
  defaultWorkerGroupId String?

  /// The master queues they are allowed to use (impacts what they can set as default and trigger runs with)
  allowedWorkerQueues String[] @default([]) @map("allowedMasterQueues")

  environments                   RuntimeEnvironment[]
  backgroundWorkers              BackgroundWorker[]
  backgroundWorkerTasks          BackgroundWorkerTask[]
  taskRuns                       TaskRun[]
  runTags                        TaskRunTag[]
  taskQueues                     TaskQueue[]
  environmentVariables           EnvironmentVariable[]
  checkpoints                    Checkpoint[]
  WorkerDeployment               WorkerDeployment[]
  CheckpointRestoreEvent         CheckpointRestoreEvent[]
  taskSchedules                  TaskSchedule[]
  alertChannels                  ProjectAlertChannel[]
  alerts                         ProjectAlert[]
  alertStorages                  ProjectAlertStorage[]
  bulkActionGroups               BulkActionGroup[]
  BackgroundWorkerFile           BackgroundWorkerFile[]
  waitpoints                     Waitpoint[]
  taskRunWaitpoints              TaskRunWaitpoint[]
  taskRunCheckpoints             TaskRunCheckpoint[]
  waitpointTags                  WaitpointTag[]
  connectedGithubRepository      ConnectedGithubRepository?
  organizationProjectIntegration OrganizationProjectIntegration[]
  customerQueries                CustomerQuery[]
  playgroundConversations        PlaygroundConversation[]

  buildSettings         Json?
  onboardingData        Json?
  taskScheduleInstances TaskScheduleInstance[]
  metricsDashboards     MetricsDashboard[]
  llmModels             LlmModel[]
  prompts               Prompt[]
  platformNotifications PlatformNotification[]
  errorGroupStates      ErrorGroupState[]
  taskIdentifiers       TaskIdentifier[]
}

enum ProjectVersion {
  V2
  V3
}

model SecretReference {
  id       String              @id @default(cuid())
  key      String              @unique
  provider SecretStoreProvider @default(DATABASE)

  environmentVariableValues EnvironmentVariableValue[]

  createdAt               DateTime                  @default(now())
  updatedAt               DateTime                  @updatedAt
  OrganizationIntegration OrganizationIntegration[]
  User                    User[]
}

enum SecretStoreProvider {
  DATABASE
  AWS_PARAM_STORE
}

model SecretStore {
  key     String @unique
  value   Json
  version String @default("1")

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  @@index([key(ops: raw("text_pattern_ops"))], type: BTree)
}

model DataMigration {
  id   String @id @default(cuid())
  name String @unique

  createdAt   DateTime  @default(now())
  updatedAt   DateTime  @updatedAt
  completedAt DateTime?
}

// ====================================================
// v3 Models
// ====================================================
model BackgroundWorker {
  id String @id @default(cuid())

  friendlyId String @unique

  engine RunEngineVersion @default(V1)

  contentHash String
  sdkVersion  String @default("unknown")
  cliVersion  String @default("unknown")

  project   Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  projectId String

  runtimeEnvironment   RuntimeEnvironment @relation(fields: [runtimeEnvironmentId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  runtimeEnvironmentId String

  version  String
  metadata Json

  runtime        String?
  runtimeVersion String?

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  tasks          BackgroundWorkerTask[]
  attempts       TaskRunAttempt[]
  lockedRuns     TaskRun[]
  files          BackgroundWorkerFile[]
  queues         TaskQueue[]
  promptVersions PromptVersion[]

  deployment WorkerDeployment?

  workerGroup   WorkerInstanceGroup? @relation(fields: [workerGroupId], references: [id], onDelete: SetNull, onUpdate: Cascade)
  workerGroupId String?

  supportsLazyAttempts Boolean @default(false)

  taskIdentifiers TaskIdentifier[]

  @@unique([projectId, runtimeEnvironmentId, version])
  @@index([runtimeEnvironmentId])
  // Get the latest worker for a given environment
  @@index([runtimeEnvironmentId, createdAt(sort: Desc)])
}

model BackgroundWorkerFile {
  id String @id @default(cuid())

  friendlyId String @unique

  filePath    String
  contentHash String
  contents    Bytes

  project   Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  projectId String

  backgroundWorkers BackgroundWorker[]

  tasks BackgroundWorkerTask[]

  createdAt DateTime @default(now())

  @@unique([projectId, contentHash])
}

model Prompt {
  id          String  @id @default(cuid())
  friendlyId  String  @unique @map("friendly_id")
  slug        String
  description String?
  type        String  @default("text") // "text" | "chat"

  organization   Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  organizationId String

  project   Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  projectId String

  runtimeEnvironment   RuntimeEnvironment @relation(fields: [runtimeEnvironmentId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  runtimeEnvironmentId String

  filePath       String?
  exportName     String?
  variableSchema Json?
  defaultModel   String?
  defaultConfig  Json?

  tags       String[]  @default([])
  archivedAt DateTime?

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  versions PromptVersion[]

  @@unique([projectId, runtimeEnvironmentId, slug])
  @@index([projectId])
  @@index([runtimeEnvironmentId])
  @@map("prompts")
}

model PromptVersion {
  id      String @id @default(cuid())
  version Int

  prompt   Prompt @relation(fields: [promptId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  promptId String

  textContent String?
  chatContent Json?

  model  String?
  config Json?

  source        String // "deploy" | "dashboard" | "api"
  commitMessage String?
  contentHash   String

  labels String[] @default([])

  worker   BackgroundWorker? @relation(fields: [workerId], references: [id])
  workerId String?

  createdBy String?
  createdAt DateTime @default(now())

  @@unique([promptId, version])
  @@index([promptId])
  @@index([contentHash])
  @@map("prompt_versions")
}

model BackgroundWorkerTask {
  id   String @id @default(cuid())
  slug String

  description String?

  friendlyId String @unique

  filePath   String
  exportName String?

  worker   BackgroundWorker @relation(fields: [workerId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  workerId String

  project   Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  projectId String

  file   BackgroundWorkerFile? @relation(fields: [fileId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  fileId String?

  runtimeEnvironment   RuntimeEnvironment @relation(fields: [runtimeEnvironmentId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  runtimeEnvironmentId String

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  attempts TaskRunAttempt[]
  runs     TaskRun[]

  queueConfig   Json?
  retryConfig   Json?
  machineConfig Json?

  queueId String?
  queue   TaskQueue? @relation(fields: [queueId], references: [id], onDelete: SetNull, onUpdate: Cascade)

  maxDurationInSeconds Int?

  ttl String?

  triggerSource TaskTriggerSource @default(STANDARD)

  /// Extra task configuration JSON. Shape depends on triggerSource.
  /// AGENT: { type: "ai-sdk-chat" }
  config Json?

  payloadSchema Json?

  @@unique([workerId, slug])
  // Quick lookup of task identifiers
  @@index([projectId, slug])
  @@index([runtimeEnvironmentId, projectId])
  @@index([runtimeEnvironmentId, slug, triggerSource])
}

enum TaskTriggerSource {
  STANDARD
  SCHEDULED
  AGENT
}

model PlaygroundConversation {
  id String @id @default(cuid())

  /// The chat session ID used by the transport
  chatId String

  /// User-editable conversation title (auto-generated from first message)
  title String @default("New conversation")

  /// Which agent this conversation is with
  agentSlug String

  /// The current active run backing this conversation (null if no run yet)
  runId String?
  run   TaskRun? @relation(fields: [runId], references: [id], onDelete: SetNull, onUpdate: Cascade)

  /// The client data JSON used for this conversation
  clientData Json?

  /// Accumulated UIMessages from completed turns (for resume without stream replay)
  messages Json?

  /// Last SSE event ID — resume from this position to avoid replaying old turns
  lastEventId String?

  project   Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  projectId String

  runtimeEnvironment   RuntimeEnvironment @relation(fields: [runtimeEnvironmentId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  runtimeEnvironmentId String

  /// The user who started this conversation
  userId String

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  @@unique([chatId, runtimeEnvironmentId])
  @@index([runtimeEnvironmentId, agentSlug, updatedAt(sort: Desc)])
  @@index([userId, runtimeEnvironmentId])
}

/// Durable, typed, bidirectional I/O primitive. Owns two S2 streams (.out / .in).
/// The row is essentially static — no status, no counters, no pointers. No
/// foreign keys: project/runtimeEnvironment/organization ids are plain
/// scalar columns (matches TaskRun pattern). List-style queries are served
/// from ClickHouse, not Postgres, so only point-lookup indexes live here.
model Session {
  id         String  @id @default(cuid())
  friendlyId String  @unique
  /// User-supplied identifier scoped to the environment. Used for
  /// idempotent upsert and for resolving sessions via the public API.
  externalId String?

  /// Plain string — intentionally not an enum.
  type String

  /// Denormalized scoping columns — no FK relations.
  projectId            String
  runtimeEnvironmentId String
  environmentType      RuntimeEnvironmentType
  organizationId       String

  /// Task this session triggers runs against. Required — Sessions are
  /// task-bound: creating a session also triggers its first run, and
  /// every subsequent re-trigger uses this same identifier.
  taskIdentifier String

  /// Trigger config used for every run this session schedules. Shape
  /// (validated at the route layer, opaque to the DB):
  ///   { basePayload: object, machine?: string, queue?: string,
  ///     tags?: string[], maxAttempts?: number,
  ///     idleTimeoutInSeconds?: number }
  /// `basePayload` carries the customer's client-data; runtime fields
  /// (chatId, messages, trigger) are merged at trigger time.
  triggerConfig Json

  /// Whether this session was created from the Test/playground UI rather
  /// than by a real chat.agent() trigger. Mirrors TaskRun.isTest.
  isTest Boolean @default(false)

  tags     String[] @default([])
  metadata Json?

  /// Live run pointer — non-FK so run deletion never cascades. Can lag
  /// reality; the `.in/append` handler re-checks the snapshot status
  /// before reusing it.
  currentRunId      String?
  /// Monotonic counter used for optimistic locking on `currentRunId`
  /// swaps. Bumped atomically alongside any update that changes
  /// `currentRunId`.
  currentRunVersion Int     @default(0)

  /// Terminal markers — written once, never flipped back.
  closedAt     DateTime?
  closedReason String?
  expiresAt    DateTime?

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  /// S2 basin where this session's stream pair lives. Stamped at create
  /// time from `Organization.streamBasinName` so reads can resolve the
  /// basin without joining org. Null when the org has no per-org basin
  /// (OSS, or pre-backfill); reads fall back to the global basin.
  streamBasinName String?

  /// Storage URI (with protocol prefix) for this session's chat.agent
  /// snapshot blob. Set on first snapshot write. Null = pre-column session,
  /// fall back to computed default path.
  chatSnapshotStoragePath String?

  runs SessionRun[]

  /// Idempotency: `(env, externalId)` uniquely identifies a session.
  /// PostgreSQL treats NULLs as distinct, so `externalId=NULL` rows never collide.
  @@unique([runtimeEnvironmentId, externalId])
  @@index([expiresAt])
  @@index([currentRunId])
}

/// Historical record of every run a Session has owned. Append-only —
/// rows are inserted on each `ensureRunForSession` claim, never updated.
/// Lets us reconstruct the run timeline of a chat for debugging /
/// dashboard surfaces. The relation cascades on Session delete (tied to
/// the session lifecycle) but `runId` is a plain string column with no
/// FK to TaskRun so run pruning is independent.
model SessionRun {
  id String @id @default(cuid())

  sessionId   String
  /// TaskRun.id (no FK — runs may be archived independently of session history)
  runId       String   @unique
  /// One of: "initial" | "continuation" | "upgrade" | "manual".
  /// Plain string for forward-compat with future trigger reasons.
  reason      String
  triggeredAt DateTime @default(now())

  session Session @relation(fields: [sessionId], references: [id], onDelete: Cascade)

  @@index([sessionId])
}

model TaskRun {
  id String @id @default(cuid())

  number     Int    @default(0)
  friendlyId String @unique

  engine RunEngineVersion @default(V1)

  status       TaskRunStatus @default(PENDING)
  statusReason String?

  idempotencyKey          String?
  idempotencyKeyExpiresAt DateTime?
  /// Stores the user-provided key and scope: { key: string, scope: "run" | "attempt" | "global" }
  idempotencyKeyOptions   Json?

  /// Debounce options: { key: string, delay: string, createdAt: Date }
  debounce Json?

  taskIdentifier String

  isTest Boolean @default(false)

  payload      String
  payloadType  String @default("application/json")
  context      Json?
  traceContext Json?

  traceId String
  spanId  String

  runtimeEnvironment   RuntimeEnvironment @relation(fields: [runtimeEnvironmentId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  runtimeEnvironmentId String

  environmentType RuntimeEnvironmentType?

  project   Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  projectId String

  organizationId String?

  // The specific queue this run is in
  queue         String
  // The queueId is set when the run is locked to a specific queue
  lockedQueueId String?

  /// The main queue that this run is part of
  workerQueue String @default("main") @map("masterQueue")

  /// User-facing geo region, stamped at trigger; workerQueue is where it actually ran.
  region String?

  /// @deprecated
  secondaryMasterQueue String?

  /// From engine v2+ this will be defined after a run has been dequeued (starting at 1)
  attemptNumber Int?

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  attempts TaskRunAttempt[] @relation("attempts")

  /// Denormized column that holds the raw tags
  runTags String[]

  /// Denormalized version of the background worker task
  taskVersion String?
  sdkVersion  String?
  cliVersion  String?

  checkpoints Checkpoint[]

  /// startedAt marks the point at which a run is dequeued from MarQS
  startedAt     DateTime?
  /// executedAt is set when the first attempt is about to execute
  executedAt    DateTime?
  completedAt   DateTime?
  machinePreset String?

  usageDurationMs Int   @default(0)
  costInCents     Float @default(0)
  baseCostInCents Float @default(0)

  lockedAt   DateTime?
  lockedBy   BackgroundWorkerTask? @relation(fields: [lockedById], references: [id])
  lockedById String?

  lockedToVersion   BackgroundWorker? @relation(fields: [lockedToVersionId], references: [id])
  lockedToVersionId String?

  /// The "priority" of the run. This is just a negative offset in ms for the queue timestamp
  /// E.g. a value of 60_000 would put the run into the queue 60s ago.
  priorityMs Int @default(0)

  concurrencyKey String?

  delayUntil        DateTime?
  queuedAt          DateTime?
  ttl               String?
  expiredAt         DateTime?
  maxAttempts       Int?
  lockedRetryConfig Json?

  /// optional token that can be used to authenticate the task run
  oneTimeUseToken String?

  ///When this run is finished, the waitpoint will be marked as completed
  associatedWaitpoint Waitpoint? @relation("CompletingRun")

  ///If there are any blocked waitpoints, the run won't be executed
  blockedByWaitpoints TaskRunWaitpoint[]

  /// All waitpoints that blocked this run at some point, used for display purposes
  connectedWaitpoints Waitpoint[] @relation("WaitpointRunConnections")

  /// Where the logs are stored
  taskEventStore String @default("taskEvent")

  queueTimestamp DateTime?

  batchItems             BatchTaskRunItem[]
  dependency             TaskRunDependency?
  CheckpointRestoreEvent CheckpointRestoreEvent[]
  executionSnapshots     TaskRunExecutionSnapshot[]

  scheduleInstanceId String?
  scheduleId         String?

  sourceBulkActionItems      BulkActionItem[] @relation("SourceActionItemRun")
  destinationBulkActionItems BulkActionItem[] @relation("DestinationActionItemRun")

  bulkActionGroupIds String[] @default([])

  logsDeletedAt DateTime?

  replayedFromTaskRunFriendlyId String?

  /// This represents the original task that that was triggered outside of a Trigger.dev task
  rootTaskRun   TaskRun? @relation("TaskRootRun", fields: [rootTaskRunId], references: [id], onDelete: SetNull, onUpdate: NoAction)
  rootTaskRunId String?

  /// The root run will have a list of all the descendant runs, children, grand children, etc.
  descendantRuns TaskRun[] @relation("TaskRootRun")

  /// The immediate parent run of this task run
  parentTaskRun   TaskRun? @relation("TaskParentRun", fields: [parentTaskRunId], references: [id], onDelete: SetNull, onUpdate: NoAction)
  parentTaskRunId String?

  /// The immediate child runs of this task run
  childRuns TaskRun[] @relation("TaskParentRun")

  /// The immediate parent attempt of this task run
  parentTaskRunAttempt   TaskRunAttempt? @relation("TaskParentRunAttempt", fields: [parentTaskRunAttemptId], references: [id], onDelete: SetNull, onUpdate: NoAction)
  parentTaskRunAttemptId String?

  /// The batch run that this task run is a part of
  batch   BatchTaskRun? @relation(fields: [batchId], references: [id], onDelete: SetNull, onUpdate: NoAction)
  batchId String?

  /// whether or not the task run was created because of a triggerAndWait for batchTriggerAndWait
  resumeParentOnCompletion Boolean @default(false)

  /// The depth of this task run in the task run hierarchy
  depth Int @default(0)

  /// The span ID of the "trigger" span in the parent task run
  parentSpanId String?

  /// Holds the state of the run chain for deadlock detection
  runChainState Json?

  /// seed run metadata
  seedMetadata     String?
  seedMetadataType String  @default("application/json")

  /// Run metadata
  metadata        String?
  metadataType    String  @default("application/json")
  metadataVersion Int     @default(1)

  /// Structured annotations: triggerSource, triggerAction, rootTriggerSource, rootScheduleId
  annotations Json?

  /// Whether the latest attempt was a warm start. Null until first attempt starts.
  isWarmStart Boolean?

  /// Run output
  output     String?
  outputType String  @default("application/json")

  /// Run error
  error Json?

  /// Organization's billing plan type (cached for fallback when billing API fails)
  planType String?

  maxDurationInSeconds Int?

  /// The version of the realtime streams implementation used by the run
  realtimeStreamsVersion String   @default("v1")
  /// Store the stream keys that are being used by the run
  realtimeStreams        String[] @default([])
  /// S2 basin where this run's realtime streams live. Stamped at create
  /// time from `Organization.streamBasinName` so reads can resolve the
  /// basin without joining org. Null when the org has no per-org basin
  /// (OSS, or pre-backfill); reads fall back to the global basin.
  streamBasinName        String?

  playgroundConversations PlaygroundConversation[]

  @@unique([oneTimeUseToken])
  @@unique([runtimeEnvironmentId, taskIdentifier, idempotencyKey])
  // Finding child runs
  @@index([parentTaskRunId])
  // Run page inspector
  @@index([spanId])
  @@index([parentSpanId])
  // Finding runs in a batch
  @@index([runTags(ops: ArrayOps)], type: Gin)
  @@index([runtimeEnvironmentId, batchId])
  @@index([runtimeEnvironmentId, createdAt(sort: Desc)])
  @@index([createdAt], type: Brin)
}

model TaskRunTemplate {
  id String @id @default(cuid())

  taskSlug      String
  triggerSource TaskTriggerSource

  label String

  payload            String?
  payloadType        String   @default("application/json")
  metadata           String?
  metadataType       String   @default("application/json")
  queue              String?
  concurrencyKey     String?
  ttlSeconds         Int?
  maxAttempts        Int?
  maxDurationSeconds Int?
  tags               String[]
  machinePreset      String?

  projectId      String
  organizationId String

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  @@index([projectId, taskSlug, triggerSource, createdAt(sort: Desc)])
}

enum TaskRunStatus {
  ///
  /// NON-FINAL STATUSES
  ///

  /// Task has been scheduled to run in the future
  DELAYED
  /// Task is waiting to be executed by a worker
  PENDING

  /// The run is pending a version update because it cannot execute without additional information (task, queue, etc.). Replaces WAITING_FOR_DEPLOY
  PENDING_VERSION

  /// Task hasn't been deployed yet but is waiting to be executed. Deprecated in favor of PENDING_VERSION
  WAITING_FOR_DEPLOY

  /// Task has been dequeued from the queue but is not yet executing
  DEQUEUED

  /// Task is currently being executed by a worker
  EXECUTING

  /// Task has been paused by the system, and will be resumed by the system
  WAITING_TO_RESUME

  /// Task has failed and is waiting to be retried
  RETRYING_AFTER_FAILURE

  /// Task has been paused by the user, and can be resumed by the user
  PAUSED

  ///
  /// FINAL STATUSES
  ///

  /// Task has been canceled by the user
  CANCELED

  /// Task was interrupted during execution, mostly this happens in development environments
  INTERRUPTED

  /// Task has been completed successfully
  COMPLETED_SUCCESSFULLY

  /// Task has been completed with errors
  COMPLETED_WITH_ERRORS

  /// Task has failed to complete, due to an error in the system
  SYSTEM_FAILURE

  /// Task has crashed and won't be retried, most likely the worker ran out of resources, e.g. memory or storage
  CRASHED

  /// Task reached the ttl without being executed
  EXPIRED

  /// Task has been timed out when using maxDuration
  TIMED_OUT
}

enum RunEngineVersion {
  /// The original version that uses marqs v1 and Graphile
  V1
  V2
}

/// Used by the RunEngine during TaskRun execution
/// It has the required information to transactionally progress a run through states,
/// and prevent side effects like heartbeats failing a run that has progressed.
/// It is optimised for performance and is designed to be cleared at some point,
/// so there are no cascading relationships to other models.
model TaskRunExecutionSnapshot {
  id String @id @default(cuid())

  /// This should always be 2+ (V1 didn't use the run engine or snapshots)
  engine RunEngineVersion @default(V2)

  /// The execution status
  executionStatus TaskRunExecutionStatus
  /// For debugging
  description     String

  /// We store invalid snapshots as a record of the run state when we tried to move
  isValid Boolean @default(true)
  error   String?

  /// The previous snapshot ID
  previousSnapshotId String?

  /// Run
  runId     String
  run       TaskRun       @relation(fields: [runId], references: [id])
  runStatus TaskRunStatus

  // Batch
  batchId String?

  /// This is the current run attempt number. Users can define how many attempts they want for a run.
  attemptNumber Int?

  /// Environment
  environmentId   String
  environmentType RuntimeEnvironmentType
  projectId       String
  organizationId  String

  /// Waitpoints that have been completed for this execution
  completedWaitpoints Waitpoint[] @relation("completedWaitpoints")

  /// An array of waitpoint IDs in the correct order, used for batches
  completedWaitpointOrder String[]

  /// Checkpoint
  checkpointId String?
  checkpoint   TaskRunCheckpoint? @relation(fields: [checkpointId], references: [id])

  /// Worker
  workerId String?
  runnerId String?

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  lastHeartbeatAt DateTime?

  /// Metadata used by various systems in the run engine
  metadata Json?

  /// Used to get the latest valid snapshot quickly
  @@index([runId, isValid, createdAt(sort: Desc)])
}

enum TaskRunExecutionStatus {
  /// Run has been created
  RUN_CREATED
  /// Run is delayed, waiting to be enqueued
  DELAYED
  /// Run is in the RunQueue
  QUEUED
  /// Run is in the RunQueue, and is also executing. This happens when a run is continued cannot reacquire concurrency
  QUEUED_EXECUTING
  /// Run has been pulled from the queue, but isn't executing yet
  PENDING_EXECUTING
  /// Run is executing on a worker
  EXECUTING
  /// Run is executing on a worker but is waiting for waitpoints to complete
  EXECUTING_WITH_WAITPOINTS
  /// Run has been suspended and may be waiting for waitpoints to complete before resuming
  SUSPENDED
  /// Run has been scheduled for cancellation
  PENDING_CANCEL
  /// Run is finished (success of failure)
  FINISHED
}

model TaskRunCheckpoint {
  id String @id @default(cuid())

  friendlyId String @unique

  type     TaskRunCheckpointType
  location String
  imageRef String?
  reason   String?
  metadata String?

  project   Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  projectId String

  runtimeEnvironment   RuntimeEnvironment @relation(fields: [runtimeEnvironmentId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  runtimeEnvironmentId String

  executionSnapshot TaskRunExecutionSnapshot[]

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

enum TaskRunCheckpointType {
  DOCKER
  KUBERNETES
  COMPUTE
}

/// A Waitpoint blocks a run from continuing until it's completed
/// If there's a waitpoint blocking a run, it shouldn't be in the queue
model Waitpoint {
  id String @id @default(cuid())

  friendlyId String @unique

  type   WaitpointType
  status WaitpointStatus @default(PENDING)

  completedAt DateTime?

  /// If it's an Event type waitpoint, this is the event. It can also be provided for the DATETIME type
  idempotencyKey             String
  /// If this is true then we can show it in the dashboard/return it from the SDK
  userProvidedIdempotencyKey Boolean

  /// If there's a user provided idempotency key, this is the time it expires at
  idempotencyKeyExpiresAt DateTime?

  /// If an idempotencyKey is no longer active, we store it here and generate a new one for the idempotencyKey field.
  /// Clearing an idempotencyKey is useful for debounce or cancelling child runs.
  /// This is a workaround because Prisma doesn't support partial indexes.
  inactiveIdempotencyKey String?

  /// If it's a RUN type waitpoint, this is the associated run
  completedByTaskRunId String?  @unique
  completedByTaskRun   TaskRun? @relation("CompletingRun", fields: [completedByTaskRunId], references: [id], onDelete: SetNull)

  /// If it's a DATETIME type waitpoint, this is the date.
  /// If it's a MANUAL waitpoint, this can be set as the `timeout`.
  completedAfter DateTime?

  /// If it's a BATCH type waitpoint, this is the associated batch
  completedByBatchId String?
  completedByBatch   BatchTaskRun? @relation(fields: [completedByBatchId], references: [id], onDelete: SetNull)

  /// The runs this waitpoint is blocking
  blockingTaskRuns TaskRunWaitpoint[]

  /// All runs that have ever been blocked by this waitpoint, used for display purposes
  connectedRuns TaskRun[] @relation("WaitpointRunConnections")

  /// When a waitpoint is complete
  completedExecutionSnapshots TaskRunExecutionSnapshot[] @relation("completedWaitpoints")

  /// When completed, an output can be stored here
  output        String?
  outputType    String  @default("application/json")
  outputIsError Boolean @default(false)

  project   Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  projectId String

  environment   RuntimeEnvironment @relation(fields: [environmentId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  environmentId String

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  /// Denormized column that holds the raw tags
  /// Denormalized column that holds the raw tags
  tags String[]

  /// Quickly find an idempotent waitpoint
  @@unique([environmentId, idempotencyKey])
  /// Quickly find a batch waitpoint
  @@index([completedByBatchId])
  /// Used on the Waitpoint dashboard pages
  /// Time period filtering
  @@index([environmentId, type, createdAt(sort: Desc)])
  /// Status filtering
  @@index([environmentId, type, status])
  /// For the waitpoint token dashboard page
  @@index([environmentId, type, id(sort: Desc)])
}

enum WaitpointType {
  RUN
  DATETIME
  MANUAL
  BATCH
}

enum WaitpointStatus {
  PENDING
  COMPLETED
}

model TaskRunWaitpoint {
  id String @id @default(cuid())

  taskRun   TaskRun @relation(fields: [taskRunId], references: [id])
  taskRunId String

  waitpoint   Waitpoint @relation(fields: [waitpointId], references: [id])
  waitpointId String

  project   Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  projectId String

  /// This span id is completed when the waitpoint is completed. This is used with cached runs (idempotent)
  spanIdToComplete String?

  //associated batch
  batchId    String?
  batch      BatchTaskRun? @relation(fields: [batchId], references: [id])
  //if there's an associated batch and this isn't set it's for the entire batch
  //if it is set, it's a specific run in the batch
  batchIndex Int?

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  /// There are two constraints, the one below and also one that Prisma doesn't support
  /// The second one implemented in SQL only prevents a TaskRun + Waitpoint with a null batchIndex
  @@unique([taskRunId, waitpointId, batchIndex])
  @@index([taskRunId])
  @@index([waitpointId])
}

model WaitpointTag {
  id   String @id @default(cuid())
  name String

  environment   RuntimeEnvironment @relation(fields: [environmentId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  environmentId String

  project   Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  projectId String

  createdAt DateTime @default(now())

  @@unique([environmentId, name])
}

model FeatureFlag {
  id String @id @default(cuid())

  key   String @unique
  value Json?

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

model WorkerInstance {
  id String @id @default(cuid())

  /// For example "worker-1"
  name String

  /// If managed, it will default to the name, e.g. "worker-1"
  /// If unmanged, it will be prefixed with the deployment ID e.g. "deploy-123-worker-1"
  resourceIdentifier String

  metadata Json?

  workerGroup   WorkerInstanceGroup @relation(fields: [workerGroupId], references: [id])
  workerGroupId String

  organization   Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  organizationId String?

  project   Project? @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  projectId String?

  environment   RuntimeEnvironment? @relation(fields: [environmentId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  environmentId String?

  deployment   WorkerDeployment? @relation(fields: [deploymentId], references: [id], onDelete: SetNull, onUpdate: Cascade)
  deploymentId String?

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  lastDequeueAt   DateTime?
  lastHeartbeatAt DateTime?

  @@unique([workerGroupId, resourceIdentifier])
}

enum WorkerInstanceGroupType {
  MANAGED
  UNMANAGED
}

enum WorkloadType {
  CONTAINER
  MICROVM
}

model WorkerInstanceGroup {
  id   String                  @id @default(cuid())
  type WorkerInstanceGroupType

  /// For example "us-east-1"
  name String

  /// If managed, it will default to the name, e.g. "us-east-1"
  /// If unmanged, it will be prefixed with the project ID e.g. "project_1-us-east-1"
  masterQueue String @unique

  /// "N. Virginia, USA", "Frankfurt, Germany", etc. Used for display purposes
  description String?
  hidden      Boolean @default(false)

  token   WorkerGroupToken @relation(fields: [tokenId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  tokenId String           @unique

  workers           WorkerInstance[]
  backgroundWorkers BackgroundWorker[]

  defaultForProjects Project[] @relation("ProjectDefaultWorkerGroup")

  organization   Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  organizationId String?

  project   Project? @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  projectId String?

  cloudProvider String?
  /// "usa", "europe", etc. Used like a pseudo enum for things like flags
  location      String?
  staticIPs     String?

  workloadType WorkloadType @default(CONTAINER)

  /// Geo region; container + MICROVM groups for one geo share it. Set-once once it has runs.
  region String?

  /// When true, runs enqueued to this worker queue may skip the intermediate queue
  /// and be pushed directly to the worker queue when concurrency is available.
  enableFastPath Boolean @default(false)

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

model WorkerGroupToken {
  id String @id @default(cuid())

  tokenHash String @unique

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  workerGroup WorkerInstanceGroup?
}

model TaskRunTag {
  id   String @id @default(cuid())
  name String

  friendlyId String @unique

  project   Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  projectId String

  createdAt DateTime @default(now())

  @@unique([projectId, name])
  //Makes run filtering by tag faster
  @@index([name, id])
}

/// This is used for triggerAndWait and batchTriggerAndWait. The taskRun is the child task, it points at a parent attempt or a batch
model TaskRunDependency {
  id String @id @default(cuid())

  /// The child run
  taskRun   TaskRun @relation(fields: [taskRunId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  taskRunId String  @unique

  checkpointEvent   CheckpointRestoreEvent? @relation(fields: [checkpointEventId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  checkpointEventId String?                 @unique

  /// An attempt that is dependent on this task run.
  dependentAttempt   TaskRunAttempt? @relation(fields: [dependentAttemptId], references: [id])
  dependentAttemptId String?

  /// A batch run that is dependent on this task run
  dependentBatchRun   BatchTaskRun? @relation("dependentBatchRun", fields: [dependentBatchRunId], references: [id])
  dependentBatchRunId String?

  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt
  resumedAt DateTime?

  @@index([dependentAttemptId])
  @@index([dependentBatchRunId])
}

/// deprecated, we hadn't included the project id in the unique constraint
model TaskRunCounter {
  taskIdentifier String @id
  lastNumber     Int    @default(0)
}

/// Used for the TaskRun number (e.g. #1,421)
model TaskRunNumberCounter {
  id String @id @default(cuid())

  taskIdentifier String
  environment    RuntimeEnvironment @relation(fields: [environmentId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  environmentId  String

  lastNumber Int @default(0)

  @@unique([taskIdentifier, environmentId])
}

/// This is not used from engine v2+, attempts use the TaskRunExecutionSnapshot and TaskRun
model TaskRunAttempt {
  id     String @id @default(cuid())
  number Int    @default(0)

  friendlyId String @unique

  taskRun   TaskRun @relation("attempts", fields: [taskRunId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  taskRunId String

  backgroundWorker   BackgroundWorker @relation(fields: [backgroundWorkerId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  backgroundWorkerId String

  backgroundWorkerTask   BackgroundWorkerTask @relation(fields: [backgroundWorkerTaskId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  backgroundWorkerTaskId String

  runtimeEnvironment   RuntimeEnvironment @relation(fields: [runtimeEnvironmentId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  runtimeEnvironmentId String

  queue   TaskQueue @relation(fields: [queueId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  queueId String

  status TaskRunAttemptStatus @default(PENDING)

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  startedAt   DateTime?
  completedAt DateTime?

  usageDurationMs Int @default(0)

  error      Json?
  output     String?
  outputType String  @default("application/json")

  dependencies      TaskRunDependency[]
  batchDependencies BatchTaskRun[]

  checkpoints            Checkpoint[]
  batchTaskRunItems      BatchTaskRunItem[]
  CheckpointRestoreEvent CheckpointRestoreEvent[]
  childRuns              TaskRun[]                @relation("TaskParentRunAttempt")

  @@unique([taskRunId, number])
  @@index([taskRunId])
}

enum TaskRunAttemptStatus {
  /// NON-FINAL
  PENDING
  EXECUTING
  PAUSED
  /// FINAL
  FAILED
  CANCELED
  COMPLETED
}

/// This is the unified otel span/log model that will eventually be replaced by clickhouse
model TaskEvent {
  id String @id @default(cuid())

  /// This matches the span name for a trace event, or the log body for a log event
  message String

  traceId    String
  spanId     String
  parentId   String?
  tracestate String?

  isError     Boolean @default(false)
  isPartial   Boolean @default(false)
  isCancelled Boolean @default(false)
  /// deprecated: don't use this, moving this to properties, this now uses TaskEventKind.LOG
  isDebug     Boolean @default(false)

  serviceName      String
  serviceNamespace String

  level  TaskEventLevel  @default(TRACE)
  kind   TaskEventKind   @default(INTERNAL)
  status TaskEventStatus @default(UNSET)

  links  Json?
  events Json?

  /// This is the time the event started in nanoseconds since the epoch
  startTime BigInt

  /// This is the duration of the event in nanoseconds
  duration BigInt @default(0)

  attemptId     String?
  attemptNumber Int?

  environmentId   String
  environmentType RuntimeEnvironmentType

  organizationId String

  projectId  String
  projectRef String

  runId     String
  runIsTest Boolean @default(false)

  idempotencyKey String?

  taskSlug       String
  taskPath       String?
  taskExportName String?

  workerId      String?
  workerVersion String?

  queueId   String?
  queueName String?

  batchId String?

  /// This represents all the span attributes available, like http.status_code, and special attributes like $style.icon, $output, $metadata.payload.userId, as it's used for searching and filtering
  properties Json

  /// This represents all span attributes in the $metadata namespace, like $metadata.payload
  metadata Json?

  /// This represents all span attributes in the $style namespace, like $style
  style Json?

  /// This represents all span attributes in the $output namespace, like $output
  output Json?

  /// This represents the mimetype of the output, such as application/json or application/super+json
  outputType String?

  payload     Json?
  payloadType String?

  createdAt DateTime @default(now())

  // This represents the amount of "usage time" the event took, e.g. the CPU time
  usageDurationMs  Int   @default(0)
  usageCostInCents Float @default(0)

  machinePreset           String?
  machinePresetCpu        Float?
  machinePresetMemory     Float?
  machinePresetCentsPerMs Float?

  /// Used on the run page
  @@index([traceId])
  /// Used when looking up span events to complete when a run completes
  @@index([spanId])
  // Used for getting all logs for a run
  @@index([runId])
}

enum TaskEventLevel {
  TRACE
  DEBUG
  LOG
  INFO
  WARN
  ERROR
}

enum TaskEventKind {
  UNSPECIFIED
  INTERNAL
  SERVER
  CLIENT
  PRODUCER
  CONSUMER
  UNRECOGNIZED
  LOG
}

enum TaskEventStatus {
  UNSET
  OK
  ERROR
  UNRECOGNIZED
}

model TaskQueue {
  id String @id @default(cuid())

  friendlyId String @unique

  name String
  type TaskQueueType @default(VIRTUAL)

  version       TaskQueueVersion @default(V1)
  orderableName String?

  project   Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  projectId String

  runtimeEnvironment   RuntimeEnvironment @relation(fields: [runtimeEnvironmentId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  runtimeEnvironmentId String

  /// Represents the current concurrency limit for the queue
  concurrencyLimit             Int?
  /// When the concurrency limit was overridden
  concurrencyLimitOverriddenAt DateTime?
  /// Who overrode the concurrency limit (will be null if overridden via the API)
  concurrencyLimitOverriddenBy String?
  /// If concurrencyLimit is overridden, this is the overridden value
  concurrencyLimitBase         Int?
  rateLimit                    Json?

  paused Boolean @default(false)

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  attempts TaskRunAttempt[]
  tasks    BackgroundWorkerTask[]
  workers  BackgroundWorker[]

  @@unique([runtimeEnvironmentId, name])
}

enum TaskQueueType {
  VIRTUAL
  NAMED
}

enum TaskQueueVersion {
  V1
  V2
}

model BatchTaskRun {
  id                      String             @id @default(cuid())
  friendlyId              String             @unique
  idempotencyKey          String?
  idempotencyKeyExpiresAt DateTime?
  status                  BatchTaskRunStatus @default(PENDING)
  runtimeEnvironmentId    String
  /// This only includes new runs, not idempotent runs.
  runs                    TaskRun[]
  createdAt               DateTime           @default(now())
  updatedAt               DateTime           @updatedAt

  // new columns
  /// Friendly IDs
  runIds       String[] @default([])
  runCount     Int      @default(0)
  payload      String?
  payloadType  String   @default("application/json")
  options      Json?
  batchVersion String   @default("v1")

  //engine v2
  /// Specific run blockers,
  runsBlocked TaskRunWaitpoint[]
  /// Waitpoints that are blocked by this batch.
  /// When a Batch is created it blocks execution of the associated parent run (for andWait)
  waitpoints  Waitpoint[]

  // This is for v3 batches
  /// sealed is set to true once no more items can be added to the batch
  sealed         Boolean   @default(false)
  sealedAt       DateTime?
  /// this is the expected number of items in the batch
  expectedCount  Int       @default(0)
  /// this is the completed number of items in the batch. once this reaches expectedCount, and the batch is sealed, the batch is considered completed
  completedCount Int       @default(0)
  completedAt    DateTime?
  resumedAt      DateTime?

  /// this is used to be able to "seal" this BatchTaskRun when all of the runs have been triggered asynchronously, and using the "parallel" processing strategy
  processingJobsCount         Int @default(0)
  processingJobsExpectedCount Int @default(0)

  /// optional token that can be used to authenticate the task run
  oneTimeUseToken String?

  // Run Engine v2 batch queue fields
  /// When processing started (status changed to PROCESSING)
  processingStartedAt   DateTime?
  /// When processing completed (all items processed)
  processingCompletedAt DateTime?
  /// Count of successfully created runs
  successfulRunCount    Int?
  /// Count of failed run creations
  failedRunCount        Int?
  /// Detailed failure records
  errors                BatchTaskRunError[]

  ///all the below properties are engine v1 only
  items                  BatchTaskRunItem[]
  taskIdentifier         String?
  checkpointEvent        CheckpointRestoreEvent? @relation(fields: [checkpointEventId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  checkpointEventId      String?                 @unique
  dependentTaskAttempt   TaskRunAttempt?         @relation(fields: [dependentTaskAttemptId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  dependentTaskAttemptId String?
  runDependencies        TaskRunDependency[]     @relation("dependentBatchRun")

  @@unique([oneTimeUseToken])
  ///this is used for all engine versions
  @@unique([runtimeEnvironmentId, idempotencyKey])
  @@index([dependentTaskAttemptId])
  // This is for the batch list dashboard page
  @@index([runtimeEnvironmentId, id(sort: Desc)])
}

enum BatchTaskRunStatus {
  PENDING
  PROCESSING
  COMPLETED
  PARTIAL_FAILED
  ABORTED
}

///Used in engine V1 only
model BatchTaskRunItem {
  id String @id @default(cuid())

  status BatchTaskRunItemStatus @default(PENDING)

  batchTaskRun   BatchTaskRun @relation(fields: [batchTaskRunId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  batchTaskRunId String

  taskRun   TaskRun @relation(fields: [taskRunId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  taskRunId String

  taskRunAttempt   TaskRunAttempt? @relation(fields: [taskRunAttemptId], references: [id], onDelete: SetNull, onUpdate: Cascade)
  taskRunAttemptId String?

  createdAt   DateTime  @default(now())
  updatedAt   DateTime  @updatedAt
  completedAt DateTime?

  @@unique([batchTaskRunId, taskRunId])
  @@index([taskRunAttemptId], map: "idx_batchtaskrunitem_taskrunattempt")
  @@index([taskRunId], map: "idx_batchtaskrunitem_taskrun")
}

enum BatchTaskRunItemStatus {
  PENDING
  FAILED
  CANCELED
  COMPLETED
}

/// Track individual run creation failures in batch processing (Run Engine v2)
model BatchTaskRunError {
  id             String       @id @default(cuid())
  batchTaskRun   BatchTaskRun @relation(fields: [batchTaskRunId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  batchTaskRunId String

  /// Which item in the batch (0-based index)
  index          Int
  /// The task identifier that was being triggered
  taskIdentifier String
  /// The payload that failed (JSON, may be truncated)
  payload        String?
  /// The options that were used
  options        Json?
  /// Error message
  error          String
  /// Error code if available
  errorCode      String?

  createdAt DateTime @default(now())

  @@unique([batchTaskRunId, index])
  @@index([batchTaskRunId])
}

model EnvironmentVariable {
  id         String                     @id @default(cuid())
  friendlyId String                     @unique
  key        String
  project    Project                    @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  projectId  String
  createdAt  DateTime                   @default(now())
  updatedAt  DateTime                   @updatedAt
  values     EnvironmentVariableValue[]

  @@unique([projectId, key])
}

model EnvironmentVariableValue {
  id               String              @id @default(cuid())
  valueReference   SecretReference?    @relation(fields: [valueReferenceId], references: [id], onDelete: SetNull, onUpdate: Cascade)
  valueReferenceId String?
  variable         EnvironmentVariable @relation(fields: [variableId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  variableId       String
  environment      RuntimeEnvironment  @relation(fields: [environmentId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  environmentId    String

  /// If true, the value is secret and cannot be revealed
  isSecret Boolean @default(false)

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  version       Int   @default(1)
  lastUpdatedBy Json?

  @@unique([variableId, environmentId])
  @@index([environmentId])
}

model Checkpoint {
  id String @id @default(cuid())

  friendlyId String @unique

  type     CheckpointType
  location String
  imageRef String
  reason   String?
  metadata String?

  events CheckpointRestoreEvent[]

  run   TaskRun @relation(fields: [runId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  runId String

  attempt       TaskRunAttempt @relation(fields: [attemptId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  attemptId     String
  attemptNumber Int?

  project   Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  projectId String

  runtimeEnvironment   RuntimeEnvironment @relation(fields: [runtimeEnvironmentId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  runtimeEnvironmentId String

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  @@index([attemptId])
  @@index([runId])
}

enum CheckpointType {
  DOCKER
  KUBERNETES
}

model CheckpointRestoreEvent {
  id String @id @default(cuid())

  type     CheckpointRestoreEventType
  reason   String?
  metadata String?

  checkpoint   Checkpoint @relation(fields: [checkpointId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  checkpointId String

  run   TaskRun @relation(fields: [runId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  runId String

  attempt   TaskRunAttempt @relation(fields: [attemptId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  attemptId String

  project   Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  projectId String

  runtimeEnvironment   RuntimeEnvironment @relation(fields: [runtimeEnvironmentId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  runtimeEnvironmentId String

  taskRunDependency      TaskRunDependency?
  batchTaskRunDependency BatchTaskRun?

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  @@index([checkpointId])
  @@index([runId])
}

enum CheckpointRestoreEventType {
  CHECKPOINT
  RESTORE
}

enum WorkerDeploymentType {
  MANAGED
  UNMANAGED
  V1
}

model WorkerDeployment {
  id String @id @default(cuid())

  contentHash String
  friendlyId  String @unique
  shortCode   String
  version     String

  runtime        String?
  runtimeVersion String?

  imageReference String?
  imagePlatform  String  @default("linux/amd64")

  externalBuildData   Json?
  buildServerMetadata Json?

  status WorkerDeploymentStatus @default(PENDING)
  type   WorkerDeploymentType   @default(V1)

  project   Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  projectId String

  environment   RuntimeEnvironment @relation(fields: [environmentId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  environmentId String

  worker   BackgroundWorker? @relation(fields: [workerId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  workerId String?           @unique

  triggeredBy   User?   @relation(fields: [triggeredById], references: [id], onDelete: SetNull, onUpdate: Cascade)
  triggeredById String?
  triggeredVia  String?
  commitSHA     String?

  startedAt   DateTime?
  installedAt DateTime?
  builtAt     DateTime?
  deployedAt  DateTime?

  failedAt  DateTime?
  errorData Json?

  canceledAt     DateTime?
  canceledReason String?

  // This is GitMeta type
  git Json?

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  promotions             WorkerDeploymentPromotion[]
  alerts                 ProjectAlert[]
  workerInstance         WorkerInstance[]
  integrationDeployments IntegrationDeployment[]

  @@unique([projectId, shortCode])
  @@unique([environmentId, version])
  @@index([commitSHA])
}

enum WorkerDeploymentStatus {
  PENDING
  INSTALLING
  /// This is the status when the image is being built
  BUILDING
  /// This is the status when the image is built and we are waiting for the indexing to finish
  DEPLOYING
  /// This is the status when the image is built and indexed, meaning we have everything we need to deploy
  DEPLOYED
  FAILED
  CANCELED
  /// This is the status when the image is built and indexing does not finish in time
  TIMED_OUT
}

model WorkerDeploymentPromotion {
  id String @id @default(cuid())

  /// This is the promotion label, e.g. "current"
  label String

  deployment   WorkerDeployment @relation(fields: [deploymentId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  deploymentId String

  environment   RuntimeEnvironment @relation(fields: [environmentId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  environmentId String

  // Only one promotion per environment can be active at a time
  @@unique([environmentId, label])
}

///Schedules can be attached to tasks to trigger them on a schedule
model TaskSchedule {
  id String @id @default(cuid())

  type ScheduleType @default(IMPERATIVE)

  ///users see this as `id`. They start with schedule_
  friendlyId     String @unique
  ///a reference to a task (not a foreign key because it's across versions)
  taskIdentifier String

  ///can be provided and we won't create another with the same key
  deduplicationKey             String  @default(cuid())
  userProvidedDeduplicationKey Boolean @default(false)

  ///the CRON pattern
  generatorExpression  String
  generatorDescription String                @default("")
  generatorType        ScheduleGeneratorType @default(CRON)

  /// These are IANA format string, or the default "UTC". E.g. "America/New_York"
  timezone String @default("UTC")

  ///Can be provided by the user then accessed inside a run
  externalId String?

  ///Instances of the schedule that are active
  instances TaskScheduleInstance[]

  /// @deprecated stop writing 2026-04-30; reads moved out of code (UI now derives from cron's previous slot). Drop in follow-up.
  lastRunTriggeredAt DateTime?

  project   Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  projectId String

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  active Boolean @default(true)

  @@unique([projectId, deduplicationKey])
  /// Dashboard list view
  @@index([projectId])
  @@index([projectId, createdAt(sort: Desc)])
}

enum ScheduleType {
  /// defined on your task using the `cron` property
  DECLARATIVE
  /// explicit calls to the SDK are used to create, or using the dashboard
  IMPERATIVE
}

enum ScheduleGeneratorType {
  CRON
}

///An instance links a schedule with an environment
model TaskScheduleInstance {
  id String @id @default(cuid())

  taskSchedule   TaskSchedule @relation(fields: [taskScheduleId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  taskScheduleId String

  environment   RuntimeEnvironment @relation(fields: [environmentId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  environmentId String

  project   Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  projectId String

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  active Boolean @default(true)

  /// @deprecated stop writing 2026-04-30; engine derives from cron + exactScheduleTime. Drop in follow-up.
  lastScheduledTimestamp DateTime?
  /// @deprecated stop writing 2026-04-30; engine derives from cron + now(). Drop in follow-up.
  nextScheduledTimestamp DateTime?

  //you can only have a schedule attached to each environment once
  @@unique([taskScheduleId, environmentId])
  @@index([environmentId])
  @@index([projectId, active])
}

model RuntimeEnvironmentSession {
  id String @id @default(cuid())

  ipAddress String

  environment   RuntimeEnvironment @relation(fields: [environmentId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  environmentId String

  createdAt      DateTime  @default(now())
  updatedAt      DateTime  @updatedAt
  disconnectedAt DateTime?

  currentEnvironments RuntimeEnvironment[] @relation("currentSession")
}

model ProjectAlertChannel {
  id String @id @default(cuid())

  friendlyId String @unique

  ///can be provided and we won't create another with the same key
  deduplicationKey             String  @default(cuid())
  userProvidedDeduplicationKey Boolean @default(false)

  integration   OrganizationIntegration? @relation(fields: [integrationId], references: [id], onDelete: SetNull, onUpdate: Cascade)
  integrationId String?

  enabled Boolean @default(true)

  type             ProjectAlertChannelType
  name             String
  properties       Json
  alertTypes       ProjectAlertType[]
  environmentTypes RuntimeEnvironmentType[] @default([STAGING, PRODUCTION])

  errorAlertConfig Json?

  project   Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  projectId String

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  alerts        ProjectAlert[]
  alertStorages ProjectAlertStorage[]

  @@unique([projectId, deduplicationKey])
}

enum ProjectAlertChannelType {
  EMAIL
  SLACK
  WEBHOOK
}

model ProjectAlert {
  id         String @id @default(cuid())
  friendlyId String @unique

  project   Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  projectId String

  environment   RuntimeEnvironment @relation(fields: [environmentId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  environmentId String

  channel   ProjectAlertChannel @relation(fields: [channelId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  channelId String

  status ProjectAlertStatus @default(PENDING)

  type ProjectAlertType

  // Run-ops split: these reference run-subgraph rows that live on the dedicated run-ops DB for run-ops id
  // runs, so the cross-DB FK can't hold. Scalar-only; the run is resolved via runStore.findRun.
  taskRunAttemptId String?

  taskRunId String?

  workerDeployment   WorkerDeployment? @relation(fields: [workerDeploymentId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  workerDeploymentId String?

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

enum ProjectAlertType {
  TASK_RUN
  /// deprecated, we don't send new alerts for this type
  TASK_RUN_ATTEMPT
  DEPLOYMENT_FAILURE
  DEPLOYMENT_SUCCESS
  ERROR_GROUP
}

enum ProjectAlertStatus {
  PENDING
  SENT
  FAILED
}

model ProjectAlertStorage {
  id String @id @default(cuid())

  project   Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  projectId String

  alertChannel   ProjectAlertChannel @relation(fields: [alertChannelId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  alertChannelId String

  alertType ProjectAlertType

  storageId   String
  storageData Json

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  @@index([alertChannelId, alertType, storageId])
}

model OrganizationIntegration {
  id String @id @default(cuid())

  friendlyId String @unique

  service                IntegrationService
  externalOrganizationId String? /// Identifier for external, integration's organization (e.g. Vercel's team)

  integrationData Json

  tokenReference   SecretReference @relation(fields: [tokenReferenceId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  tokenReferenceId String

  organization   Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  organizationId String

  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt
  deletedAt DateTime?

  alertChannels                  ProjectAlertChannel[]
  organizationProjectIntegration OrganizationProjectIntegration[]

  @@index([externalOrganizationId])
}

model OrganizationProjectIntegration {
  id String @id @default(cuid())

  organizationIntegration   OrganizationIntegration @relation(fields: [organizationIntegrationId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  organizationIntegrationId String

  project   Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  projectId String

  externalEntityId String /// Identifier for webhooks, for example Vercel's projectId
  integrationData  Json /// Save useful data like config or external entity name
  installedBy      String? /// UserId who installed the integration

  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt
  deletedAt DateTime?

  @@index([projectId])
  @@index([projectId, organizationIntegrationId])
  @@index([externalEntityId])
}

enum IntegrationService {
  SLACK
  VERCEL
}

/// Bulk actions, like canceling and replaying runs
model BulkActionGroup {
  id String @id @default(cuid())

  friendlyId String @unique

  project   Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  projectId String

  /// If this is set then it's a V2 Bulk Action that supports queries
  environment   RuntimeEnvironment? @relation(fields: [environmentId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  environmentId String?

  type BulkActionType

  /// When the group is created it's pending. After we've processed all the items it's completed. This does not mean the associated runs are completed.
  status BulkActionStatus @default(PENDING)

  /// The query that will be executed to get the runs to process (if null, we are passing run ids directly)
  queryName    String?
  /// The params that will be passed to the query
  params       Json?
  /// Billing-limit dedupe key (grace hitAt or resolve job key). Null for dashboard bulk actions.
  dedupeKey    String?
  /// The cursor that will be passed to the query (null for the first page)
  cursor       Json?
  /// The number of runs that have been processed successfully
  successCount Int     @default(0)
  /// The number of runs that have been failed
  failureCount Int     @default(0)
  /// The total number of runs that will be processed
  totalCount   Int     @default(0)

  completionNotification BulkActionNotificationType @default(NONE)

  /// The userId who did the bulk action
  user   User?   @relation(fields: [userId], references: [id], onDelete: SetNull, onUpdate: Cascade)
  userId String?

  /// The name of the bulk action
  name String?

  /// The time the bulk action was completed
  completedAt DateTime?

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  @@index([environmentId, createdAt(sort: Desc)])
  // NOTE: the real index is PARTIAL (`WHERE "dedupeKey" IS NOT NULL`) and created
  // CONCURRENTLY — see migration 20260626120001_bulk_action_group_dedupe_key_index.
  // Prisma can't express partial indexes, so this @@index is an approximation (as with
  // the INCLUDE/fillfactor indexes elsewhere in this schema). `migrate dev` will report
  // drift here; do NOT accept its drop/recreate — keep the hand-written migration.
  @@index([environmentId, type, dedupeKey])
  // Backs the per-environment concurrent-replay limit (count of PENDING REPLAY groups).
  // Plain composite (not partial) so Prisma's parameterized count reliably uses it; the
  // migration creates it CONCURRENTLY to avoid locking writes. See migration
  // 20260702120000_bulk_action_group_pending_replay_index.
  @@index([environmentId, status, type])
}

enum BulkActionType {
  /// Cancels existing runs. This populates the destination runs.
  CANCEL
  /// Replays existing runs. The original runs go as source runs, and the new runs go as destination runs.
  REPLAY
}

enum BulkActionStatus {
  PENDING
  COMPLETED
  ABORTED
}

enum BulkActionNotificationType {
  NONE
  EMAIL
}

model BulkActionItem {
  id String @id @default(cuid())

  /// @deprecated not used in new BulkActions
  friendlyId String?

  groupId String

  type BulkActionType

  /// When the item is created it's pending. After we've processed the item it's completed. This does not mean the associated runs are completed.
  status BulkActionItemStatus @default(PENDING)

  /// The run that is the source of the action, e.g. when replaying this is the original run
  sourceRun   TaskRun @relation("SourceActionItemRun", fields: [sourceRunId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  sourceRunId String

  /// The run that's a result of the action, this will be set when the run has been created
  destinationRun   TaskRun? @relation("DestinationActionItemRun", fields: [destinationRunId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  destinationRunId String?

  error String?

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

enum BulkActionItemStatus {
  PENDING
  COMPLETED
  FAILED
}

model RealtimeStreamChunk {
  id String @id @default(cuid())

  key   String
  value String

  sequence Int

  runId String

  createdAt DateTime @default(now())

  @@index([runId])
  @@index([createdAt])
}

/// This is the unified otel span/log model that will eventually be replaced by clickhouse
model TaskEventPartitioned {
  id      String @default(cuid())
  /// This matches the span name for a trace event, or the log body for a log event
  message String

  traceId    String
  spanId     String
  parentId   String?
  tracestate String?

  isError     Boolean @default(false)
  isPartial   Boolean @default(false)
  isCancelled Boolean @default(false)

  serviceName      String
  serviceNamespace String

  level  TaskEventLevel  @default(TRACE)
  kind   TaskEventKind   @default(INTERNAL)
  status TaskEventStatus @default(UNSET)

  links  Json?
  events Json?

  /// This is the time the event started in nanoseconds since the epoch
  startTime BigInt

  /// This is the duration of the event in nanoseconds
  duration BigInt @default(0)

  attemptId     String?
  attemptNumber Int?

  environmentId   String
  environmentType RuntimeEnvironmentType

  organizationId String

  projectId  String
  projectRef String

  runId     String
  runIsTest Boolean @default(false)

  idempotencyKey String?

  taskSlug       String
  taskPath       String?
  taskExportName String?

  workerId      String?
  workerVersion String?

  queueId   String?
  queueName String?

  batchId String?

  /// This represents all the span attributes available, like http.status_code, and special attributes like $style.icon, $output, $metadata.payload.userId, as it's used for searching and filtering
  properties Json

  /// This represents all span attributes in the $metadata namespace, like $metadata.payload
  metadata Json?

  /// This represents all span attributes in the $style namespace, like $style
  style Json?

  /// This represents all span attributes in the $output namespace, like $output
  output Json?

  /// This represents the mimetype of the output, such as application/json or application/super+json
  outputType String?

  payload     Json?
  payloadType String?

  createdAt DateTime @default(now())

  // This represents the amount of "usage time" the event took, e.g. the CPU time
  usageDurationMs  Int   @default(0)
  usageCostInCents Float @default(0)

  machinePreset           String?
  machinePresetCpu        Float?
  machinePresetMemory     Float?
  machinePresetCentsPerMs Float?

  @@id([id, createdAt])
  /// Used on the run page
  @@index([traceId])
  /// Used when looking up span events to complete when a run completes
  @@index([spanId])
  // Used for getting all logs for a run
  @@index([runId])
}

enum GithubRepositorySelection {
  ALL
  SELECTED
}

model GithubAppInstallation {
  id String @id @default(cuid())

  appInstallationId   BigInt                    @unique
  targetId            BigInt
  targetType          String
  accountHandle       String
  permissions         Json?
  repositorySelection GithubRepositorySelection
  installedBy         String?

  organization   Organization @relation(fields: [organizationId], references: [id])
  organizationId String

  repositories GithubRepository[]

  deletedAt   DateTime?
  suspendedAt DateTime?
  createdAt   DateTime  @default(now())
  updatedAt   DateTime  @updatedAt

  @@index([organizationId])
}

model GithubRepository {
  id String @id @default(cuid())

  githubId      BigInt
  name          String
  fullName      String
  htmlUrl       String
  private       Boolean
  defaultBranch String

  installation   GithubAppInstallation @relation(fields: [installationId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  installationId String

  createdAt                 DateTime                    @default(now())
  updatedAt                 DateTime                    @updatedAt
  ConnectedGithubRepository ConnectedGithubRepository[]

  @@unique([installationId, githubId])
  @@index([installationId])
}

model ConnectedGithubRepository {
  id String @id @default(cuid())

  project   Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  projectId String

  repository   GithubRepository @relation(fields: [repositoryId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  repositoryId String

  // Branch configuration by environment slug, stored as JSON
  // Example: {
  //   "prod": { "branch": "main" },
  //   "staging": { "branch": "staging" },
  // }
  // We could alternatively store tracking branch configurations in a separate table and reference the runtime environments properly.
  // We don't need that functionality currently so it's simpler to store it here.
  // Should we need to introduce such functionality in the future, e.g., supporting custom environments, the migration should be straightforward.
  branchTracking Json

  previewDeploymentsEnabled Boolean @default(true)

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  // A project can only be connected to one repository
  @@unique([projectId])
  @@index([repositoryId])
}

enum ImpersonationAuditLogAction {
  START
  STOP
}

model ImpersonationAuditLog {
  id String @id @default(cuid())

  action ImpersonationAuditLogAction

  /// The admin user who initiated/ended the impersonation
  admin   User   @relation("ImpersonationAdmin", fields: [adminId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  adminId String

  /// The user being impersonated
  target   User   @relation("ImpersonationTarget", fields: [targetId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  targetId String

  ipAddress String?

  createdAt DateTime @default(now())

  @@index([adminId])
  @@index([targetId])
  @@index([createdAt])
}

enum CustomerQuerySource {
  DASHBOARD
  API
}

enum CustomerQueryScope {
  ORGANIZATION
  PROJECT
  ENVIRONMENT
}

model CustomerQuery {
  id String @id @default(cuid())

  /// The TSQL query text that was executed
  query String

  /// The scope of the query (determines which tenant IDs were used)
  scope CustomerQueryScope

  /// Query execution statistics from ClickHouse
  stats Json

  /// AI-generated title summarizing the query
  title String?

  /// Where the query originated from
  source CustomerQuerySource @default(DASHBOARD)

  organization   Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  organizationId String

  project   Project? @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  projectId String?

  environment   RuntimeEnvironment? @relation(fields: [environmentId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  environmentId String?

  /// Optional user who executed the query (null for API calls)
  user   User?   @relation(fields: [userId], references: [id], onDelete: SetNull, onUpdate: Cascade)
  userId String?

  /// Time filter settings used when the query was executed
  /// Period like "7d", "24h", etc.
  filterPeriod String?
  /// Custom start date (ISO 8601)
  filterFrom   DateTime?
  /// Custom end date (ISO 8601)
  filterTo     DateTime?

  createdAt DateTime @default(now())

  /// Fast lookup for history menu (most recent 20 per org)
  @@index([organizationId, createdAt(sort: Desc)])
  /// For Stripe metering job - find unprocessed queries
  @@index([createdAt])
}

model IntegrationDeployment {
  id String @id @default(cuid())

  integrationName         String /// For example Vercel
  integrationDeploymentId String /// External ID
  commitSHA               String
  deploymentId            String?
  status                  String? /// External deployment status

  workerDeployment WorkerDeployment? @relation(fields: [deploymentId], references: [id], onDelete: SetNull, onUpdate: Cascade)

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  @@index([commitSHA])
  @@index([deploymentId])
}

/// A user-defined metrics dashboard
model MetricsDashboard {
  id String @id @default(cuid())

  friendlyId String @unique

  title       String
  description String @default("")

  organization   Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  organizationId String

  project   Project? @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  projectId String?

  /// Who created the dashboard
  owner   User?   @relation(fields: [ownerId], references: [id], onDelete: SetNull, onUpdate: Cascade)
  ownerId String?

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  /// JSON that defines the config, queries, layout and config of all widgets. 
  /// There will be a version field for the format.
  layout String

  /// Fast lookup for the list
  @@index([projectId, createdAt(sort: Desc)])
}

// ====================================================
// LLM Pricing Models
// ====================================================

/// A known LLM model or model pattern for cost tracking
model LlmModel {
  id           String    @id @default(cuid())
  friendlyId   String    @unique @map("friendly_id")
  projectId    String?   @map("project_id")
  project      Project?  @relation(fields: [projectId], references: [id], onDelete: Cascade)
  modelName    String    @map("model_name")
  matchPattern String    @map("match_pattern")
  startDate    DateTime? @map("start_date")
  source       String    @default("default") // "default", "admin", "project"
  createdAt    DateTime  @default(now()) @map("created_at")
  updatedAt    DateTime  @updatedAt @map("updated_at")

  // Catalog metadata for model registry
  provider        String?  @map("provider")
  description     String?  @map("description")
  contextWindow   Int?     @map("context_window")
  maxOutputTokens Int?     @map("max_output_tokens")
  capabilities    String[] @default([]) @map("capabilities")
  isHidden        Boolean  @default(false) @map("is_hidden")
  baseModelName   String?  @map("base_model_name")
  // "tokens", "images", "characters", "minutes", "requests", "free", "not_findable"; null until researched
  pricingUnit     String?  @map("pricing_unit")

  pricingTiers LlmPricingTier[]
  prices       LlmPrice[]

  @@unique([projectId, modelName, startDate])
  @@index([projectId])
  @@map("llm_models")
}

/// A pricing tier for a model (supports volume-based or conditional pricing)
model LlmPricingTier {
  id         String   @id @default(cuid())
  modelId    String   @map("model_id")
  model      LlmModel @relation(fields: [modelId], references: [id], onDelete: Cascade)
  name       String
  isDefault  Boolean  @default(true) @map("is_default")
  priority   Int      @default(0)
  conditions Json     @default("[]") @db.JsonB

  prices LlmPrice[]

  @@unique([modelId, priority])
  @@unique([modelId, name])
  @@map("llm_pricing_tiers")
}

/// A price point for a usage type within a pricing tier
model LlmPrice {
  id            String         @id @default(cuid())
  modelId       String         @map("model_id")
  model         LlmModel       @relation(fields: [modelId], references: [id], onDelete: Cascade)
  pricingTierId String         @map("pricing_tier_id")
  pricingTier   LlmPricingTier @relation(fields: [pricingTierId], references: [id], onDelete: Cascade)
  usageType     String         @map("usage_type")
  price         Decimal        @db.Decimal(20, 12)

  @@unique([modelId, usageType, pricingTierId])
  @@map("llm_prices")
}

enum PlatformNotificationSurface {
  WEBAPP
  CLI
}

enum PlatformNotificationScope {
  USER
  PROJECT
  ORGANIZATION
  GLOBAL
}

/// Admin-created notification definitions
model PlatformNotification {
  id String @id @default(cuid())

  friendlyId String @unique @default(cuid())

  /// Admin-facing title for identification in admin tools
  title String

  /// Versioned JSON rendering content (see payload schema in spec)
  payload Json

  surface PlatformNotificationSurface
  scope   PlatformNotificationScope

  /// Set when scope = USER
  user   User?   @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  userId String?

  /// Set when scope = ORGANIZATION
  organization   Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  organizationId String?

  /// Set when scope = PROJECT
  project   Project? @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  projectId String?

  /// When notification becomes active
  startsAt DateTime

  /// When notification expires
  endsAt DateTime

  /// CLI: auto-expire N days after user first saw it
  cliMaxDaysAfterFirstSeen Int?

  /// CLI: auto-expire after shown N times to user
  cliMaxShowCount Int?

  /// CLI: only show every N-th request (e.g. cliShowEvery=3 means show on 3rd, 6th, 9th…)
  cliShowEvery Int?

  /// Ordering within same scope level (higher = more important)
  priority Int @default(0)

  /// Soft delete
  archivedAt DateTime?

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  interactions PlatformNotificationInteraction[]

  @@index([surface, scope, startsAt])
}

/// Per-user tracking of notification views and dismissals
model PlatformNotificationInteraction {
  id String @id @default(cuid())

  notification   PlatformNotification @relation(fields: [notificationId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  notificationId String

  user   User   @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  userId String

  /// Set by beacon/CLI GET on first impression
  firstSeenAt DateTime @default(now())

  /// Times shown (incremented per beacon/CLI view)
  showCount Int @default(1)

  /// User dismissed in webapp
  webappDismissedAt DateTime?

  /// User clicked a link in webapp
  webappClickedAt DateTime?

  /// Auto-dismissed or expired in CLI
  cliDismissedAt DateTime?

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  @@unique([notificationId, userId])
}

enum ErrorGroupStatus {
  UNRESOLVED
  RESOLVED
  IGNORED
}

/**
 * Error group state is used to track when a user has interacted with an error (ignored/resolved)
 * The actual error data is in ClickHouse.
 */
model ErrorGroupState {
  id String @id @default(cuid())

  organization   Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  organizationId String

  project   Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  projectId String

  /**
   * You can ignore/resolve an error across all environments, or specific ones
   */
  environment   RuntimeEnvironment? @relation(fields: [environmentId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  environmentId String?

  taskIdentifier   String
  errorFingerprint String

  status ErrorGroupStatus @default(UNRESOLVED)

  /**
   * Error is ignored until this date
   */
  ignoredUntil                 DateTime?
  /**
   * Error is ignored until this occurrence rate
   */
  ignoredUntilOccurrenceRate   Int?
  /**
   * Error is ignored until this total occurrences
   */
  ignoredUntilTotalOccurrences Int?

  /// Total occurrence count at the time the error was ignored (from ClickHouse).
  /// Used with ignoredUntilTotalOccurrences to compute occurrences since ignoring.
  ignoredAtOccurrenceCount BigInt?

  /**
   * Error was ignored at this date
   */
  ignoredAt       DateTime?
  /**
   * Reason for ignoring the error
   */
  ignoredReason   String?
  /**
   * User who ignored the error
   */
  ignoredByUserId String?

  /**
   * Error was resolved at this date
   */
  resolvedAt        DateTime?
  /**
   * Error was resolved in this version
   */
  resolvedInVersion String?
  /**
   * User who resolved the error
   */
  resolvedBy        String?

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  @@unique([environmentId, taskIdentifier, errorFingerprint])
  @@index([environmentId, status])
}

model TaskIdentifier {
  id String @id @default(cuid())

  runtimeEnvironment   RuntimeEnvironment @relation(fields: [runtimeEnvironmentId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  runtimeEnvironmentId String

  project   Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  projectId String

  slug String

  currentTriggerSource TaskTriggerSource @default(STANDARD)

  currentWorker   BackgroundWorker? @relation(fields: [currentWorkerId], references: [id], onDelete: SetNull, onUpdate: Cascade)
  currentWorkerId String?

  firstSeenAt          DateTime @default(now())
  lastSeenAt           DateTime @default(now())
  isInLatestDeployment Boolean  @default(true)

  @@unique([runtimeEnvironmentId, slug])
  @@index([runtimeEnvironmentId, isInLatestDeployment])
}

enum DataStoreKind {
  CLICKHOUSE
}

/// Defines org-scoped data store overrides (e.g. dedicated ClickHouse for HIPAA orgs).
/// Multiple organizations can share a single data store row via organizationIds.
model OrganizationDataStore {
  id              String        @id @default(cuid())
  /// Human-readable unique key (e.g. "hipaa-clickhouse-us-east")
  key             String        @unique
  /// Organization IDs that use this data store
  organizationIds String[]
  kind            DataStoreKind
  /// Versioned config JSON. Structure is discriminated by the top-level `version` field.
  config          Json
  createdAt       DateTime      @default(now())
  updatedAt       DateTime      @updatedAt

  @@index([kind])
}
