Files
DKP d04467018e feat(webapp,database): save platform notifications as drafts and publish later (#4743)
## Summary

The platform notifications admin page can now save a notification as a
draft without committing to a schedule, then publish it later by
entering start and end dates. Drafts stay hidden from the webapp panel,
the CLI, and the "What's new" changelog until they are published.

## Design

A draft is an `isDraft` flag on `PlatformNotification`, not nullable
dates, so the existing index and every read query stay intact. All three
reader queries filter on the flag, so a draft can never surface
regardless of its placeholder dates. Publishing writes the real start
and end dates and clears the flag; the publish dialog validates the
range and shows inline errors. Editing a draft keeps it a draft, with
the schedule fields hidden until publish.

Also folds in a small tweak: the "Send preview to me" test button now
appears when editing a notification, not just when creating one.
2026-08-20 22:16:32 +01:00

3370 lines
102 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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", "driverAdapters"]
}
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[]
createdApiKeys ApiKey[]
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[]
@@index([userId])
}
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[]
apiKeys ApiKey[]
revokedApiKeys RevokedApiKey[]
// A partial unique index also enforces one STAGING/PREVIEW root per project and type.
// It is defined in SQL because Prisma does not support partial indexes in the schema.
@@unique([projectId, slug, orgMemberId])
@@unique([projectId, shortcode])
@@index([parentEnvironmentId])
@@index([projectId])
@@index([organizationId])
}
model ApiKey {
id String @id @default(cuid())
name String
keyHash String @unique @map("key_hash")
lastFour String @map("last_four")
runtimeEnvironment RuntimeEnvironment @relation(fields: [runtimeEnvironmentId], references: [id], onDelete: Cascade, onUpdate: Cascade)
runtimeEnvironmentId String @map("runtime_environment_id")
createdBy User? @relation(fields: [createdByUserId], references: [id], onDelete: SetNull, onUpdate: Cascade)
createdByUserId String? @map("created_by_user_id")
presetId String? @map("preset_id")
scopes String[]
lastUsedAt DateTime? @map("last_used_at")
revokedAt DateTime? @map("revoked_at")
expiresAt DateTime? @map("expires_at")
updatedAt DateTime @updatedAt @map("updated_at")
createdAt DateTime @default(now()) @map("created_at")
@@index([runtimeEnvironmentId, revokedAt, createdAt(sort: Desc)])
@@map("api_keys")
}
/// 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?
/// Set the first time the CLI `init` command completes against this project. Drives the dev onboarding progress.
initializedAt DateTime?
/// Runtime used when a deployment config does not specify one. Null preserves the legacy Node 20 fallback.
defaultRuntime String?
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([projectId, slug, createdAt])
@@index([runtimeEnvironmentId, projectId])
@@index([runtimeEnvironmentId, slug, triggerSource])
}
enum TaskTriggerSource {
STANDARD
SCHEDULED
AGENT
WEBHOOK
}
model WebhookEndpoint {
id String @id @default(cuid())
friendlyId String @unique // wh_... (WebhookEndpointId IdUtil "wh")
/// URL key. CSPRNG, NOT a friendlyId: crypto.randomBytes(16) base64url.
/// Generated by the create path (deploy-sync / P2 API), NOT by @default.
opaqueId String @unique // global unique (Q2): ingress resolves the endpoint by opaqueId alone
// scope (plain columns, NO @relation -> no FK, matching TaskEventPartitioned)
organizationId String
projectId String
runtimeEnvironmentId String
environmentType RuntimeEnvironmentType // denormalized from the env at deploy-sync (immutable per endpoint), Q3
// tenant scope. "" sentinel on declared (P1); P2 dynamic create fills these.
// Sentinel not null so the 4-column @@unique actually bites.
endpointTenantId String @default("")
endpointExternalRef String @default("")
source String // provider tag e.g. "stripe","slack","github"
handlerWebhookId String // declared webhook() id (string ref, GOLDEN LAW, no relation)
routingTarget Json // RoutingTarget tagged union ({ type: "task" } | { type: "session" })
verifierArtifact Json // VerifierArtifact tagged union (config|preset in P1)
filter String? // source filter DSL string (display/round-trip)
filterAst Json? // compiled FilterAst, evaluated at ingest; null = route all
filterAstVersion Int? // re-parse `filter` on a format bump
metadata Json @default("{}") // arbitrary user metadata; flows into the webhook task
// who supplies the secret/key; drives the Connect UI (paste vs generate). From the source.
secretProvisioning String @default("either") // "provider" | "integrator" | "either"
/// SecretReference.key string. Plain String, NO @relation -> no FK to SecretReference.
signingSecretKey String?
status WebhookEndpointStatus @default(ACTIVE)
/// When an operator disabled the endpoint via the dashboard/API. Null means the declarative sync
/// owns the status: a redeploy that re-declares a previously-removed (auto-deactivated) webhook
/// reactivates it. Non-null means the operator disabled it, so the sync leaves the status alone.
manuallyDeactivatedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([runtimeEnvironmentId, handlerWebhookId, endpointTenantId, endpointExternalRef]) // deploy-sync key
@@index([runtimeEnvironmentId, source])
@@index([projectId, createdAt(sort: Desc)])
}
enum WebhookEndpointStatus {
ACTIVE
INACTIVE
DELETING
}
model WebhookDelivery {
id String @default(cuid())
friendlyId String // whd_... (WebhookDeliveryId IdUtil "whd"). Uniqueness scoped by the composite PK.
// plain columns, NO @relation -> no FK
webhookEndpointId String
organizationId String
projectId String
runtimeEnvironmentId String
environmentType RuntimeEnvironmentType
externalDeliveryId String // provider id (Stripe event id, GitHub X-GitHub-Delivery)
idempotencyKey String // = externalDeliveryId; passed to Run Engine when routing to a task
runId String? // the triggered run, if task target
status WebhookDeliveryStatus @default(PENDING)
/// Set from the x-trigger-test ingress header; marks console/test-send deliveries so the list can filter them.
isTest Boolean @default(false)
parsedEvent Json? // size-capped snapshot of the verified event (full event lives in ClickHouse)
headers Json? // inbound request headers, surfaced to the webhook task via onEvent({ headers })
rawBodyHash String? // sha256 of raw bytes; cheap P2 replay anchor
errorMessage String?
filterReason String? // why a FILTERED delivery was not routed (failing clause + actual value)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt // Q14: real column; the status transitions update it, maps straight to CH updated_at
processedAt DateTime?
@@id([id, createdAt]) // composite PK: the partition key MUST be in every PK/unique on a partitioned table
@@index([webhookEndpointId, createdAt(sort: Desc)]) // dashboard recent-deliveries; also partition-pruned
}
enum WebhookDeliveryStatus {
PENDING
PROCESSING
SUCCEEDED
FAILED
FILTERED
}
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?
/// If the override was expressed as a percentage of the environment limit, this stores that
/// percentage (the source of truth). The absolute concurrencyLimit is materialized from it.
/// Decimal(5,2) allows fractional percentages like 12.50% (0.01100.00).
concurrencyLimitOverridePercent Decimal? @db.Decimal(5, 2)
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)])
@@index([runtimeEnvironmentId, createdAt(sort: Desc), 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])
@@index([valueReferenceId])
}
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?
externalId 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])
@@index([environmentId, createdAt])
@@index([environmentId, status, id])
@@index([environmentId, externalId])
}
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")
// Cron spread
windowDurationSeconds Int?
windowPercentage Int?
///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
// Durable cron spread phase
schedulePhase Int?
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
@@index([channelId])
}
enum ProjectAlertType {
TASK_RUN
/// deprecated, we don't send new alerts for this type
TASK_RUN_ATTEMPT
DEPLOYMENT_FAILURE
DEPLOYMENT_SUCCESS
ERROR_GROUP
DASHBOARD_AGENT_WATCH
}
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)
/// Staged-but-unpublished. While true the notification is hidden from all
/// user-facing reads regardless of startsAt/endsAt; publishing sets real
/// dates and flips this to false.
isDraft Boolean @default(false)
/// 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])
}
model LogsSearchProjectorCheckpoint {
id BigInt @id @default(autoincrement())
projectorId String
mode String
windowStart DateTime
windowEnd DateTime
queryId String?
completedAt DateTime @default(now())
@@unique([projectorId, mode, windowStart, windowEnd])
@@index([projectorId, mode, windowEnd(sort: Desc)])
}
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])
}