type SlackUserIdentity { slackTeamId: String! slackUserId: String! } type User { id: ID! """The full name e.g. Grace Hopper.""" fullName: String! """A short name for use in UI e.g. Grace.""" publicName: String! """The avatar URL of the user.""" avatarUrl: String """The uploaded avatar for the user.""" avatar: WorkspaceFile """The email associated with this user. Email is unique per user.""" email: String! """Retrieve roles for a specific workspace + user.""" roles: [Role!]! """The role of the user in the workspace.""" role: Role """Connected slack users to this Plain account.""" slackIdentities: [SlackUserIdentity!]! """The labels associated with this user.""" labels: [Label!]! """The default saved threads view for this user.""" defaultSavedThreadsView: SavedThreadsView """The user's current availability status.""" status: UserStatus! """When the user's status was last changed.""" statusChangedAt: DateTime! """The user's configured working hours for automatic status switching.""" workingHours: UserWorkingHours createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! """ Whether the user has been deleted from the workspace. Deleted users are still returned by queries so that historical references remain intact. """ isDeleted: Boolean! deletedAt: DateTime deletedBy: Actor } """ A Plain user's core account record, representing their identity independent of any workspace membership. """ type UserAccount { id: ID! """The full name e.g. Grace Hopper.""" fullName: String! """A short name for use in UI e.g. Grace.""" publicName: String! """The email associated with this user. Email is unique per user.""" email: String! } enum UserStatus { """The user is currently active and available.""" ONLINE """The user is not available.""" OFFLINE BREAK @deprecated(reason: "Unused in new work hours tracking") """ The user is temporarily away (used in workspaces configured to show AWAY instead of OFFLINE during out-of-hours periods). """ AWAY } type Workspace { id: ID! """ The internal name of the workspace, used within Plain and not shown to customers. """ name: String! """ The public-facing display name of the workspace shown to customers and in external communications. """ publicName: String! """ Whether this workspace is a demo workspace created for evaluation purposes. """ isDemoWorkspace: Boolean! domainName: String @deprecated(reason: "Use domainNames instead") """ The list of email domain names associated with this workspace, used to automatically associate customers with the workspace. """ domainNames: [String!]! createdBy: InternalActor! createdAt: DateTime! updatedBy: InternalActor! updatedAt: DateTime! workspaceEmailSettings: WorkspaceEmailSettings! workspaceChatSettings: WorkspaceChatSettings! logo: WorkspaceFile """ The WorkOS organization ID used for SSO and directory sync, if configured. """ workOSOrganizationId: String """ The WorkOS Hub organization ID, if this workspace is part of a hub organization. """ workOSHubOrganizationId: String } type WorkspaceInvite { id: ID! """Who sent this invite.""" createdBy: InternalActor! """When the invite was created.""" createdAt: DateTime! """The email that is being invited.""" email: String! """The workspace they are being invited to.""" workspace: Workspace! """Whether the person has accepted the invite.""" isAccepted: Boolean! """ The roles that will be assigned to the user when they accept the invite. Deprecated in favour of the role field. """ roles: [Role!]! """Who updated this invite.""" updatedBy: InternalActor! """When the invite was updated.""" updatedAt: DateTime! """Whether the user would be assigned a billing rota seat upon joining.""" usingBillingRotaSeat: Boolean! """ The built-in role that will be assigned to the user when they accept the invite. Prefer this field over roles. """ role: Role """ The custom role that the invite will assign on workspace joining, if specified. """ customRole: CustomRole } type Role { id: ID! name: String! description: String """The list of permission strings granted to users who hold this role.""" permissions: [String!]! isAssignableToCustomer: Boolean! @deprecated(reason: "Use isAssignableToThread instead") """Whether this role can be used when assigning a user to a thread.""" isAssignableToThread: Boolean! """Whether this role can be used when assigning a user to a task.""" isAssignableToTask: Boolean! assignableBillingSeats: [BillingSeatType!]! @deprecated(reason: "Don't use. Will be removed soon.") requiresBillableSeat: Boolean! @deprecated(reason: "Don't use. Will be removed soon.") """ The stable enum key for built-in roles (e.g. OWNER, SUPPORT). Null for custom roles. """ key: RoleKey """If this role is a custom role, this field contains the custom role ID.""" customRoleId: ID customRole: CustomRole } type RoleScopeDefinition { resource: RoleScopeResourceType! scopes: [RoleScope!]! } type RoleScope { """ The dimension on which thread visibility is filtered (e.g. label, tier, channel). """ primitiveType: ThreadScopePrimitiveType! """ Whether users see all threads, only threads matching specific values, or all threads except those matching specific values. """ accessMode: RoleScopeAccessMode! """IDs of the values included/excluded (empty for VIEW_ANY).""" values: [ID!]! } enum LabelTypeType { """A workspace-wide label type available across all teams.""" DEFAULT """A label type that is scoped to a specific team.""" TEAM } type LabelType { id: ID! name: String! icon: String color: String """ Whether this is a workspace-wide label type (`DEFAULT`) or one scoped to a specific team (`TEAM`). """ type: LabelTypeType! description: String """ The parent label type, if this label type is nested under another. Null for top-level label types. """ parentLabelType: LabelType """The position of the label type. Always relative to its parent.""" position: String! """ An optional identifier you can set to correlate this label type with a record in your own system. Unique within a workspace. Set via `createLabelType` or `updateLabelType`. """ externalId: String """ Whether this label type has been archived. Archived label types remain on threads that already carry them but cannot be applied to new threads. """ isArchived: Boolean! """ The user or machine user who archived this label type. Null if the label type is not archived. """ archivedBy: InternalActor """ When this label type was archived. Null if the label type is not archived. """ archivedAt: DateTime """ When true, Plain's AI features will not suggest or automatically apply this label type. """ isExcludedFromAi: Boolean! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } """ A label applied to a thread or user. Each label is an instance of a LabelType and records who applied it and when. """ type Label { id: ID! labelType: LabelType! createdAt: DateTime! createdBy: Actor! updatedAt: DateTime! updatedBy: Actor! } """An object modelling an email address and if it's been verified.""" type EmailAddress { """The email address.""" email: String! """ If the email address ownership has been verified (e.g. via sending an email with a code). If the email is not verified, Plain may not email this address. """ isVerified: Boolean! """When the email became verified in Plain.""" verifiedAt: DateTime } type Company { id: ID! name: String! """ URL of the company's logo image. Pass an optional `size` argument (pixels) to request a resized version. Null if no logo is set. """ logoUrl(size: Int): String """ The primary domain name associated with this company (e.g. `acme.com`). Plain uses this to automatically assign customers whose email addresses match the domain. """ domainName: String! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! """ The support tier assigned to this company, which determines its SLA and prioritisation rules. Null if no tier is assigned. """ tier: Tier """ The Slack or other channel associations that are automatically applied to new threads belonging to customers of this company. """ threadChannelAssociations: [ThreadChannelAssociation!]! """ The annual contract value for this company in cents (e.g. 10000 = $100.00). Null if not set. Used for prioritisation and reporting. """ contractValue: Int """ The Plain user responsible for managing this company's account. Null if no account owner is assigned. """ accountOwner: User """ Whether this company has been deleted. Deleted companies are still returned by queries when you filter by `isDeleted: true`, but are no longer linked to customers. """ isDeleted: Boolean! deletedAt: DateTime deletedBy: InternalActor } type CompanyEdge { cursor: String! node: Company! } type CompanyConnection { edges: [CompanyEdge!]! pageInfo: PageInfo! } type TenantEdge { cursor: String! node: Tenant! } type TenantConnection { edges: [TenantEdge!]! pageInfo: PageInfo! } """ Deprecated: customer-level status has been replaced by per-thread status. Use `Thread.status` and `ThreadStatus` instead. """ enum CustomerStatus { IDLE @deprecated(reason: "Use ThreadStatus.DONE instead") ACTIVE @deprecated(reason: "Use ThreadStatus.TODO instead") SNOOZED @deprecated(reason: "Use ThreadStatus.SNOOZED instead") } type EmailCustomerIdentity { email: String! } type DiscordCustomerIdentity { discordUserId: String! } type SlackCustomerIdentity { slackUserId: String! } union CustomerIdentity = EmailCustomerIdentity | DiscordCustomerIdentity | SlackCustomerIdentity """ The core customer entity. A customer only exists (ideally) once. Uniqueness is guaranteed on both of these fields: 1. `externalId` if provided 2. `email` """ type Customer { """Uniquely identifies a customer in Plain.""" id: ID! """Your system's ID for this customer.""" externalId: ID """The full name of the customer.""" fullName: String! """An optional short name of the customer, typically their first name.""" shortName: String """The customer's email address.""" email: EmailAddress! """The avatar URL of the customer.""" avatarUrl: String """The user the customer is assigned to.""" assignedToUser: UserActor """When the customer was assigned to a user.""" assignedAt: DateTime """A subquery to fetch the customer's group memberships.""" customerGroupMemberships(filters: CustomerGroupMembershipsFilter, first: Int, after: String, last: Int, before: String): CustomerGroupMembershipConnection! """A subquery to fetch the customer's tenants.""" tenantMemberships(first: Int, after: String, last: Int, before: String): CustomerTenantMembershipConnection! """The company the customer belongs to.""" company: Company """ True when the customer was auto-created without a known identity (e.g. from an anonymous chat session). Anonymous customers may have limited profile data. """ isAnonymous: Boolean! createdAt: DateTime! createdBy: Actor! updatedAt: DateTime! updatedBy: Actor! """ When the customer was marked as spam. Null if the customer has not been marked as spam. """ markedAsSpamAt: DateTime """The internal user or machine user who marked this customer as spam.""" markedAsSpamBy: InternalActor """ All known channel identities for this customer (email, Discord, Slack, etc.). """ identities: [CustomerIdentity!]! status: CustomerStatus @deprecated(reason: "Use Thread.status instead") statusChangedAt: DateTime @deprecated(reason: "Use Thread.statusChangedAt instead") lastIdleAt: DateTime @deprecated(reason: "Use Thread.statusChangedAt instead") } type CustomerGroup { id: ID! name: String! """ A unique, human-readable identifier for the group (e.g. 'enterprise') used when referencing the group in API calls and integrations. """ key: String! color: String! """ An optional identifier from your own system, used to reference this group when calling `upsertCustomerGroup` without knowing the Plain-assigned ID. """ externalId: String createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } type CustomerGroupMembership { """The ID of the customer who is a member of the group.""" customerId: ID! customerGroup: CustomerGroup! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } type CustomerGroupEdge { cursor: String! node: CustomerGroup! } type CustomerGroupConnection { edges: [CustomerGroupEdge!]! pageInfo: PageInfo! } type CustomerGroupMembershipEdge { cursor: String! node: CustomerGroupMembership! } type CustomerGroupMembershipConnection { edges: [CustomerGroupMembershipEdge!]! pageInfo: PageInfo! } """ Represents a customer's membership in a tenant. Returned on the `tenantMemberships` sub-query of a `Customer`. """ type CustomerTenantMembership { tenant: Tenant! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } type CustomerTenantMembershipEdge { cursor: String! node: CustomerTenantMembership! } type CustomerTenantMembershipConnection { edges: [CustomerTenantMembershipEdge!]! pageInfo: PageInfo! } """ The data type of a thread field schema, which determines which value field is used when reading or writing thread field values. """ enum ThreadFieldSchemaType { """Stores a free-text value as a string.""" STRING """Stores a boolean value.""" BOOL """ Stores one value chosen from a predefined set of allowed string values. """ ENUM """Stores a numeric value as a float.""" NUMBER """Stores a monetary amount as a float.""" CURRENCY """Stores a date/time value as a DateTime scalar.""" DATE } type DependsOnThreadFieldType { threadFieldSchemaId: ID! threadFieldSchemaValue: String! } type DependsOnLabelType { labelTypeId: ID! } """ Defines the shape and behaviour of a custom field that can be attached to threads. """ type ThreadFieldSchema { id: ID! label: String! """ A stable, URL-safe identifier for the field used when reading and writing values. Set at creation and immutable thereafter. """ key: String! description: String! """ Controls the position of this field relative to other thread field schemas in the UI. """ order: Int! type: ThreadFieldSchemaType! """Valid options for ENUM-typed fields. Empty for all other field types.""" enumValues: [String!]! defaultStringValue: String defaultBooleanValue: Boolean defaultNumberValue: Float defaultDateValue: DateTime """ When true, a thread cannot be marked as done until this field has a value. """ isRequired: Boolean! """ When true, Plain's AI will attempt to automatically fill this field based on the thread's content. Only supported for ENUM and BOOL field types. """ isAiAutoFillEnabled: Boolean! """ When true, the field value can only be set via the API and is not editable by agents in the Plain UI. """ isClientReadonly: Boolean! """ If set, this field is only shown when the referenced sibling field has a specific value. """ dependsOnThreadField: DependsOnThreadFieldType """ If non-empty, this field is only shown when the thread has at least one of the listed label types. """ dependsOnLabels: [DependsOnLabelType!]! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } """A stored value for a custom thread field on a specific thread.""" type ThreadField { id: ID! threadId: ID! """The stable key of the corresponding thread field schema.""" key: String! type: ThreadFieldSchemaType! """ True when this field value was set automatically by Plain's AI rather than by a human or API call. """ isAiGenerated: Boolean! stringValue: String booleanValue: Boolean numberValue: Float dateValue: DateTime createdAt: DateTime! createdBy: Actor! updatedAt: DateTime! updatedBy: Actor! } union ThreadDiscussionChannelDetails = ThreadDiscussionSlackChannelDetails | ThreadDiscussionEmailChannelDetails | ThreadDiscussionCursorWorkspaceBackgroundAgentChannelDetails | ThreadDiscussionAgentSessionChannelDetails type ThreadDiscussionSlackChannelDetails { slackTeamId: ID! slackChannelId: ID! slackChannelName: String! slackMessageLink: String } type ThreadDiscussionEmailChannelDetails { emailRecipients: [String!]! } type ThreadDiscussionCursorWorkspaceBackgroundAgentChannelDetails { cursorWorkspaceIntegrationId: ID! repositoryUrl: String } type ThreadDiscussionAgentSessionChannelDetails { agentSessionId: ID! } enum ThreadDiscussionVisibility { """Visible to all members of the workspace.""" WORKSPACE """Visible only to the user who created the discussion.""" USER } enum ThreadDiscussionStatus { """ The agent is actively running (only applies to AGENT_SESSION discussions). """ IN_PROGRESS """The discussion is open but no agent is currently running.""" IDLE """The discussion has been marked as resolved.""" RESOLVED APPROVAL_REQUESTED } type ThreadDiscussion { id: ID! """ The ID of the Plain thread this discussion is attached to. Null for global Sidekick agent sessions that were not started from a thread. """ threadId: ID """ The support thread when threadId is set. Null when the discussion has no thread (e.g. some global Sidekick sessions). """ thread: Thread title: String! messages(first: Int, after: String, last: Int, before: String): ThreadDiscussionMessageConnection! """ High-level status of the discussion. RESOLVED when resolvedAt is set. For agent session discussions, IN_PROGRESS while the agent is running; APPROVAL_REQUESTED when the agent has finished working but is blocked on an unresolved approval request; otherwise IDLE. Non-agent-session discussions are IDLE until resolved. """ status: ThreadDiscussionStatus! resolvedAt: DateTime """ Timestamp of the most recent message in the discussion. Null when no messages have been posted yet. """ lastActivityAt: DateTime """ True when the discussion has an unread agent response (set when a Sidekick agent session settles into IDLE, cleared when the session is opened / marked read). """ isUnread: Boolean! createdAt: DateTime! createdBy: Actor! updatedAt: DateTime! updatedBy: Actor! """ Channel-specific details for the discussion (Slack channel info, email recipients, or agent session ID). The concrete type depends on the discussion type. """ channelDetails: ThreadDiscussionChannelDetails """ Who can see this discussion. WORKSPACE means visible to all workspace members, USER means visible only to the creator. Null when not yet set. """ visibility: ThreadDiscussionVisibility """ When the discussion was started from an entity page (company, tenant, etc.). Null when source is thread-only, a plain link, or absent. """ sourceEntityId: ID """ The type of entity for sourceEntityId (e.g. COMPANY, TENANT). Null when sourceEntityId is not set. """ sourceEntityType: DiscussionSourceEntityType """ The page path where this discussion was started (e.g. '/workspace/ws_xxx/insights'). Null when not set (older rows, or thread/entity-only context). """ sourcePageLink: String type: ThreadDiscussionType! @deprecated(reason: "Use channelDetails instead") slackTeamId: String @deprecated(reason: "Use channelDetails.slackTeamId instead") slackChannelId: String @deprecated(reason: "Use channelDetails.slackChannelId instead") slackChannelName: String @deprecated(reason: "Use channelDetails.slackChannelName instead") slackMessageLink: String @deprecated(reason: "Use channelDetails.slackMessageLink instead") emailRecipients: [String!]! @deprecated(reason: "Use channelDetails.emailRecipients instead") """ User-authored messages that the user has submitted but the agent has not picked up yet because it was mid-turn. Empty for non-AGENT_SESSION discussions. Frontend can render these as greyed-out preview bubbles pinned to the bottom of the chat; they disappear when the agent picks them up (a real ThreadDiscussionMessage is created at that point with a correctly-ordered timestamp) or when the queue is cleared by error / timeout / manual stop. """ queuedAgentSessionMessages: [QueuedAgentSessionMessage!]! } """ A user message that has been submitted to a Sidekick AGENT_SESSION discussion while the agent was mid-turn. Lives in the queue until the agent picks it up. Once picked up, it's promoted to a regular ThreadDiscussionMessage and removed from the queue. """ type QueuedAgentSessionMessage { id: ID! text: String! workspaceFiles: [WorkspaceFile!]! createdAt: DateTime! createdBy: Actor! updatedAt: DateTime! updatedBy: Actor! } input DeleteQueuedAgentSessionMessageInput { queuedAgentSessionMessageId: ID! } type DeleteQueuedAgentSessionMessageOutput { error: MutationError } input EditQueuedAgentSessionMessageInput { queuedAgentSessionMessageId: ID! """ New markdown body for the queued message. Attachments are preserved as-is. """ markdownContent: String! } type EditQueuedAgentSessionMessageOutput { queuedAgentSessionMessage: QueuedAgentSessionMessage error: MutationError } type ThreadDiscussionEdge { node: ThreadDiscussion! cursor: String! } type ThreadDiscussionConnection { edges: [ThreadDiscussionEdge!]! pageInfo: PageInfo! } input DiscussionsFilter { createdByUserIds: [ID!] discussionTypes: [DiscussionType!] threadIds: [ID!] """ Only return entity-scoped discussions whose source entity id is one of the provided values. """ sourceEntityIds: [ID!] """ Only return entity-scoped discussions whose source entity type is one of the provided values. """ sourceEntityTypes: [DiscussionSourceEntityType!] """ Only return page-scoped discussions whose source page link is one of the provided values. """ sourcePageLinks: [String!] """ Only return discussions whose last activity is at or after this timestamp. ISO 8601 format (e.g. 2024-10-28T18:30:00Z). """ lastActivityAtAfter: String """ Only return discussions whose last activity is strictly before this timestamp. ISO 8601 format (e.g. 2024-10-28T18:30:00Z). """ lastActivityAtBefore: String """Only return discussions whose status is one of the provided values.""" statuses: [ThreadDiscussionStatus!] """ When true, only return discussions that are unread (have an unread agent response). When false, only return read discussions. Can be combined with statuses. """ isUnread: Boolean """ Match discussions that satisfy all of the nested filters. Combined with the other fields on this filter using AND. """ and: [DiscussionsFilter!] """ Match discussions that satisfy any of the nested filters. Combined with the other fields on this filter using AND. """ or: [DiscussionsFilter!] """Match discussions that do not satisfy the nested filter.""" not: DiscussionsFilter } enum DiscussionsSortField { CREATED_AT LAST_ACTIVITY_AT UPDATED_AT } input DiscussionsSort { field: DiscussionsSortField! direction: SortDirection! } type ThreadDiscussionMessageConnection { edges: [ThreadDiscussionMessageEdge!]! pageInfo: PageInfo! } type ThreadDiscussionMessageEdge { node: ThreadDiscussionMessage! cursor: String! } type ThreadDiscussionMessageReaction { name: String! actors: [Actor!]! imageUrl: String } type ThreadDiscussionMessage { id: ID! """The ID of the discussion this message belongs to.""" threadDiscussionId: ID! type: ThreadDiscussionMessageType! text: String! """A direct link to the message in Slack. Null for non-Slack discussions.""" slackMessageLink: String """ Customer-scoped attachments. Populated for EMAIL / SLACK / CURSOR messages. """ attachments: [Attachment!]! """Workspace-scoped files. Populated for Sidekick AGENT_SESSION messages.""" workspaceFiles: [WorkspaceFile!]! """ True when the originating Slack message had files that were not imported. Pair with slackMessageLink to render a view-in-Slack affordance. """ hasUnprocessedAttachments: Boolean! """ Timestamp of the last edit to the original Slack message, if it was edited after being synced. Null for non-Slack messages. """ lastEditedOnSlackAt: DateTime """ Timestamp when the original Slack message was deleted. Null if not deleted or for non-Slack messages. """ deletedOnSlackAt: DateTime reactions: [ThreadDiscussionMessageReaction!]! """ Discriminator string for the entry union. Use this to determine which concrete type to expect in the entry field. """ entryType: String! entry: ThreadDiscussionEntryPayload createdAt: DateTime! createdBy: Actor! updatedAt: DateTime! updatedBy: Actor! } union ThreadDiscussionEntryPayload = ThreadDiscussionMessageEntryPayload | ThreadDiscussionToolCallEntryPayload | ThreadDiscussionApprovalRequestEntryPayload type ThreadDiscussionMessageEntryPayload { text: String! type: ThreadDiscussionMessageType! attachments: [Attachment!]! isFinal: Boolean! """ Slack Block Kit blocks (https://api.slack.com/block-kit) sent with this message, if any. JSON-encoded array of blocks. Null when the message was sent as plain markdown. The Plain UI renders a placeholder + a link to the Slack message instead of rendering the blocks natively. """ slackBlocks: String } type ThreadDiscussionToolCallEntryPayload { service: String! op: String! description: String! args: String isSuccess: Boolean! error: String durationMs: Int! } enum AgentApprovalStatus { PENDING APPROVED DENIED } """ Subset of AgentApprovalStatus valid as a resolution input (excludes PENDING). """ enum AgentApprovalDecision { APPROVED DENIED } type ThreadDiscussionApprovalRequestEntryPayload { approvalId: ID! leaseId: ID! justification: String! status: AgentApprovalStatus! reviewerNote: String resolvedAt: DateTime resolvedBy: InternalActor """ JSON-encoded array of { service, op, maxInvocations, usedCount, args }. """ calls: String! } type WorkflowRule { id: ID! name: String! """JSON-encoded payload of the rule definition.""" payload: String! """ Timestamp when the rule was last published. Null means the rule is an inactive draft. """ publishedAt: DateTime """ Display order of this rule relative to other rules. Lower values appear first. """ order: Int! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } type WorkflowRuleEdge { cursor: String! node: WorkflowRule! } type WorkflowRuleConnection { edges: [WorkflowRuleEdge!]! pageInfo: PageInfo! } type Workflow { id: ID! name: String! """JSON-encoded trigger configuration.""" trigger: String! """The ID of the first step to execute, or null if workflow has no steps.""" startStepId: ID """The steps in this workflow.""" steps: [WorkflowStep!]! """ Timestamp when the workflow was last published. Null means the workflow is an inactive draft. """ publishedAt: DateTime """ Display order of this workflow relative to other workflows. Lower values appear first. """ order: Int! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } type WorkflowEdge { cursor: String! node: Workflow! } type WorkflowConnection { edges: [WorkflowEdge!]! pageInfo: PageInfo! } """Controls how a workflow is triggered.""" enum WorkflowTriggerType { """ The workflow runs only when explicitly triggered via the `triggerWorkflow` mutation or a UI button. """ MANUAL """ The workflow runs automatically in response to domain events (e.g. thread created, label added). """ EVENTS } input WorkflowsFilter { triggerTypes: [WorkflowTriggerType!] """ Filter by published status. true returns only published workflows (publishedAt set), false returns only unpublished workflows (publishedAt null). """ isPublished: Boolean } """The role a step plays in a workflow's execution graph.""" enum WorkflowStepType { """ Evaluates a condition; routes execution to one of two branches based on the result. """ CONDITION """ Performs an operation such as assigning a thread, adding a label, or sending a message. """ ACTION """ Pauses execution for a configured duration, then continues to the next step. """ WAIT } type WorkflowStep { id: ID! workflowId: ID! """The type of step: CONDITION or ACTION.""" type: WorkflowStepType! """Optional name for the step.""" name: String """JSON-encoded payload containing the action or condition configuration.""" payload: String! """ Array of next step IDs. For ACTION: 1 element (next step). For CONDITION: 2 elements (true branch, false branch). Null means terminal/no branch. """ transitions: [ID]! """X position of this step on the canvas.""" positionX: Float! """Y position of this step on the canvas.""" positionY: Float! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } """The lifecycle state of a workflow execution.""" enum WorkflowExecutionStatus { """Execution has been accepted and is waiting to be processed.""" QUEUED """Execution is actively running steps.""" RUNNING """All steps completed successfully.""" SUCCESS """Execution stopped due to an error in a step.""" FAILED """ Execution is paused at a WAIT step until the configured duration elapses. """ WAITING """Execution was cancelled before it could complete.""" CANCELLED } enum WorkflowExecutionEntityType { THREAD } type WorkflowExecution { id: ID! workflowId: ID! """ The workflow this execution belongs to. Null if the workflow has been deleted. """ workflow: Workflow """ The event type that triggered this execution (e.g., thread.thread_created). """ triggeredBy: String! executionStatus: WorkflowExecutionStatus! """Human-readable error message when `executionStatus` is FAILED.""" errorMessage: String """When the execution began processing (null while QUEUED).""" startedAt: DateTime """ When the execution reached a terminal state (SUCCESS, FAILED, or CANCELLED). """ completedAt: DateTime """ Total wall-clock time in milliseconds from `startedAt` to `completedAt`. """ executionDurationMs: Int """The type of entity this execution was triggered for.""" entityType: WorkflowExecutionEntityType """The ID of the entity this execution was triggered for.""" entityId: ID """ The actor that manually triggered this execution, if it was manually triggered. """ manuallyTriggeredBy: InternalActor """The step executions for this workflow execution.""" stepExecutions: [WorkflowStepExecution!]! } enum WorkflowStepExecutionStatus { RUNNING SUCCESS FAILED } type WorkflowStepExecution { id: ID! workflowStepId: ID! """ The version of the step definition at the time this execution ran. Increments each time the step is updated. """ workflowStepVersion: Int! status: WorkflowStepExecutionStatus! startedAt: DateTime completedAt: DateTime """JSON-encoded output containing the step execution result.""" output: String } type WorkflowExecutionEdge { cursor: String! node: WorkflowExecution! } type WorkflowExecutionConnection { edges: [WorkflowExecutionEdge!]! pageInfo: PageInfo! } input WorkflowExecutionByEntityFilter { entityType: WorkflowExecutionEntityType! entityId: ID! workflowId: ID executionStatus: WorkflowExecutionStatus } type ChatAppSecret { chatAppId: ID! secret: String! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } """ Metadata confirming that a signing secret exists for a chat app, without exposing the secret value. The raw secret is only available at creation time via `createChatAppSecret`. """ type ChatAppHiddenSecret { chatAppId: ID! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } type ChatApp { id: ID! name: String! """An optional image used to represent this chat app in the Plain UI.""" logo: WorkspaceFile createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } type ChatAppEdge { cursor: String! node: ChatApp! } type ChatAppConnection { edges: [ChatAppEdge!]! pageInfo: PageInfo! } type Note { id: ID! """The plain-text body of the note.""" text: String! """ The Markdown body of the note. When present, this is preferred over `text` in rich-text contexts. """ markdown: String customer: Customer! attachments: [Attachment!]! """ Optional external identifier for this note set by your system at creation time. """ externalId: ID """Whether the note has been edited since it was first created.""" isEdited: Boolean! editedAt: DateTime editedBy: InternalActor """Whether the note has been soft-deleted.""" isDeleted: Boolean! createdAt: DateTime! createdBy: InternalActor! deletedAt: DateTime deletedBy: InternalActor updatedAt: DateTime! updatedBy: InternalActor! } type SavedThreadsViewSort { """ The built-in thread field to sort by, or null when sorting by a custom thread field. """ field: ThreadsSortField direction: SortDirection """ The key of the custom thread field to sort by, used when `field` is null. """ threadFieldKey: String } type SavedThreadsViewFilterThreadFieldNumber { value: Float gte: Float lte: Float } type SavedThreadsViewFilterThreadFieldDate { after: DateTime before: DateTime } type SavedThreadsViewFilterThreadField { """The schema key of the thread field being filtered on.""" key: String! stringValue: String booleanValue: Boolean number: SavedThreadsViewFilterThreadFieldNumber date: SavedThreadsViewFilterThreadFieldDate } type SavedThreadsViewFilterTenantField { """The external ID of the tenant field schema being filtered on.""" externalFieldId: ID! stringValue: String booleanValue: Boolean numberValue: Float stringArrayValue: [String!]! dateValue: DateTime """IDs of users matched when filtering on a user-reference tenant field.""" userReferenceValues: [ID!]! } """ Filters threads by a specific external link source, combining the source system type and the source's identifier. """ type SavedThreadsViewFilterThreadLinkSource { """ The type of the external system that created the thread link (e.g. the integration name). """ sourceType: String! """ The identifier of the specific source object within that external system. """ sourceId: ID! } type ThreadsDisplayOptions { hasStatus: Boolean! hasCustomer: Boolean! hasCompany: Boolean! hasPreviewText: Boolean! hasTier: Boolean! hasCustomerGroups: Boolean! hasLabels: Boolean! hasLinearIssues: Boolean! @deprecated(reason: "Use hasIssueTrackerIssues instead") hasJiraIssues: Boolean! @deprecated(reason: "Use hasIssueTrackerIssues instead") hasLinkedThreads: Boolean! hasServiceLevelAgreements: Boolean! hasChannels: Boolean! hasLastUpdated: Boolean! hasAssignees: Boolean! hasRef: Boolean! hasIssueTrackerIssues: Boolean! hasTasks: Boolean! hasThreadFieldKeys: [String!]! hasTenants: Boolean! hasTenantFieldExternalIds: [ID!]! } enum ThreadsLayout { TABLE BOARD } enum ThreadsGroupBy { NONE PRIORITY STATUS COMPANY LABEL TIER CHANNEL ASSIGNEE CUSTOMER_GROUP TENANT } """ Filter fields for nested and/or/not expressions in SavedThreadsViewFilter. This is separate from SavedThreadsViewFilter because nested filters should only contain filter criteria, not top-level display configuration (sort, displayOptions, groupBy, layout). Unlike ThreadsFilter which is self-referential (pure filtering at every level), SavedThreadsViewFilter has display config that only makes sense at the top level. """ type SavedThreadsViewNestedFilter { statuses: [ThreadStatus!]! statusDetails: [StatusDetailType!]! priorities: [Int!]! assignedToUser: [ID!]! participants: [ID!]! customerGroups: [ID!]! companies: [ID!]! tenants: [ID!]! tiers: [ID!]! labelTypeIds: [ID!]! messageSource: [MessageSource!]! supportEmailAddresses: [String!]! slaTypes: [String!]! slaStatuses: [String!]! threadFields: [SavedThreadsViewFilterThreadField!]! tenantFields: [SavedThreadsViewFilterTenantField!]! threadLinkGroupIds: [ID!]! threadLinkSources: [SavedThreadsViewFilterThreadLinkSource!]! createdAtFilter: DatetimeFilterOutput surveyResponse: SurveyResponseFilterOutput and: [SavedThreadsViewNestedFilter!] or: [SavedThreadsViewNestedFilter!] not: SavedThreadsViewNestedFilter } type SavedThreadsViewFilter { statuses: [ThreadStatus!]! statusDetails: [StatusDetailType!]! priorities: [Int!]! assignedToUser: [ID!]! participants: [ID!]! customerGroups: [ID!]! companies: [ID!]! tenants: [ID!]! tiers: [ID!]! labelTypeIds: [ID!]! messageSource: [MessageSource!]! supportEmailAddresses: [String!]! slaTypes: [String!]! slaStatuses: [String!]! threadFields: [SavedThreadsViewFilterThreadField!]! tenantFields: [SavedThreadsViewFilterTenantField!]! threadLinkGroupIds: [ID!]! threadLinkSources: [SavedThreadsViewFilterThreadLinkSource!]! createdAtFilter: DatetimeFilterOutput surveyResponse: SurveyResponseFilterOutput """The sort order applied when listing threads in this view.""" sort: SavedThreadsViewSort! """ Which columns or attributes are shown in the thread list for this view. """ displayOptions: ThreadsDisplayOptions! """The field by which threads are grouped when this view is displayed.""" groupBy: ThreadsGroupBy! """ The visual layout used to display threads in this view (e.g. list table or kanban board). """ layout: ThreadsLayout! and: [SavedThreadsViewNestedFilter!] or: [SavedThreadsViewNestedFilter!] not: SavedThreadsViewNestedFilter } type SavedThreadsView { id: ID! name: String! """ A slug identifying the icon displayed alongside this view's name (lowercase alphanumeric, underscores, and hyphens). """ icon: String! color: String! """ The complete filter, sort, grouping, and display configuration that defines which threads this view shows and how they are presented. """ threadsFilter: SavedThreadsViewFilter! """ When true, this view is hidden from the navigation sidebar for all workspace members. """ isHidden: Boolean! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } type SavedThreadsViewConnection { pageInfo: PageInfo! edges: [SavedThreadsViewEdge!]! } type SavedThreadsViewEdge { cursor: String! node: SavedThreadsView! } type FavoritePage { id: ID! """ A caller-defined string that identifies which page is favorited (e.g. 'customers' or 'customers/c_1234'). Unique per user within a workspace. """ key: String! createdAt: DateTime! """The user who created this favorite page entry.""" createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } type FavoritePageConnection { pageInfo: PageInfo! edges: [FavoritePageEdge!]! } type FavoritePageEdge { cursor: String! node: FavoritePage! } type Snippet { id: ID! name: String! """ The plain-text body of the snippet, used in channels that do not render markdown. """ text: String! """ Optional markdown body. When present, this is preferred over `text` in rich-text channels. """ markdown: String """ Folder path used to group snippets in the Plain app. Only alphanumeric characters are allowed. Null means the snippet is ungrouped. """ path: String """ Whether the snippet has been soft-deleted. Soft-deleted snippets are hidden from the snippet picker but remain queryable by ID. """ isDeleted: Boolean! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! """ The timestamp at which the snippet was soft-deleted, or null if it has not been deleted. """ deletedAt: DateTime """ The user or machine user that deleted the snippet, or null if it has not been deleted. """ deletedBy: InternalActor } type TeamSettings { id: ID! """ The ID of the label type (of kind TEAM) that these settings belong to. Teams in Plain are modelled as label types. """ labelTypeId: ID! """ Whether round-robin assignment is enabled for this team. When true, incoming threads are automatically assigned to team members in rotation up to the limit set by roundRobinMaxCapacity. """ isRoundRobinEnabled: Boolean! """ The maximum number of open threads a team member can hold before they are skipped in the round-robin rotation. Defaults to 5 when settings are first created. """ roundRobinMaxCapacity: Int! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } """The lifecycle status of a task.""" enum TaskStatus { """The task has not been started yet.""" TODO """The task is actively being worked on.""" IN_PROGRESS """The task has been completed.""" DONE } union TaskAssignee = User | MachineUser | System type Task { id: ID! """ Short human-readable identifier for this task (e.g. `T-123`), as shown in the Plain app. """ ref: String! title: String! """ Optional longer description providing context or instructions for the task. """ description: String status: TaskStatus! """Numeric priority value; lower numbers indicate higher priority.""" priority: Int! """ The company this task is linked to, if any. Mutually exclusive with `tenant`. """ company: Company """ The tenant this task is linked to, if any. Mutually exclusive with `company`. """ tenant: Tenant """ The user or machine user currently assigned to this task, or null if unassigned. """ assignedTo: TaskAssignee """When the task was most recently assigned.""" assignedAt: DateTime """Paginated list of threads linked to this task.""" threadLinks(first: Int, after: String, last: Int, before: String): ThreadLinkConnection! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! """ True if this task has been soft-deleted. Deleted tasks are excluded from the `tasks` list query but remain fetchable by ID. """ isDeleted: Boolean! """When the task was deleted, or null if it has not been deleted.""" deletedAt: DateTime """Who deleted the task, or null if it has not been deleted.""" deletedBy: InternalActor } type TaskConnection { edges: [TaskEdge!]! pageInfo: PageInfo! totalCount: Int! } type TaskEdge { cursor: String! node: Task! } type EmailSignature { """The plain-text version of the email signature.""" text: String! """The markdown-formatted version of the email signature, if provided.""" markdown: String createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } type Chat { id: ID! text: String customerReadAt: DateTime attachments: [Attachment!]! createdAt: DateTime! createdBy: Actor! updatedAt: DateTime! updatedBy: Actor! } type NoteMentionNotificationDetail { threadId: ID! """ The ID of the timeline entry (note) in which the current user was mentioned. """ timelineEntryId: ID! """The actor who authored the note that mentions the current user.""" mentionedBy: InternalActor! } type ThreadAssignmentNotificationDetail { threadId: ID! """ The actor who performed the thread assignment that triggered this notification. """ assignedBy: InternalActor! } type EmailBounceNotificationDetail { threadId: ID! } type DiscussionResolvedNotificationDetail { threadId: ID! """The ID of the discussion that was resolved.""" threadDiscussionId: ID! } union InternalNotificationDetail = NoteMentionNotificationDetail | ThreadAssignmentNotificationDetail | EmailBounceNotificationDetail | DiscussionResolvedNotificationDetail """ An internal notification displayed to workspace members in the Plain app. Each notification is for a specific user. """ type InternalNotification { """The unique ID of this notification.""" id: ID! """The user this notification is for.""" userId: ID! """ The notification type discriminator. Known values include 'thread_assignment', 'note_mention', 'email_bounce', and 'discussion_resolved'. Use `details` for type-specific data. """ type: String! """The title of the notification.""" title: String! """Optional description providing more detail about the notification.""" description: String """ Structured data specific to the notification type. The concrete type is one of NoteMentionNotificationDetail, ThreadAssignmentNotificationDetail, EmailBounceNotificationDetail, or DiscussionResolvedNotificationDetail. Null when no extra context is available. """ details: InternalNotificationDetail """ When the notification was marked as read by the user. Null means unread. """ readAt: DateTime """ When the notification was archived by the user. Null means not archived. """ archivedAt: DateTime """ The actor who archived this notification. Null if the notification has not been archived. """ archivedBy: InternalActor createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } type InternalNotificationEdge { cursor: String! node: InternalNotification! } type InternalNotificationConnection { edges: [InternalNotificationEdge!]! pageInfo: PageInfo! totalCount: Int! } input InternalNotificationsFilter { createdAt: DatetimeFilter isRead: Boolean } type PageInfo { hasPreviousPage: Boolean! hasNextPage: Boolean! startCursor: String endCursor: String } type DateTime { unixTimestamp: String! iso8601: String! } type Time { iso8601: String! } enum SortDirection { ASC DESC } type WorkspaceEdge { cursor: String! node: Workspace! } type WorkspaceConnection { edges: [WorkspaceEdge!]! pageInfo: PageInfo! } type WorkspaceInviteEdge { cursor: String! node: WorkspaceInvite! } type WorkspaceInviteConnection { edges: [WorkspaceInviteEdge!]! pageInfo: PageInfo! } input UsersFilter { isAssignableToCustomer: Boolean @deprecated(reason: "Use isAssignableToThread instead") isAssignableToThread: Boolean """ When set, filters workspace members by task assignability instead of thread assignability. Do not combine with isAssignableToThread or isAssignableToCustomer. """ isAssignableToTask: Boolean } type UserEdge { cursor: String! node: User! } type UserConnection { edges: [UserEdge!]! pageInfo: PageInfo! } type RoleEdge { cursor: String! node: Role! } type RoleConnection { edges: [RoleEdge!]! pageInfo: PageInfo! } input RoleFilter { version: Int } type CustomRole { id: ID! name: String! description: String """ The built-in role whose permissions this custom role inherits (e.g. `role_support`, `role_viewer`). Determines what actions users with this role can perform. """ permissionsPreset: String! """ The thread-visibility scope rules configured for this role. An empty array means no restrictions — users can see all threads. """ scopeDefinitions: [RoleScopeDefinition!]! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } type CustomRoleEdge { cursor: String! node: CustomRole! } type CustomRoleConnection { edges: [CustomRoleEdge!]! pageInfo: PageInfo! } type LabelTypeEdge { cursor: String! node: LabelType! } type LabelTypeConnection { edges: [LabelTypeEdge!]! pageInfo: PageInfo! } input LabelTypeFilter { isArchived: Boolean } type ThreadFieldSchemaEdge { cursor: String! node: ThreadFieldSchema! } type ThreadFieldSchemaConnection { edges: [ThreadFieldSchemaEdge!]! pageInfo: PageInfo! } input CustomersFilter { isMarkedAsSpam: Boolean """ Filters customers to those with at least one of the given customer group IDs. Customers with no groups will not be included. Can be combined with other group filters. """ customerGroupIds: [ID!] """ Filters customers to those with at least one of the given customer group keys. Customers with no groups will not be included. Can be combined with other group filters. """ customerGroupKeys: [String!] """ Filters customers to those belonging to the given companies. Customers who dont belong to any of the given companies will not be included. Can be combined with other company filters. """ companyIdentifiers: [CompanyIdentifierInput!] """ Filters customers to those belonging to the given tenants. Customers who dont belong to any of the given tenants will not be included. Can be combined with other company filters. """ tenantIdentifiers: [TenantIdentifierInput!] """ Filters customers to those who are members of a Slack channel with the given Slack channel ID. """ slackChannelId: ID updatedAt: DatetimeFilter } enum CustomersSortField { FULL_NAME } input CustomersSort { field: CustomersSortField! direction: SortDirection! } type CustomerEdge { cursor: String! node: Customer! } type CustomerConnection { edges: [CustomerEdge!]! pageInfo: PageInfo! totalCount: Int! } type SnippetEdge { cursor: String! node: Snippet! } type SnippetConnection { edges: [SnippetEdge!]! pageInfo: PageInfo! } type UserActor { userId: ID! user: User! } type CustomerActor { customerId: ID! customer: Customer! } type DeletedCustomerActor { customerId: ID! } """A summary of a workflow with only id and name fields.""" type WorkflowSummary { id: ID! name: String! } type SystemActor { systemId: ID! """The workflow execution ID that created this action, if applicable.""" workflowExecutionId: ID """The workflow that created this action, if applicable.""" workflow: WorkflowSummary } type System { id: ID! } type MachineUserActor { machineUserId: ID! machineUser: MachineUser! } type NoteEntry { noteId: ID! text: String! markdown: String attachments: [Attachment!]! isEdited: Boolean! editedAt: DateTime editedBy: InternalActor } type ChatEntry { chatId: ID! text: String customerReadAt: DateTime attachments: [Attachment!]! } interface TimelineEventEntry { timelineEventId: ID! title: String! components: [EventComponent!]! customerId: ID! externalId: ID """ Whether this event should be rendered collapsed by default in the timeline. """ isCollapsed: Boolean! } type ThreadEventEntry implements TimelineEventEntry { timelineEventId: ID! title: String! components: [EventComponent!]! customerId: ID! externalId: ID """ Whether this event should be rendered collapsed by default in the timeline. """ isCollapsed: Boolean! } type CustomerEventEntry implements TimelineEventEntry { timelineEventId: ID! title: String! components: [EventComponent!]! customerId: ID! externalId: ID """ Whether this event should be rendered collapsed by default in the timeline. """ isCollapsed: Boolean! } type SlackReaction { name: String! actors: [Actor!]! imageUrl: String } type SlackMessageEntry { slackMessageLink: String slackWebMessageLink: String! text: String! customerId: ID! relatedThread: SlackMessageEntryRelatedThread attachments: [Attachment!]! lastEditedOnSlackAt: DateTime deletedOnSlackAt: DateTime reactions: [SlackReaction!]! } type SlackMessageEntryRelatedThread { threadId: ID! } type SlackReplyEntry { slackMessageLink: String slackWebMessageLink: String! customerId: ID! text: String! attachments: [Attachment!]! lastEditedOnSlackAt: DateTime deletedOnSlackAt: DateTime reactions: [SlackReaction!]! } type MSTeamsMessage { id: ID! threadId: ID msTeamsTenantId: ID msTeamsConversationId: ID msTeamsMessageId: ID msTeamsTeamId: ID parentMessageId: ID msTeamsMessageLink: String! text: String! markdownContent: String createdAt: DateTime! createdBy: Actor! updatedAt: DateTime! updatedBy: Actor! attachments: [Attachment!]! hasUnprocessedAttachments: Boolean! lastEditedOnMsTeamsAt: DateTime deletedOnMsTeamsAt: DateTime } type MSTeamsMessageEntry { text: String! customerId: ID! markdownContent: String msTeamsMessageId: ID! msTeamsMessageLink: String! attachments: [Attachment!]! hasUnprocessedAttachments: Boolean! lastEditedOnMsTeamsAt: DateTime deletedOnMsTeamsAt: DateTime } type DiscordMessageEntry { customerId: ID! discordMessageId: ID! markdownContent: String attachments: [Attachment!]! lastEditedOnDiscordAt: DateTime deletedOnDiscordAt: DateTime discordMessageLink: String! } type ThreadDiscussionEntry { customerId: ID! threadDiscussionId: ID! discussionType: ThreadDiscussionType! emailRecipients: [String!]! slackChannelName: String slackMessageLink: String } type ThreadDiscussionResolvedEntry { customerId: ID! threadDiscussionId: ID! discussionType: ThreadDiscussionType! emailRecipients: [String!]! slackChannelName: String slackMessageLink: String resolvedAt: DateTime! } type ThreadDiscussionMessageEntry { customerId: ID! threadDiscussionId: ID! threadDiscussionMessageId: ID! discussionType: ThreadDiscussionType! text: String! resolvedText: String! type: ThreadDiscussionMessageType! slackMessageLink: String slackMessageTimestamp: String attachments: [Attachment!]! hasUnprocessedAttachments: Boolean! lastEditedOnSlackAt: DateTime deletedOnSlackAt: DateTime reactions: [ThreadDiscussionMessageReaction!]! } type FileSize { bytes: Int! kiloBytes: Float! megaBytes: Float! } type Attachment { id: ID! fileName: String! """ The size of the attachment exposed in multiple units (bytes, kilobytes, and megabytes). """ fileSize: FileSize! """ The file extension without a leading dot (e.g. `pdf`, `png`). Null if the file has no extension. """ fileExtension: String fileMimeType: String! """ The channel or message type this attachment was uploaded for, which determines size limits and storage handling. """ type: AttachmentType! createdAt: DateTime! createdBy: Actor! updatedAt: DateTime! updatedBy: Actor! } type DiscordMessage { discordMessageId: ID! markdownContent: String attachments: [Attachment!]! lastEditedOnDiscordAt: DateTime deletedOnDiscordAt: DateTime discordMessageLink: String! createdAt: DateTime! createdBy: Actor! updatedAt: DateTime! updatedBy: Actor! } type CustomerEmailActor { customerId: ID! customer: Customer! } type DeletedCustomerEmailActor { customerId: ID! } type UserEmailActor { userId: ID! user: User! } type SupportEmailAddressEmailActor { supportEmailAddress: String! } union EmailActor = CustomerEmailActor | DeletedCustomerEmailActor | UserEmailActor | SupportEmailAddressEmailActor type EmailParticipant { name: String email: String! emailActor: EmailActor } enum EmailAuthenticity { PASS FAIL UNKNOWN } enum EmailBounceReason { """ Recipient was on suppression list at send time. The original SMTP failure that suppressed it is in 'details' when available. """ INACTIVE_RECIPIENT """Recipient is a no-reply address.""" NO_REPLY_RECIPIENT """Received a permanent delivery failure (e.g. mailbox does not exist).""" HARD_BOUNCE """ Receiving server temporarily failed delivery (e.g. greylisting, network issue). """ TRANSIENT """Recipient marked the email as spam.""" SPAM_COMPLAINT """Recipient's mail server flagged the email as spam.""" SPAM_NOTIFICATION """ An auto-responder reply was received from the recipient (e.g. out-of-office). """ AUTO_RESPONDER """Recipient domain failed DNS resolution.""" DNS_ERROR """Recipient's server returned a soft bounce.""" SOFT_BOUNCE """Recipient was undeliverable for an unspecified reason.""" UNDELIVERABLE """Recipient address is malformed or invalid.""" BAD_EMAIL_ADDRESS """Recipient was manually deactivated.""" MANUALLY_DEACTIVATED """Recipient was blocked by provider.""" BLOCKED """Bounce reason is not known.""" UNKNOWN } type EmailBounce { bouncedAt: DateTime! recipient: EmailParticipant! isSendRetriable: Boolean! """ Why the bounce happened. Use this to differentiate suppression-list rejections (INACTIVE_RECIPIENT) from real delivery failures (HARD_BOUNCE, SPAM_COMPLAINT, etc). """ reason: EmailBounceReason! """ Free-form bounce detail string (typically the SMTP response from the receiving server). Null when no detail is available. """ details: String } enum EmailSendStatus { """The email is being sent.""" PENDING """The email was sent successfully to all recipients.""" SENT """ Some (or all) of the recipients bounced the email, meaning they did not recieve it. Check 'bounces' for more details on which recipients bounced. """ BOUNCED """ The email failed to send. This will happen if the main recipient (To) bounced the email, or if there was an unexpected error sending the email. """ FAILED } type EmailEntry { emailId: ID! to: EmailParticipant! from: EmailParticipant! additionalRecipients: [EmailParticipant!]! hiddenRecipients: [EmailParticipant!]! subject: String """The most recent email's text content.""" textContent: String """ Boolean indicating whether there is more text content available that can be resolved via the `fullTextContent` field. """ hasMoreTextContent: Boolean! """The full email's text content, including all replies.""" fullTextContent: String """The most recent email's markdown content.""" markdownContent: String """ Boolean indicating whether there is more markdown content available that can be resolved via the `fullMarkdownContent` field. """ hasMoreMarkdownContent: Boolean! """The full email's markdown content, including all replies.""" fullMarkdownContent: String authenticity: EmailAuthenticity! """ When the email was sent. Only set for outbound emails and will be null until the email is sent. """ sentAt: DateTime """ Informs whether the email was sent successfully, bounced or failed. If the email is still being sent, the status will be 'PENDING'. Only set for outbound emails. """ sendStatus: EmailSendStatus """When the email was received by Plain.""" receivedAt: DateTime """All the attachments included in this email.""" attachments: [Attachment!]! """ Whether this email entry is the start of a new thread in Plain. Can be used to show the full email content. """ isStartOfThread: Boolean! """ If any of the recipients bounces the email, this will contain the list of bounces. """ bounces: [EmailBounce!]! """The category of the email.""" category: EmailCategory! } enum ComponentTextSize { S M L } enum ComponentTextColor { NORMAL MUTED SUCCESS WARNING ERROR } enum ComponentPlainTextSize { S M L } enum ComponentPlainTextColor { NORMAL MUTED SUCCESS WARNING ERROR } enum ComponentBadgeColor { GREY GREEN YELLOW RED BLUE } type ComponentText { textSize: ComponentTextSize textColor: ComponentTextColor text: String! color: ComponentTextColor @deprecated(reason: "Use textColor instead, which has the same type") size: ComponentTextSize @deprecated(reason: "Use textSize instead, which has the same type") } type ComponentPlainText { plainTextSize: ComponentPlainTextSize plainTextColor: ComponentPlainTextColor plainText: String! } enum ComponentSpacerSize { XS S M L XL } type ComponentSpacer { spacerSize: ComponentSpacerSize! size: ComponentSpacerSize! @deprecated(reason: "Use spacerSize instead, which has the same type") } enum ComponentDividerSpacingSize { XS S M L XL } type ComponentDivider { dividerSpacingSize: ComponentDividerSpacingSize spacingSize: ComponentDividerSpacingSize @deprecated(reason: "use dividerSpacingSize instead") } type ComponentLinkButton { linkButtonUrl: String! linkButtonLabel: String! url: String! @deprecated(reason: "use linkButtonUrl instead") label: String! @deprecated(reason: "use linkButtonLabel instead") } type ComponentWorkflowButtonWorkflowIdentifier { workflowId: ID workflowKey: String } type ComponentWorkflowButton { workflowButtonWorkflowIdentifier: ComponentWorkflowButtonWorkflowIdentifier! workflowButtonLabel: String! } type ComponentCopyButton { copyButtonValue: String! copyButtonTooltipLabel: String } type ComponentBadge { badgeLabel: String! badgeColor: ComponentBadgeColor } type ComponentDateTime { dateTimeIso8601: DateTime! } type ComponentUser { user: User userMachineUser: MachineUser } type ComponentRow { rowMainContent: [ComponentRowContent!]! rowAsideContent: [ComponentRowContent!]! } type ComponentContainer { containerContent: [ComponentContainerContent!]! } union ComponentContainerContent = ComponentText | ComponentPlainText | ComponentSpacer | ComponentDivider | ComponentLinkButton | ComponentWorkflowButton | ComponentBadge | ComponentCopyButton | ComponentDateTime | ComponentUser | ComponentRow union ComponentRowContent = ComponentText | ComponentPlainText | ComponentSpacer | ComponentDivider | ComponentLinkButton | ComponentWorkflowButton | ComponentBadge | ComponentCopyButton | ComponentDateTime | ComponentUser union CustomTimelineEntryComponent = ComponentText | ComponentPlainText | ComponentSpacer | ComponentDivider | ComponentLinkButton | ComponentWorkflowButton | ComponentRow | ComponentContainer | ComponentBadge | ComponentCopyButton | ComponentDateTime | ComponentUser union EventComponent = ComponentText | ComponentPlainText | ComponentSpacer | ComponentDivider | ComponentLinkButton | ComponentWorkflowButton | ComponentRow | ComponentContainer | ComponentBadge | ComponentCopyButton | ComponentDateTime | ComponentUser union CustomerCardComponent = ComponentText | ComponentPlainText | ComponentSpacer | ComponentDivider | ComponentLinkButton | ComponentWorkflowButton | ComponentRow | ComponentContainer | ComponentBadge | ComponentCopyButton | ComponentDateTime | ComponentUser type CustomEntry { externalId: ID title: String! type: String components: [CustomTimelineEntryComponent!]! attachments: [Attachment!]! } type ThreadAssignmentTransitionedEntry { previousAssignee: ThreadAssignee nextAssignee: ThreadAssignee } type ThreadAdditionalAssigneesTransitionedEntry { previousAssignees: [ThreadAssignee!]! nextAssignees: [ThreadAssignee!]! } type ThreadStatusTransitionedEntry { previousStatus: ThreadStatus! previousStatusDetail: ThreadStatusDetail nextStatus: ThreadStatus! nextStatusDetail: ThreadStatusDetail } type ThreadPriorityChangedEntry { previousPriority: Int! nextPriority: Int! } type ThreadLabelsChangedEntry { previousLabels: [Label!]! nextLabels: [Label!]! } """ A timeline entry recording a change in the SLA tracking status for a thread. """ type ServiceLevelAgreementStatusTransitionedEntry { """The SLA status before the transition.""" previousStatus: ServiceLevelAgreementStatus! """The SLA status after the transition.""" nextStatus: ServiceLevelAgreementStatus! serviceLevelAgreement: ServiceLevelAgreement } union Actor = UserActor | CustomerActor | DeletedCustomerActor | SystemActor | MachineUserActor union InternalActor = UserActor | SystemActor | MachineUserActor """A union of all possible entries that can appear in a timeline.""" union Entry = NoteEntry | ChatEntry | EmailEntry | CustomEntry | LinearIssueThreadLinkStateTransitionedEntry | ThreadAssignmentTransitionedEntry | ThreadAdditionalAssigneesTransitionedEntry | ThreadStatusTransitionedEntry | ThreadPriorityChangedEntry | ThreadEventEntry | CustomerEventEntry | SlackMessageEntry | SlackReplyEntry | ThreadLabelsChangedEntry | ThreadDiscussionEntry | ThreadDiscussionMessageEntry | ThreadDiscussionResolvedEntry | ServiceLevelAgreementStatusTransitionedEntry | MSTeamsMessageEntry | DiscordMessageEntry | ThreadLinkCreatedEntry | ThreadLinkUpdatedEntry | ThreadLinkDeletedEntry | ThreadLinkTargetCreatedEntry | ThreadLinkTargetDeletedEntry | HelpCenterAiConversationMessageEntry | CustomerSurveyRequestedEntry | MergedThreadMessageEntry type HelpCenterAiConversationMessageEntry { helpCenterId: ID! helpCenterAiConversationId: ID! messageId: ID! markdown: String! } type LinearIssueThreadLinkStateTransitionedEntry { linearIssueId: ID! """ Refers to the id of the WorkflowState object in Linear. This can be used to fetch the WorkflowState from the Linear API. """ previousLinearStateId: ID! """ Refers to the id of the WorkflowState object in Linear. This can be used to fetch the WorkflowState from the Linear API. """ nextLinearStateId: ID! } type ThreadLinkCreatedEntry { threadLink: ThreadLink! } type ThreadLinkUpdatedEntry { threadLink: ThreadLink! previousThreadLink: ThreadLink! } type ThreadLinkDeletedEntry { threadLink: ThreadLink! } type ThreadLinkTargetCreatedEntry { threadLink: ThreadLink! sourceThread: ChildThreadDetails! } type ThreadLinkTargetDeletedEntry { threadLink: ThreadLink! sourceThread: ChildThreadDetails! } type CustomerSurveyRequestedEntry { customerId: ID! threadId: ID! customerSurveyId: ID! surveyResponseId: ID! surveyResponsePublicId: String! } type MergedThreadMessageEntry { threadLinkId: ID! childThreadDetails: ChildThreadDetails! childTimelineEntry: TimelineEntry } type ChildThreadDetails { id: ID! ref: String! title: String } type TimelineEntry { id: ID! customerId: ID! threadId: ID! timestamp: DateTime! """ The payload of this timeline entry. Use an inline fragment on the concrete type (e.g. `NoteEntry`, `EmailEntry`, `CustomerEventEntry`) to access type-specific fields. """ entry: Entry! actor: Actor! """ A plain-text rendering of this timeline entry suitable for use in LLM prompts or search indexing. """ llmText: String } type CustomerEvent { """The ID of the event.""" id: ID! """The customer that this event belongs to.""" customerId: ID! """The title of the event.""" title: String! """The list of components of the event.""" components: [EventComponent!]! """ Whether this event should be rendered collapsed by default in the timeline. """ isCollapsed: Boolean! """The datetime when this event was created.""" createdAt: DateTime! """The actor who created this event.""" createdBy: Actor! """The datetime when this event was last updated.""" updatedAt: DateTime! """The actor who last updated this event.""" updatedBy: Actor! } type ThreadEvent { """The ID of the event.""" id: ID! """The customer that this event belongs to.""" customerId: ID! """The thread that this event belongs to.""" threadId: ID! """The title of the event.""" title: String! """The list of components of the event.""" components: [EventComponent!]! """ Whether this event should be rendered collapsed by default in the timeline. """ isCollapsed: Boolean! """The datetime when this event was created.""" createdAt: DateTime! """The actor who created this event.""" createdBy: Actor! """The datetime when this event was last updated.""" updatedAt: DateTime! """The actor who last updated this event.""" updatedBy: Actor! } type TimelineEntryEdge { cursor: String! node: TimelineEntry! } type TimelineEntryConnection { edges: [TimelineEntryEdge!]! pageInfo: PageInfo! } type MachineUser { id: ID! """Internal display name, visible only to workspace members.""" fullName: String! """ Customer-facing name shown when the machine user interacts with customers (e.g. sends messages). """ publicName: String! description: String """ The type of machine user. Defaults to API_USER if not specified during creation. """ type: MachineUserType! avatar: WorkspaceFile """Fetches a single API key belonging to this machine user by its ID.""" apiKey(apiKeyId: ID!): ApiKey """Paginated list of all API keys belonging to this machine user.""" apiKeys(first: Int, after: String, last: Int, before: String): ApiKeyConnection! createdBy: InternalActor! createdAt: DateTime! updatedBy: InternalActor! updatedAt: DateTime! """ True if the machine user has been deleted. Deleted machine users lose all their API keys and can no longer authenticate. """ isDeleted: Boolean! deletedAt: DateTime deletedBy: Actor } type MachineUserEdge { cursor: String! node: MachineUser! } type MachineUserConnection { edges: [MachineUserEdge!]! pageInfo: PageInfo! } type ApiKey { id: ID! description: String """ The list of permission strings granted to this API key, e.g. `customer:read`. These determine which API operations the key is authorized to perform. """ permissions: [String!]! createdBy: InternalActor! createdAt: DateTime! updatedBy: InternalActor! updatedAt: DateTime! """ Whether the key has been revoked. Deleted keys can no longer authenticate API requests. """ isDeleted: Boolean! """ The timestamp at which the key was revoked, or null if it is still active. """ deletedAt: DateTime """The actor who revoked the key, or null if it has not been deleted.""" deletedBy: Actor } type ApiKeyEdge { cursor: String! node: ApiKey! } type ApiKeyConnection { edges: [ApiKeyEdge!]! pageInfo: PageInfo! } """ A list of permission strings granted to a user or machine user in the current workspace context. """ type Permissions { """ The permission strings, e.g. 'thread:read', 'customer:create'. Use the `permissions` query to enumerate all valid values. """ permissions: [String!]! } enum MSTeamsChannelMemberRole { """A guest member with limited access to the channel's history.""" guest """A channel owner with full administrative access.""" owner } type MSTeamsChannelMember { id: ID! roles: [MSTeamsChannelMemberRole!]! displayName: String! """ ISO 8601 timestamp indicating how far back in the channel's message history this member can see, as reported by Microsoft Teams. """ visibleHistoryStartDateTime: String! """The Microsoft Azure AD object ID for this user.""" userId: ID! email: String! """The Azure Active Directory tenant ID this member belongs to.""" tenantId: ID! } type MSTeamsChannelMembers { members: [MSTeamsChannelMember!]! } type WorkspaceMSTeamsInstallationInfo { installationUrl: String! } """ Short-lived WorkOS admin portal URLs and an embeddable widget token for configuring SSO, directory sync, and domain verification. All URLs and the widget token are generated on demand and should be used immediately; do not cache or store them. """ type WorkOSConfiguration { """URL to configure SSO settings in the WorkOS admin portal.""" ssoUrl: String! """URL to configure directory sync settings in the WorkOS admin portal.""" directorySyncUrl: String! """ URL to configure domain verification settings in the WorkOS admin portal. """ domainVerificationUrl: String! """ A short-lived token (valid for one hour) for rendering embedded WorkOS admin UI components directly in your own interface, as an alternative to redirecting users to the portal URLs. """ widgetToken: String! } type UserMSTeamsInstallationInfo { installationUrl: String } type UserMSTeamsIntegration { id: ID! """ The Azure Active Directory tenant ID of the Microsoft Teams organization this user is connected to. """ msTeamsTenantId: ID! """ When true, the user must re-authorize their personal MS Teams integration. """ isReinstallRequired: Boolean! """ The preferred username (typically an email address) of the user's Microsoft account, as reported by Microsoft identity. """ msTeamsPreferredUsername: String createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } type UserSlackInstallationInfo { installationUrl: String! } type WorkspaceSlackInstallationInfo { installationUrl: String! } type WorkspaceSlackSidekickInstallationInfo { installationUrl: String! } type WorkspaceSlackChannelInstallationInfo { installationUrl: String! } type UserAuthSlackInstallationInfo { installationUrl: String! } type UserSlackIntegration { integrationId: ID! slackTeamName: String! """ True when the integration has lost access and needs to be re-authorized via the OAuth flow. """ isReinstallRequired: Boolean! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } type UserAuthSlackIntegration { integrationId: ID! """The Slack workspace ID this user-auth integration is connected to.""" slackTeamId: String! slackTeamName: String! """ True when the integration has lost access and needs to be re-authorized via the OAuth flow. """ isReinstallRequired: Boolean! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } type WorkspaceSlackIntegration { integrationId: ID! """ The name of the Slack channel where workspace notifications are posted. """ slackChannelName: String! slackTeamId: String! slackTeamName: String! """URL of the Slack workspace's icon at 68px. Null if unavailable.""" slackTeamImageUrl68px: String """ True when the integration has lost access and needs to be re-authorized via the OAuth flow. """ isReinstallRequired: Boolean! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } type WorkspaceSlackIntegrationEdge { cursor: String! node: WorkspaceSlackIntegration! } type WorkspaceSlackIntegrationConnection { edges: [WorkspaceSlackIntegrationEdge!]! pageInfo: PageInfo! } type WorkspaceSlackChannelIntegrationEdge { cursor: String! node: WorkspaceSlackChannelIntegration! } type WorkspaceSlackChannelIntegrationConnection { edges: [WorkspaceSlackChannelIntegrationEdge!]! pageInfo: PageInfo! } type WorkspaceSlackChannelIntegration { integrationId: ID! slackTeamId: String! slackTeamName: String! slackTeamImageUrl68px: String """ True when the integration has lost access and needs to be re-authorized via refreshWorkspaceSlackChannelIntegration. """ isReinstallRequired: Boolean! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } type WorkspaceSlackSidekickIntegration { """Unique identifier for this Sidekick integration record.""" integrationId: ID! """The Slack workspace (team) ID where Sidekick is installed.""" slackTeamId: ID! slackTeamName: String! """ The Slack channel ID of the #ask-plain channel that Sidekick monitors for user questions. """ askSidekickSlackChannelId: ID! """ The name of the Slack channel that Sidekick monitors for user questions. """ askSidekickSlackChannelName: String! """ True if the Slack app must be reinstalled to restore missing OAuth scopes or permissions; when true, direct the user through the installation flow again. """ isReinstallRequired: Boolean! """ Optional custom instructions that guide Sidekick's behavior and tone. Null means no custom instructions are set. Update via updateSidekickSlackConfig. """ operatingInstructions: String createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } type WorkspaceDiscordChannelIntegration { id: ID! """ The unique identifier of the Discord server (guild) connected to this workspace. """ discordGuildId: String! """The name of the Discord server (guild) at the time of connection.""" discordGuildName: String! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } type WorkspaceDiscordChannelInstallationInfo { installationUrl: String! } type WorkspaceDiscordIntegration { """The unique identifier for this webhook-based Discord integration.""" integrationId: ID! name: String! """ The Discord incoming webhook URL used to post messages to the target channel. """ webhookUrl: String! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } type WorkspaceDiscordChannelIntegrationEdge { cursor: String! node: WorkspaceDiscordChannelIntegration! } type WorkspaceDiscordChannelIntegrationConnection { edges: [WorkspaceDiscordChannelIntegrationEdge!]! pageInfo: PageInfo! } type UserAuthDiscordChannelInstallationInfo { installationUrl: String! } type WorkspaceDiscordIntegrationEdge { cursor: String! node: WorkspaceDiscordIntegration! } type WorkspaceDiscordIntegrationConnection { edges: [WorkspaceDiscordIntegrationEdge!]! pageInfo: PageInfo! } type UserLinearInstallationInfo { installationUrl: String! } type LinearIntegrationToken { """ A short-lived OAuth access token that can be used to call the Linear API on behalf of the authenticated user. """ token: String! } """ An OAuth access token representing the current user's connection to Jira. Use this token to make authenticated calls to the Jira API on behalf of the user. """ type JiraIntegrationToken { """The OAuth access token value to include in Jira API requests.""" token: String! createdAt: DateTime! } type UserLinearIntegration { """Unique identifier for this Linear integration record.""" integrationId: ID! """The name of the Linear organisation the user authenticated with.""" linearOrganisationName: String! """ The Linear-assigned identifier for the organisation the user authenticated with. """ linearOrganisationId: ID! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } """ A workspace-level Linear integration using the Linear app actor OAuth flow. Unlike UserLinearIntegration this is not tied to a single user, so machine users can use it. """ type WorkspaceLinearIntegration { integrationId: ID! linearOrganisationName: String! linearOrganisationId: ID! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } type WorkspaceLinearInstallationInfo { installationUrl: String! } type GithubUserAuthIntegration { id: ID! """The GitHub username (login) of the connected GitHub account.""" githubUsername: String! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } type WorkspaceCursorIntegration { id: ID! """ A redacted preview of the Cursor API token (e.g. `test-cur...-123`). The full token is never returned after creation. """ tokenPreview: String! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } type CursorRepository { """The GitHub organisation or user that owns the repository.""" owner: String! """The repository name within the owner's account.""" name: String! """The full HTTPS URL of the repository.""" repository: String! } """An API header that will be sent to the configured API URL.""" type CustomerCardConfigApiHeader { """ The name of the header, trimmed and treated case insensitively for deduplication purposes (min length: 1, max length: 100). Not all header names are allowed. """ name: String! """ The value of the header, treated case sensitively for deduplication purposes (min length: 1, max length: 500). """ value: String! } """ The configuration of a customer card that defines four important things: - The title of the card - The key of the card, which will be used in the request payload to the API URL - The order in which the cards should appear - Which API the card should be loaded from (and the required authentication headers) Configs that have the same API URL and API Headers will be loaded in batch. API header names are treated case insensitively. A maximum of 25 customer cards can be configured. """ type CustomerCardConfig { """The ID of the customer card config.""" id: ID! """ The order in which this customer card config should be shown. Duplicate order numbers are allowed, in case the order is the same they will be sorted based on `id`. The minimum is 0 and the maximum is 100000. """ order: Int! """The title of the card (max length: 500 characters).""" title: String! """ The key of the card, sent to your API in the request payload to identify which card is being requested (must be unique in a workspace, max length: 500 characters, must match regex: `[a-zA-Z0-9_-]+`). """ key: String! """ How long (in seconds) to cache a loaded card when your API response does not include its own TTL. Your API can override this per-response. (minimum: 15 seconds, maximum: 1 year or 31,536,000 seconds). """ defaultTimeToLiveSeconds: Int! """ The URL from which this card should be loaded (must start with `https://` and be a valid URL, max length: 600 characters). Requires the `customerCardConfigApiDetails:read` permission. """ apiUrl: String! """ An array of headers name-value pairs (maximum length of array: 20). Requires the `customerCardConfigApiDetails:read` permission. """ apiHeaders: [CustomerCardConfigApiHeader!]! """ Indicates if the customer card is enabled or not. Disabled customer card configs are not loaded or displayed for customers. """ isEnabled: Boolean! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } """ Identifies the context to which a setting value is attached. The combination of `scopeType` and `id` uniquely addresses one setting row. """ type SettingScope { """ The entity ID for scopes that require one (e.g. a chat app ID for `CHAT`, a Slack channel integration ID for `WORKSPACE_SLACK_CHANNEL`). Null for user- or workspace-level scopes where the ID is inferred from the authenticated session. """ id: ID """The category of scope this setting belongs to.""" scopeType: SettingScopeType! } """A boolean setting""" type BooleanSetting { """The setting code.""" code: String! """ The value of the setting. This is named uniquely (instead of just `value`) so that the union has unique fields. """ booleanValue: Boolean! """The scope of the setting.""" scope: SettingScope! } """A string setting""" type StringSetting { """The setting code.""" code: String! """ The value of the setting. This is named uniquely (instead of just `value`) so that the union has unique fields. """ stringValue: String! """The scope of the setting.""" scope: SettingScope! } """A number setting""" type NumberSetting { """The setting code.""" code: String! """ The value of the setting. This is named uniquely (instead of just `value`) so that the union has unique fields. """ numberValue: Int! """The scope of the setting.""" scope: SettingScope! } """A string array setting""" type StringArraySetting { """The setting code.""" code: String! """ The value of the setting. This is named uniquely (instead of just `value`) so that the union has unique fields. """ stringArrayValue: [String!]! """The scope of the setting.""" scope: SettingScope! } """ A union of all possible setting value types. Use inline fragments (`... on BooleanSetting`, `... on StringSetting`, `... on NumberSetting`, `... on StringArraySetting`) to access the concrete value for a given setting code. """ union Setting = BooleanSetting | StringSetting | NumberSetting | StringArraySetting """An enum to describe the type of scope the setting is for.""" enum SettingScopeType { """ Scope for any user level settings An `id` is not needed as it will implicitly be the authenticated user's id. """ USER """ Scope for any chat application settings An `id` is mandatory and should be a chat application id (`liveChatApp_123`) """ CHAT """ Scope for the authenticated user's email notification settings. An `id` is not needed as it will implicitly be the authenticated user's id. """ USER_EMAIL_NOTIFICATIONS """ Scope for the authenticated user's slack notification settings. An `id` is not needed as it will implicitly be the authenticated user's id. """ USER_SLACK_NOTIFICATIONS """ Scope for slack support channel settings. An `id` is mandatory and should be a workspace slack channel integration id (`wsSlackInt_123`) """ WORKSPACE_SLACK_CHANNEL """ Scope for per-channel slack ingestion overrides (`ingestion_mode`, `manual_ingestion_emoji`, `manual_ingestion_customer_enabled`). An `id` is mandatory and should be a connected slack channel id (`slackChan_123`). Falls back to `WORKSPACE_SLACK_CHANNEL` when no per-channel value is set. """ WORKSPACE_SLACK_CONNECTED_CHANNEL """ Scope for slack notifications configured for the whole workspace. An `id` is mandatory and should be a workspace slack integration id (`wsSlackInt_123`) """ WORKSPACE_SLACK_NOTIFICATIONS """ Scope for discord notifications configured for the whole workspace. An `id` is mandatory and should be a workspace discord integration id (`wsDiscordInt_123`) """ WORKSPACE_DISCORD_NOTIFICATIONS """ Scope for workspace level settings for the whole workspace. An `id` is not needed as it will implicitly be the current workspace id. """ WORKSPACE } """ The different ways in which a string is matched. Exactly one of these must be provided in a single search expression. """ input StringSearchExpression { """Case-insensitive match values containing the provided string.""" caseInsensitiveContains: String } """ The customer attributes available for search, each of them mapped to a search expression. Exactly one of them must be provided in a single search condition. """ input CustomerSearchCondition { """Search expression on the customer's full name.""" fullName: StringSearchExpression """Search expression on the customer's short name.""" shortName: StringSearchExpression """Search expression on the customer's email address.""" email: StringSearchExpression """Search expression on the customer's external id.""" externalId: StringSearchExpression """ Search expression on specific timeline entries' text (email, chat) sent or received by the customer. Common English stop-words will be removed from the text to search. """ timelineEntryText: StringSearchExpression } """ A query to search for customers. Search queries are combinations of search conditions, as defined below. At least one search condition must be provided. """ input CustomersSearchQuery { """ An array of search conditions that will be combined using a 'logical OR' to search for customers. """ or: [CustomerSearchCondition!] } type CustomerSearchEdge { cursor: String! node: Customer! } type CustomerSearchConnection { edges: [CustomerSearchEdge!]! pageInfo: PageInfo! } """ Represents the current load state of a single customer card for a specific customer. A customer can only have one card instance per config at any point in time. The three concrete implementations — `CustomerCardInstanceLoading`, `CustomerCardInstanceLoaded`, and `CustomerCardInstanceError` — reflect whether the card is being fetched, has loaded successfully, or failed to load. """ interface CustomerCardInstance { """ The ID of the customer card instance. A new ID is generated for each load. """ id: ID! """The customer the instance is for.""" customerId: ID! """ The thread the instance is for. Null if this card is not loaded in a thread context. """ threadId: ID """The customer card config this instance is for.""" customerCardConfig: CustomerCardConfig! createdAt: DateTime! createdBy: Actor! updatedAt: DateTime! updatedBy: Actor! } """ A customer card instance that is currently being fetched from the configured API URL. The `createdAt` timestamp indicates when the load was initiated. Subscribe to `customerCardInstanceChanges` to be notified when it transitions to `CustomerCardInstanceLoaded` or `CustomerCardInstanceError`. """ type CustomerCardInstanceLoading implements CustomerCardInstance { """ The ID of the customer card instance. A new ID is generated for each load. """ id: ID! """The customer the instance is for.""" customerId: ID! """ The thread the instance is for. Null if this card is not loaded in a thread context. """ threadId: ID """The customer card config this instance is for.""" customerCardConfig: CustomerCardConfig! createdAt: DateTime! createdBy: Actor! updatedAt: DateTime! updatedBy: Actor! } """ A successfully loaded customer card instance containing the card components returned by your API. The card remains valid until `expiresAt`, after which the next access will trigger a reload. """ type CustomerCardInstanceLoaded implements CustomerCardInstance { """ The ID of the customer card instance. A new ID is generated for each load. """ id: ID! """The customer the instance is for.""" customerId: ID! """ The thread the instance is for. Null if this card is not loaded in a thread context. """ threadId: ID """The customer card config this instance is for.""" customerCardConfig: CustomerCardConfig! """ The list of components of the customer card. If this is null it means the customer card was returned on the API, but the components array was empty. """ components: [CustomerCardComponent!] """When the customer card was received from the API.""" loadedAt: DateTime! """ When this cached card instance will expire and trigger a reload on the next access. """ expiresAt: DateTime! createdAt: DateTime! createdBy: Actor! updatedAt: DateTime! updatedBy: Actor! } """The configured API URL didn't return a requested card key.""" type CustomerCardInstanceMissingCardErrorDetail { message: String! cardKey: String! } """An invalid response body was returned from the configured API URL.""" type CustomerCardInstanceResponseBodyErrorDetail { message: String! responseBody: String! @deprecated(reason: "No longer supported, returns dummy data") } """A non-200 status code was returned from the configured API URL.""" type CustomerCardInstanceStatusCodeErrorDetail { message: String! statusCode: Int! responseBody: String! @deprecated(reason: "No longer supported, returns dummy data") } """Plain failed to make the request to the configured API URL.""" type CustomerCardInstanceRequestErrorDetail { message: String! errorCode: String! } """ An unknown error occurred. If this error is persistent, please contact our support. """ type CustomerCardInstanceUnknownErrorDetail { message: String! } """The card failed to load within the timeout.""" type CustomerCardInstanceTimeoutErrorDetail { message: String! timeoutSeconds: Int! } """The card exceeded the maximum allowed size.""" type CustomerCardInstanceCardTooBigErrorDetail { message: String! cardKey: String! sizeBytes: Int! maxSizeBytes: Int! } """Details for the reasons why the customer card failed to load.""" union CustomerCardInstanceErrorDetail = CustomerCardInstanceMissingCardErrorDetail | CustomerCardInstanceResponseBodyErrorDetail | CustomerCardInstanceStatusCodeErrorDetail | CustomerCardInstanceRequestErrorDetail | CustomerCardInstanceUnknownErrorDetail | CustomerCardInstanceTimeoutErrorDetail | CustomerCardInstanceCardTooBigErrorDetail """ A customer card instance that failed to load. Inspect `errorDetail` to determine why: the card API returned a non-200 status, timed out, returned an invalid body, or did not include the requested card key. """ type CustomerCardInstanceError implements CustomerCardInstance { """ The ID of the customer card instance. A new ID is generated for each load. """ id: ID! """The customer the instance is for.""" customerId: ID! """ The thread the instance is for. Null if this card is not loaded in a thread context. """ threadId: ID """The customer card config this instance is for.""" customerCardConfig: CustomerCardConfig! """The details of the customer card load error.""" errorDetail: CustomerCardInstanceErrorDetail! createdAt: DateTime! createdBy: Actor! updatedAt: DateTime! updatedBy: Actor! } type Query { """ Returns the UserAccount for the currently authenticated user, or null if no account has been created yet. Useful during onboarding to check whether account setup is complete. """ myUserAccount: UserAccount """ Returns the full User record for the currently authenticated human user within the current workspace. Returns null if the caller is not a human user or is not a member of a workspace. Not available to machine users. """ myUser: User """ Returns the machine user that owns the current API key. Only callable with a machine user API key; returns a FORBIDDEN error when called with a human user session. """ myMachineUser: MachineUser """ Returns the Workspace associated with the current API key or session. Useful for confirming which workspace a request is scoped to. Returns null if no workspace is in context. """ myWorkspace: Workspace """ Returns the full list of permission strings granted to the currently authenticated user or machine user in this workspace. Useful for inspecting what actions the caller is authorized to perform. """ myPermissions: Permissions! """ Returns all workspaces the currently authenticated human user belongs to, paginated. Not available to machine users. Use this to let users switch between workspaces. """ myWorkspaces(first: Int, after: String, last: Int, before: String): WorkspaceConnection! """ Returns all pending workspace invites sent to the currently authenticated user's email address. Use this to let a user see and act on workspaces they have been invited to join. """ myWorkspaceInvites(first: Int, after: String, last: Int, before: String): WorkspaceInviteConnection! """ Returns the current user's personal Slack notifications integration, or null if none is connected. """ mySlackIntegration: UserSlackIntegration """ Returns a Slack OAuth installation URL for the current user to connect their personal Slack notifications integration. Pass this URL to Slack's OAuth flow and exchange the resulting code with createMySlackIntegration. """ mySlackInstallationInfo(redirectUrl: String!): UserSlackInstallationInfo! """ Returns the current user's Linear integration, or null if no integration has been set up. """ myLinearIntegration: UserLinearIntegration """ Returns the OAuth installation URL to initiate the Linear authorization flow for the current user. Pass the returned installationUrl to your UI so the user can grant Plain access to their Linear account. The redirectUrl must match the redirect URI registered for the Linear OAuth app; the authorization code delivered there is then passed to createMyLinearIntegration. """ myLinearInstallationInfo(redirectUrl: String!): UserLinearInstallationInfo! """ Returns a short-lived Linear access token for the current user's integration, or null if no integration exists. The token is automatically refreshed when it is close to expiry, so callers always receive a usable credential. Use this token to make Linear API calls on behalf of the authenticated user. """ myLinearIntegrationToken: LinearIntegrationToken """ The workspace-level Linear app integration, if one has been authorised. """ linearAppIntegration: WorkspaceLinearIntegration """ Builds the Linear OAuth installation URL for the app actor flow (actor=app). """ workspaceLinearInstallationInfo(redirectUrl: String!): WorkspaceLinearInstallationInfo! """ Returns the GitHub user authentication integration for the currently authenticated user, or null if no integration exists. """ githubUserAuthIntegration: GithubUserAuthIntegration """ Returns the workspace's Cursor integration, or null if none has been configured. """ workspaceCursorIntegration: WorkspaceCursorIntegration """ Returns the list of GitHub repositories accessible via the given Cursor integration. Results are fetched live from the Cursor API and cached for up to 5 minutes. Requires the `workspaceCursorIntegration:read` permission. """ cursorRepositories(integrationId: ID!): [CursorRepository!]! """ Returns the current user's Jira OAuth access token, automatically refreshing it if it is near expiry. Returns null if the user has not connected their Jira account. Requires the `userJiraIntegration:read` permission. """ myJiraIntegrationToken: JiraIntegrationToken """ Returns the email signature configured for the currently authenticated user, or null if none has been set. """ myEmailSignature: EmailSignature """ Returns the favorite pages saved by the currently authenticated user, ordered by most recently created first. Each user's favorites are independent — this query only returns pages belonging to the caller. """ myFavoritePages(first: Int, after: String, last: Int, before: String): FavoritePageConnection! """ Returns the available billing plans that can be selected for self-serve checkout. Supports standard forward and backward pagination. """ billingPlans(first: Int, after: String, last: Int, before: String): BillingPlanConnection! """ Returns the current workspace billing subscription, including plan, status, trial info, feature entitlements, and credit balances. Returns null if the workspace has no billing account. """ myBillingSubscription: BillingSubscription """ Returns the current billing rota for the workspace, listing which users are actively on-rota and which are off-rota. The rota is used to track which users currently consume an active eng-rota seat. """ myBillingRota: BillingRota """ Sidekick credit usage for the workspace, bucketed by the UTC day each session resolved, over the given date range. Use it to show a per-day breakdown of how Sidekick is consuming credits. Requires the `billing:read` permission. """ sidekickCreditUsageByDay(input: SidekickCreditUsageByDayInput!): SidekickCreditUsageByDay! """ Returns short-lived WorkOS admin portal URLs and an embeddable widget token so workspace admins can configure SSO, directory sync, and domain verification. Each URL is a single-use link generated on the fly by WorkOS; fetch this query immediately before redirecting the user. Requires the `workspace:edit` permission and an active SSO entitlement (Frontier plan or above). Returns null if the workspace has no WorkOS organization configured. """ workOSConfiguration: WorkOSConfiguration """ Returns a paginated list of label types in the workspace. By default includes both active and archived label types; pass `filters: { isArchived: false }` to exclude archived ones. Requires the `labelType:read` permission. """ labelTypes(filters: LabelTypeFilter, first: Int, after: String, last: Int, before: String): LabelTypeConnection! """ Returns a single label type by its ID. Returns null if no label type with that ID exists. Requires the `labelType:read` permission. """ labelType(labelTypeId: ID!): LabelType """ Returns a label type by its external ID. Returns null if no match is found. External IDs are unique within a workspace and are set when creating or updating a label type. Requires the `labelType:read` permission. """ labelTypeByExternalId(externalId: ID!): LabelType """ Returns all roles available in the workspace, including both built-in roles (Owner, Admin, Support, Viewer, None) and any custom roles. Supports cursor-based pagination. Requires the `roles:read` permission. """ roles(first: Int, after: String, last: Int, before: String, filters: RoleFilter): RoleConnection! """ Returns all custom roles defined in the workspace. Use this to list the custom roles you have created and inspect their scope definitions. Supports cursor-based pagination. Requires the `roles:read` permission. """ customRoles(first: Int, after: String, last: Int, before: String): CustomRoleConnection! """ Returns a single custom role by its ID, or null if not found. Requires the `roles:read` permission. """ customRole(customRoleId: ID!): CustomRole """ Returns a paginated list of all timeline entries for a customer, ordered from oldest to newest. Timeline entries include every event, message, note, and automated activity that has occurred across all of the customer's threads. Supports cursor-based pagination via `first`/`after` and `last`/`before` arguments. """ timelineEntries(customerId: ID!, first: Int, after: String, last: Int, before: String): TimelineEntryConnection! """ Fetches a single timeline entry by its ID within a customer's timeline. Returns null if the entry does not exist. """ timelineEntry(customerId: ID!, timelineEntryId: ID!): TimelineEntry """ Fetch a workspace by its ID. Returns null if the workspace does not exist or the caller does not have access to it. """ workspace(workspaceId: ID!): Workspace """ Fetch a single workspace member by their ID. Returns null if no user with that ID exists. Requires the `user:read` permission. """ user(userId: ID!): User """ Fetch a workspace member by their email address. Returns null if no match is found. Deleted users are also returned — check the `isDeleted`, `deletedAt`, and `deletedBy` fields to determine whether the user has been removed. Requires the `user:read` permission. """ userByEmail(email: String!): User """ List all human members of the workspace, with optional filters to narrow results by role assignability. Supports cursor-based pagination. Requires the `user:read` permission. """ users(filters: UsersFilter, first: Int, after: String, last: Int, before: String): UserConnection! """ Returns all pending invites for the current workspace. Use this to view outstanding invitations and their status from an admin perspective. """ workspaceInvites(first: Int, after: String, last: Int, before: String): WorkspaceInviteConnection! """ Fetch a single customer by their Plain customer ID. Returns null if no customer with that ID exists. Requires the `customer:read` permission. """ customer(customerId: ID!): Customer """ Fetch a paginated list of all customers in the workspace. Supports filtering by group membership, company, spam status and more via `filters`, and ordering via `sortBy`. Use cursor-based pagination (`first`/`after` or `last`/`before`) to page through large result sets. Requires the `customer:read` permission. """ customers(filters: CustomersFilter, sortBy: CustomersSort, first: Int, after: String, last: Int, before: String): CustomerConnection! """ Fetch a customer by their email address. Returns null if no customer with that email exists. Requires the `customer:read` permission. """ customerByEmail(email: String!): Customer """ Get a customer by its external ID. A customer's external ID is unique within a workspace. """ customerByExternalId(externalId: ID!): Customer """ Fetch a single customer group by its ID. Returns null if no group with the given ID exists in the workspace. """ customerGroup(customerGroupId: ID!): CustomerGroup """ Fetch a paginated list of all customer groups in the workspace. Optionally filter by external IDs using the `filters` argument. Uses cursor-based pagination. """ customerGroups(filters: CustomerGroupsFilter, first: Int, after: String, last: Int, before: String): CustomerGroupConnection! """ Returns all thread field schemas defined in the workspace, paginated. Use this to discover which custom fields exist and their configuration before reading or writing thread field values. """ threadFieldSchemas(first: Int, after: String, last: Int, before: String): ThreadFieldSchemaConnection! """ Fetches a single thread field schema by its ID. Returns null if no schema with the given ID exists. """ threadFieldSchema(threadFieldSchemaId: ID!): ThreadFieldSchema """ Returns the current customer card instances for a customer, triggering a fresh load for any cards that are expired or in an error state. Cards that are cached and within their TTL are returned immediately as `CustomerCardInstanceLoaded`. Cards that need to be fetched are returned as `CustomerCardInstanceLoading`; subscribe to `customerCardInstanceChanges` to receive the result when loading completes. Pass `threadId` to provide thread context to your card API endpoint. A maximum of 25 card instances will be returned, due to only allowing 25 customer card configs. """ customerCardInstances(customerId: ID!, threadId: ID): [CustomerCardInstance!]! """ Search for customers using a case-insensitive partial match across name, short name, email, and external ID. Results are sorted by most recently active first. Best suited for human-driven lookups (e.g. a search box) rather than precise programmatic resolution — use `customerByEmail` or `customerByExternalId` for exact lookups. Requires the `customer:read` permission. """ searchCustomers(searchQuery: CustomersSearchQuery!, first: Int, after: String, last: Int, before: String): CustomerSearchConnection! """ Returns a paginated list of all snippets in the workspace. Use this to sync or display the full snippet library. Supports standard forward and backward cursor pagination. Requires the `snippet:read` permission. """ snippets(first: Int, after: String, last: Int, before: String): SnippetConnection! """ Fetches a single snippet by its ID, or null if no snippet with that ID exists. Returns soft-deleted snippets (where `isDeleted` is true). Requires the `snippet:read` permission. """ snippet(snippetId: ID!): Snippet """ Returns the workspace's email settings, including whether email is enabled, any custom domain configuration, and BCC addresses. """ workspaceEmailSettings: WorkspaceEmailSettings! """ Returns the workspace-level chat settings, including whether the live-chat channel is enabled for this workspace. """ workspaceChatSettings: WorkspaceChatSettings! """ Fetches a single machine user by ID. Returns null if no machine user with the given ID exists in the workspace. """ machineUser(machineUserId: ID!): MachineUser """ Returns a paginated list of all machine users in the workspace. Use the optional `filters.type` argument to restrict results to a specific machine user type (e.g. API_USER or AI_AGENT). """ machineUsers(filters: MachineUsersFilter, first: Int, after: String, last: Int, before: String): MachineUserConnection! """ Returns the complete list of all permission strings defined in Plain, sorted alphabetically. Use this to discover valid permission values when building role or API key management UIs. Requires the `permission:read` permission. """ permissions: Permissions! """ Returns the Microsoft Teams admin-consent URL that a workspace administrator must visit to authorize Plain's bot for the workspace's Azure AD tenant. Pass the URL the user should be redirected to after consent as `redirectUrl`. If the workspace already has an integration, the URL is scoped to that tenant to prevent accidentally consenting in the wrong one. """ workspaceMSTeamsInstallationInfo(redirectUrl: String!): WorkspaceMSTeamsInstallationInfo! """ Returns the workspace-level Microsoft Teams integration, or null if none has been created yet. """ workspaceMSTeamsIntegration: WorkspaceMSTeamsIntegration """ Returns the OAuth installation URL for the current user to connect their personal Microsoft Teams account to Plain. Returns null in `installationUrl` if the workspace does not have an MS Teams workspace integration configured. """ myMSTeamsInstallationInfo(redirectUrl: String!): UserMSTeamsInstallationInfo! """ Returns the Microsoft Teams integration for the currently authenticated user, or null if the user has not connected their account. """ myMSTeamsIntegration: UserMSTeamsIntegration """ Returns a paginated list of Microsoft Teams channels that are connected to this workspace. Supports forward and backward cursor pagination. """ connectedMSTeamsChannels(first: Int, after: String, last: Int, before: String): ConnectedMSTeamsChannelConnection! """ Fetches the current members of a Microsoft Teams channel directly from the Teams API. Requires a workspace MS Teams integration and calls Microsoft Graph; use this to populate member lists when resolving customers for a channel. """ getMSTeamsMembersForChannel(msTeamsChannelId: ID!, msTeamsTeamId: ID!): MSTeamsChannelMembers! """ Returns a Slack OAuth installation URL for connecting a workspace-level Slack notifications integration. Pass this URL to Slack's OAuth flow and exchange the resulting code with createWorkspaceSlackIntegration. """ workspaceSlackInstallationInfo(redirectUrl: String!): WorkspaceSlackInstallationInfo! """ Returns a paginated list of all workspace-level Slack notifications integrations for this workspace. """ workspaceSlackIntegrations(first: Int, after: String, last: Int, before: String): WorkspaceSlackIntegrationConnection! """ Returns a single workspace-level Slack notifications integration by ID, or null if not found. """ workspaceSlackIntegration(integrationId: ID!): WorkspaceSlackIntegration """ Returns a Slack OAuth installation URL for connecting a workspace Slack channel integration. Pass this URL to Slack's OAuth flow and exchange the resulting code with createWorkspaceSlackChannelIntegration. """ workspaceSlackChannelInstallationInfo(redirectUrl: String!): WorkspaceSlackChannelInstallationInfo! """ Returns a single workspace Slack channel integration by ID, or null if not found. """ workspaceSlackChannelIntegration(integrationId: ID!): WorkspaceSlackChannelIntegration """ Returns a paginated list of all workspace Slack channel integrations for this workspace. """ workspaceSlackChannelIntegrations(first: Int, after: String, last: Int, before: String): WorkspaceSlackChannelIntegrationConnection! """ Returns the OAuth installation URL to begin installing Plain's Sidekick AI agent into your Slack workspace. Pass the returned URL to your users to redirect them through Slack's OAuth flow; on completion, pass the resulting auth code to createWorkspaceSlackSidekickIntegration. """ workspaceSlackSidekickInstallationInfo(redirectUrl: String!): WorkspaceSlackSidekickInstallationInfo! """ Returns the current workspace's Slack Sidekick integration, or null if Sidekick has not been installed. Requires the workspaceSlackSidekickIntegration:read permission. """ workspaceSlackSidekickIntegration: WorkspaceSlackSidekickIntegration """ Returns the Discord OAuth installation URL needed to connect a Discord server (guild) to this workspace. Redirect the user to the returned URL to begin the OAuth flow; after authorization Discord will redirect back to the provided `redirectUrl` with an `authCode` you can pass to `createWorkspaceDiscordChannelIntegration`. """ workspaceDiscordChannelInstallationInfo(redirectUrl: String!): WorkspaceDiscordChannelInstallationInfo! """ Fetches a single workspace Discord channel integration by its ID. Returns null if no integration with that ID exists. """ workspaceDiscordChannelIntegration(integrationId: ID!): WorkspaceDiscordChannelIntegration """ Returns a paginated list of all Discord server (guild) channel integrations connected to this workspace. """ workspaceDiscordChannelIntegrations(first: Int, after: String, last: Int, before: String): WorkspaceDiscordChannelIntegrationConnection! """ Returns the current user's personal Discord authentication for a specific guild, or null if the user has not connected their Discord account to that guild. """ userAuthDiscordChannelIntegration(discordGuildId: String!): UserAuthDiscordChannelIntegration """ Returns a paginated list of all personal Discord authentication integrations for the current user across all guilds. """ userAuthDiscordChannelIntegrations(first: Int, after: String, last: Int, before: String): UserAuthDiscordChannelIntegrationConnection! """ Returns the Discord OAuth URL for a user to personally authenticate with Discord. Individual users must complete this flow (in addition to the workspace-level integration) before they can send Discord messages on behalf of themselves. After authorization, Discord redirects back to `redirectUrl` with an `authCode` for `createUserAuthDiscordChannelIntegration`. """ userAuthDiscordChannelInstallationInfo(redirectUrl: String!): UserAuthDiscordChannelInstallationInfo! """ Searches for slack users in a thread based on a search term. The search term can be part of either the slack's handle or full name. """ searchThreadSlackUsers(threadId: ID!, searchQuery: String!, first: Int, after: String, last: Int, before: String): SlackUserConnection! """ Searches for slack users in a slack channel based on a search term. The search term can be part of either the slack's handle or full name. """ searchSlackUsers(slackTeamId: String!, slackChannelId: String!, searchQuery: String!, first: Int, after: String, last: Int, before: String): SlackUserConnection! """ Gets all slack channels for this workspace, which match the specified filters. """ connectedSlackChannels(filters: ConnectedSlackChannelsFilter, first: Int, after: String, last: Int, before: String): ConnectedSlackChannelConnection! """ Returns a single connected Slack channel by its Plain ID, or null if not found. """ connectedSlackChannel(connectedSlackChannelId: ID!): ConnectedSlackChannel """ Gets the auto-join rules for a workspace slack channel integration. Rules that have not yet been saved through `setSlackAutoJoinRules` are read from the legacy prefix/suffix settings and have a null id. """ slackAutoJoinRules(integrationId: ID!): [SlackAutoJoinRule!]! """ Returns a Slack user associated with the given thread, looked up by their Slack user ID. Returns null if the user is not found in that thread's Slack context. """ threadSlackUser(threadId: ID!, slackUserId: ID!): SlackUser """ Returns a Slack user within a specific channel, looked up by their Slack user ID. Returns null if the user is not found in that channel. """ slackUser(slackTeamId: String!, slackChannelId: String!, slackUserId: ID!): SlackUser """ Resolves a Slack message permalink to the Plain thread it belongs to. Returns null if no thread is associated with the given Slack message. """ threadBySlackPermalink(slackPermalink: String!): Thread """ Returns the Slack channels the current user belongs to within the given Slack workspace. Useful for populating channel pickers in UI integrations. """ userSlackChannelMemberships(slackTeamId: String!): [SlackChannelMembership!]! """ Returns the current user's user-auth Slack integration for the given Slack workspace, or null if not connected. Used for integrations that send messages as the user rather than as a bot. """ userAuthSlackIntegration(slackTeamId: String!): UserAuthSlackIntegration """ Returns the current user's user-auth Slack integration for the Slack workspace associated with the given thread, or null if not connected. """ userAuthSlackIntegrationByThreadId(threadId: ID!): UserAuthSlackIntegration """ Returns a Slack OAuth installation URL for the current user to connect their user-auth Slack integration. Optionally scoped to a specific Slack workspace. Pass the resulting code to createUserAuthSlackIntegration. """ userAuthSlackInstallationInfo(redirectUrl: String!, slackTeamId: String): UserAuthSlackInstallationInfo! """ Returns a paginated list of all webhook-based Discord integrations configured for this workspace. """ workspaceDiscordIntegrations(first: Int, after: String, last: Int, before: String): WorkspaceDiscordIntegrationConnection! """ Fetches a single webhook-based workspace Discord integration by its ID. Returns null if not found. """ workspaceDiscordIntegration(integrationId: ID!): WorkspaceDiscordIntegration """ Returns a paginated list of Discord channels synced from a specific guild. Use `refreshConnectedDiscordChannels` to pull the latest channel list from Discord before querying. """ connectedDiscordChannels(discordGuildId: String!, first: Int, after: String, last: Int, before: String): ConnectedDiscordChannelConnection! """ Returns all customer card configs for the workspace, ordered by their `order` field. """ customerCardConfigs: [CustomerCardConfig!]! """ Returns a single customer card config by ID. Returns null if no config with the given ID exists. """ customerCardConfig(customerCardConfigId: ID!): CustomerCardConfig """ Fetch the effective value of a named setting at the given scope. Returns null when the setting has no stored value at that scope and no default applies. Use this to read notification preferences, workflow flags, chat-app configuration, and similar per-scope options. The `code` identifies which setting to read (e.g. `workflow/unassign_thread_after_mark_thread_as_done`) and `scope` pins the context (workspace, user, chat app, Slack channel, etc.). """ setting(code: String!, scope: SettingScopeInput!): Setting """ List all available webhook schema versions. Use this to discover which versions you can pin a webhook target to, and to check whether a version you are already using has been deprecated. Requires the `webhookTarget:read` permission. """ webhookVersions(first: Int, after: String, last: Int, before: String): WebhookVersionConnection! """ Fetch a single webhook target by its ID. Returns null if no target with that ID exists in the workspace. Requires the `webhookTarget:read` permission. """ webhookTarget(webhookTargetId: ID!): WebhookTarget """ List all webhook targets registered in the workspace, paginated. Returns every endpoint Plain is configured to deliver events to. Requires the `webhookTarget:read` permission. """ webhookTargets(first: Int, after: String, last: Int, before: String): WebhookTargetConnection! """ List delivery attempts for a webhook target, newest first. Each attempt records the event that was delivered, when it was attempted, how long it took, and whether it succeeded or failed. Use the optional `filters` argument to narrow results by event type or result status — useful for surfacing recent failures in your own observability tooling. Requires the `webhookTarget:read` permission. """ webhookDeliveryAttempts(webhookTargetId: ID!, filters: WebhookDeliveryAttemptFilter, first: Int, after: String, last: Int, before: String): WebhookDeliveryAttemptConnection! """ Fetch the full JSON request body that Plain sent (or would send) for a given public event. Returns null if the event has expired — events are retained for 30 days. Use this alongside `webhookDeliveryAttempts` to inspect the exact payload for a failed delivery attempt and replay or debug it. Requires the `webhookTarget:read` permission. """ publicEventRequestBody(publicEventId: ID!): String """ List every event type that can be subscribed to on a webhook target. Each entry includes the event type identifier and a human-readable description. Use this to discover valid values for `eventSubscriptions` when creating or updating a webhook target. Requires the `subscriptionEventTypes:read` permission. """ subscriptionEventTypes: [SubscriptionEventType!]! """ Get a workflow rule by id. Returns null if no rule with the given ID exists. Workflow rules are the older, condition-plus-action configuration model; see `workflow` for the newer step-based model. """ workflowRule(workflowRuleId: ID!): WorkflowRule """ List all workflow rules in the workspace, paginated. Rules are returned in their configured display order. Use `workflowRule` to fetch a single rule by ID. """ workflowRules(first: Int, after: String, last: Int, before: String): WorkflowRuleConnection! """ Get a workflow by id. Returns null if no workflow with the given ID exists. Workflows are the step-based automation model that supports conditions, actions, and wait steps connected in a graph. """ workflow(workflowId: ID!): Workflow """ List workflows in the workspace, paginated. Use `filters` to narrow results by trigger type (MANUAL or EVENTS) or published status. """ workflows(first: Int, after: String, last: Int, before: String, filters: WorkflowsFilter): WorkflowConnection! """ Get a single workflow execution by id. Returns null if no execution with the given ID exists. Use this to inspect the status, timing, and per-step results of a specific run. """ workflowExecution(workflowExecutionId: ID!): WorkflowExecution """ List executions for a specific workflow, paginated. Use this to audit the run history of a workflow and check for failures. """ workflowExecutions(workflowId: ID!, first: Int, after: String, last: Int, before: String): WorkflowExecutionConnection! """ List workflow executions filtered by the entity they ran against (e.g. a specific thread), paginated. Useful for showing all automation that has fired on a given thread. Optionally narrow further by workflow ID or execution status. """ workflowExecutionsByEntity(filters: WorkflowExecutionByEntityFilter!, first: Int, after: String, last: Int, before: String): WorkflowExecutionConnection! """ Fetch a single chat app by its ID. Returns null if no chat app with that ID exists in the workspace. Requires the `chatApp:read` permission. """ chatApp(chatAppId: ID!): ChatApp """ List all chat apps in the workspace. Supports cursor-based pagination. Requires the `chatApp:read` permission. """ chatApps(first: Int, after: String, last: Int, before: String): ChatAppConnection! """ Check whether a signing secret exists for a chat app. Returns metadata about the secret (creation time, actor) but never the secret value itself — the raw secret is only returned once, at creation time via `createChatAppSecret`. Returns null if no secret has been created for the given chat app. Requires the `chatAppSecret:read` permission. """ chatAppSecret(chatAppId: ID!): ChatAppHiddenSecret """ Fetch a single thread by its Plain-assigned ID. Returns null if no thread with that ID exists. Requires the `thread:read` permission. """ thread(threadId: ID!): Thread """ Fetch a thread by its human-readable ref (e.g. `T-1234`). Useful when the ref is more convenient to store than the internal ID. Returns null if no match. Requires the `thread:read` permission. """ threadByRef(ref: String!): Thread """ Fetch a thread by the external ID you assigned it, scoped to a specific customer. Because `externalId` is only unique per-customer, both `customerId` and `externalId` are required. Returns null if no match. Requires the `thread:read` permission. """ threadByExternalId(customerId: ID!, externalId: ID!): Thread """ List threads with optional filtering and sorting, returned as a paginated connection. Supports rich filters (status, assignee, customer, label, priority, date ranges, tenant, tier, thread fields, and more) and multiple sort orders. Use this query to build inbox-style views or to export threads in bulk. Requires the `thread:read` permission. """ threads(filters: ThreadsFilter, sortBy: ThreadsSort, first: Int, after: String, last: Int, before: String): ThreadConnection! """ Full-text search across thread titles, message contents, and customer names/emails. Accepts optional `ThreadsFilter` to narrow results further. For exact lookups by ID, ref, or external ID use the dedicated queries instead. Requires the `thread:read` permission. """ searchThreads(searchQuery: ThreadsSearchQuery!, filters: ThreadsFilter, first: Int, after: String, last: Int, before: String): ThreadSearchResultConnection! """ Paginated list of threads that have been deleted. Only threads deleted after the deletion audit log was enabled for your workspace are included. Useful for auditing or syncing deletions to an external system. Requires the `thread:read` permission. """ deletedThreads(filters: DeletedThreadsFilter, first: Int, after: String, last: Int, before: String): DeletedThreadConnection! """ Fetch a single autoresponder by its ID. Returns null if no autoresponder with that ID exists. """ autoresponder(autoresponderId: ID!): Autoresponder """ List all autoresponders in the workspace, ordered by their configured priority order. Supports cursor-based pagination. """ autoresponders(first: Int, after: String, last: Int, before: String): AutoresponderConnection! """ Fetch a named time-series metric for the workspace, returning data points bucketed over a time range. Pass a metric name such as `threads_created_count` or `threads_first_response_time__p50` and optional options to control the date range, grouping dimension, and interval. Requires the `metrics:read` permission; agent-specific metric names additionally require `metricsAgent:read` and the `team_reporting` billing feature. The available look-back window is determined by your plan's `insights_max_days` entitlement. """ timeSeriesMetric(name: String!, options: TimeSeriesMetricOptions): TimeSeriesMetric """ Fetch a named single-value (aggregate) metric for the workspace over a given time range. Use this for summary figures such as median first-response time or SLA compliance percentage. Supports the same `dimension` grouping as `timeSeriesMetric` so results can be broken down by company, label type, tier, and more. Requires `metrics:read`; agent metrics require `metricsAgent:read`. """ singleValueMetric(name: String!, options: SingleValueMetricOptions): SingleValueMetric """ Fetch a named heatmap metric for the workspace, distributing activity across a 7-day × 24-hour grid (Monday–Sunday, hour 0–23 UTC). Each cell reports the thread count, its share of total volume as a percentage, and the list of thread IDs. Use this to identify peak support hours during a date range. Requires `metrics:read`. """ heatmapMetric(name: String!, options: HeatmapMetricOptionsInput): HeatmapMetric """ Fetch a time-series metric for threads with rich filter and group-by support. Choose a `TimeSeriesMetricName` (e.g. `threads_created_count`, `threads_first_response_time__p50`), specify a mandatory date range and bucketing interval, and optionally group results by assignee, company, tier, label type, and more. This is the preferred reporting query when you need server-side filtering across thread attributes. Requires `metrics:read`. """ threadTimeSeriesMetric(input: ThreadTimeSeriesMetricInput!): ThreadTimeSeriesMetric! """ Fetch an aggregate (single-value) metric for threads with rich filter and group-by support. Returns one value per group bucket for the specified date range and `SingleValueMetricName`, making it suitable for summary dashboards or leaderboard-style breakdowns by assignee, tier, or label type. Requires `metrics:read`. """ threadSingleValueMetric(input: ThreadSingleValueMetricInput!): ThreadSingleValueMetric! """ Fetch a heatmap metric for threads with filter support, distributing thread activity across a 7-day × 24-hour grid. Unlike the legacy `heatmapMetric`, this query uses the unified `ThreadMetricFilters` input for consistent filtering by label, tier, assignee, and more. Requires `metrics:read`. """ threadHeatmapMetric(input: ThreadHeatmapMetricInput!): ThreadHeatmapMetric! """ Returns a paginated list of all companies in your workspace. Supports cursor-based pagination and optional filtering by ID or deletion status. Requires the `company:read` permission. """ companies(first: Int, after: String, last: Int, before: String, filters: CompaniesFilter): CompanyConnection! """ Fetches a single company by its ID. Returns null if no company with that ID exists. Requires the `company:read` permission. """ company(companyId: ID!): Company """ Searches companies by name or domain using a case-insensitive partial match. The search term must be at least 2 characters long. Supports cursor-based pagination and optional filtering. Each match is returned as a `CompanySearchResult` that wraps the matched company. Requires the `company:read` permission. """ searchCompanies(searchQuery: CompaniesSearchQuery!, filters: CompaniesFilter, first: Int, after: String, last: Int, before: String): CompanySearchResultConnection! """ Fetch the settings for a team (label type of kind TEAM) identified by its label type ID. Returns null if no settings have been configured yet for the team. """ teamSettings(labelTypeId: ID!): TeamSettings """ Returns a paginated list of tenants in the workspace. Use the `filters` argument to narrow by ID, deleted status, or last-updated time. Requires the `tenant:read` permission. """ tenants(first: Int, after: String, last: Int, before: String, filters: TenantsFilter): TenantConnection! """ Fetches a single tenant by its Plain-assigned ID. Returns null if no matching tenant is found. Requires the `tenant:read` permission. """ tenant(tenantId: ID!): Tenant """ Fetch a single task by its ID. Returns null if no task with that ID exists. Requires the `task:read` permission. """ task(taskId: ID!): Task """ Fetch a single task by its short human-readable ref (e.g. `T-123`), as displayed in the Plain app. Returns null if no task matches. Requires the `task:read` permission. """ taskByRef(ref: String!): Task """ Fetch a paginated list of tasks in the workspace. Use `filters` to narrow results by status, assignee, company, or tenant. Use `sortBy` to order by priority, status, or creation/update time. Requires the `task:read` permission. """ tasks(first: Int, after: String, last: Int, before: String, filters: TasksFilter, sortBy: TasksSort): TaskConnection! """ Returns the list of tenant field schemas defined in the workspace. Use the `source` and `isVisible` filters to narrow results. Supports cursor-based pagination. """ tenantFieldSchemas(filters: TenantFieldSchemasFilter, first: Int, after: String, last: Int, before: String): TenantFieldSchemaConnection! """ Searches tenants by name (case-insensitive partial match) or by external ID (exact match). The search term must be at least 2 characters long. Returns a paginated list of results. Requires the `tenant:read` permission. """ searchTenants(searchQuery: TenantsSearchQuery!, first: Int, after: String, last: Int, before: String): TenantSearchResultConnection! """ Fetch a single discussion by its ID. Returns null if no discussion with the given ID exists. """ threadDiscussion(threadDiscussionId: ID!): ThreadDiscussion """ Fetch a single discussion by its ID. Preferred over threadDiscussion for new integrations. """ discussion(discussionId: ID!): ThreadDiscussion """ List discussions in the workspace, with optional filtering and sorting. Supports cursor-based pagination. Filter by thread, status, creator, last-activity timestamps, source entity, or discussion type. """ discussions(filters: DiscussionsFilter, sortBy: DiscussionsSort, first: Int, after: String, last: Int, before: String): ThreadDiscussionConnection! """ Fetch a single completed service authorization by its ID. Returns null if not found or if the authorization is still pending. """ serviceAuthorization(serviceAuthorizationId: ID!): ServiceAuthorization """ List all completed service authorizations for the workspace. Only authorizations with status CONNECTED or REINSTALL_REQUIRED are returned; pending authorizations are excluded. Optionally filter by serviceIntegrationKey. Supports cursor-based pagination. Requires the `serviceAuthorization:read` permission. """ serviceAuthorizations(first: Int, after: String, last: Int, before: String, filters: ServiceAuthorizationsFilter): ServiceAuthorizationConnection! """ Returns every GitHub repository the Sidekick integration (identified by serviceAuthorizationId) can currently see via its OAuth token. Use this list to let users pick which repos Sidekick should focus on before calling updateSidekickGithubConfig. """ sidekickGithubAccessibleRepos(serviceAuthorizationId: ID!): [SidekickGithubRepo!]! """ Returns the Sidekick GitHub configuration for a workspace: which repositories are selected for Sidekick to use and any workspace-level or per-repo operating instructions. Returns null when no configuration has been saved yet. """ sidekickGithubServiceConfig(serviceAuthorizationId: ID!): SidekickGithubServiceConfig """ Returns the Sidekick configuration for a connected service (Datadog, Sentry, Grafana, Linear, Notion, incident.io, Attio, HubSpot, Jira, Granola, LaunchDarkly or Grain), identified by its serviceAuthorizationId. Includes any operating instructions that guide how Sidekick uses the integration. Returns null when no configuration has been saved yet. GitHub and PostHog have their own configuration shapes — use sidekickGithubServiceConfig / sidekickPosthogServiceConfig instead. """ sidekickServiceConfig(serviceAuthorizationId: ID!): SidekickServiceConfig """ Returns the Sidekick PostHog configuration for a workspace, including any operating instructions and the default project the agent queries. Returns null when no configuration has been saved yet. """ sidekickPosthogServiceConfig(serviceAuthorizationId: ID!): SidekickPosthogServiceConfig """ Returns workspace-level Sidekick settings, including the custom prompt that is appended to every Sidekick session's system prompt. Always returns an object; fields inside are null when not configured. """ sidekickSettings: SidekickSettings! """ The Sidekick tool approval policies for the workspace: every catalog tool with its effective approval mode (per-workspace override merged over the factory default). Optionally filter to a single service (e.g. "plain"). """ agentSandboxToolPolicies(service: String): [AgentSandboxToolPolicy!]! """ Fetch the available tenant lists (e.g. company lists or account views) from a connected external service such as Attio, HubSpot, or Salesforce. Use this to let users choose which list to sync when setting up an import. Requires a completed service authorization ID. """ importerTenantLists(serviceAuthorizationId: ID!, first: Int, after: String, last: Int, before: String): ImporterTenantListConnection! """ Fetch the import job definition for a connected service integration. Pass isEnabled: true to return only the currently active definition, or isEnabled: false to return any definition regardless of enabled state. Returns null if no matching definition exists. """ importJobDefinition(serviceIntegrationKey: String!, isEnabled: Boolean): ImportJobDefinition """ List import jobs, optionally filtered by service integration key or import job definition ID. Each job represents a single sync run triggered by an import job definition, and contains per-entity-type runs with their progress and status. """ importJobs(filters: ImportJobsFilter, first: Int, after: String, last: Int, before: String): ImportJobConnection! """ Returns all tiers in the workspace, sorted by creation date. Supports cursor-based pagination. Requires the `tier:read` permission. """ tiers(first: Int, after: String, last: Int, before: String): TierConnection! """ Returns a single tier by its Plain ID. Returns null if no tier with that ID exists. Requires the `tier:read` permission. """ tier(tierId: ID!): Tier """ Fetch the workspace's business hours configuration. Deprecated — use businessHoursSlots instead, which supports timezone-aware slots. """ businessHours: BusinessHours @deprecated(reason: "Use businessHoursSlots instead.") """ Return all active business hours slots for the workspace. Each slot specifies a weekday, timezone, and open/close times. Returns an empty list if no business hours are configured. Requires the `businessHours:read` permission. """ businessHoursSlots: [BusinessHoursSlot!]! """ Returns the workspace's HMAC configuration, including the secret used to sign outbound webhook and HTTP-request payloads. Returns null if no secret has been generated yet. Requires the `workspaceHmac:read` permission. """ workspaceHmac: WorkspaceHmac """ Returns a paginated list of thread link groups, each representing a distinct external entity (e.g. a Linear issue or Jira ticket) that is linked to one or more threads. Use this to build views that aggregate threads by a shared linked issue. Supports filtering by status, specific group IDs, company, or tier. """ threadLinkGroups(first: Int, after: String, last: Int, before: String, filters: ThreadLinkGroupFilter): ThreadLinkGroupConnection! """ Find threads that are semantically similar to the given thread, ranked by relevance. Each result includes the thread and a distance score (lower means more similar). Useful for surfacing related support history or finding duplicate issues. Requires the `thread:read` permission. """ relatedThreads(threadId: ID!): [ThreadWithDistance!]! """ Perform a semantic (vector) search across your workspace's knowledge sources and help center articles. Returns up to `pageSize` results (default 10, max 50) ranked by relevance. Use `options` to restrict results by label type, content type (indexed documents vs help center articles), or to include help center articles that are not publicly accessible. """ searchKnowledgeSources(searchQuery: String!, pageSize: Int, options: SearchKnowledgeSourcesOptions): [KnowledgeSourceSearchResult!]! """ Returns all AI-generated thread clusters for the current workspace, sorted by thread count descending. Clusters group semantically similar threads together so you can spot trends and recurring issues at a glance. The optional `variant` argument selects which clustering model run to return; omit it to receive the default variant. This API is in beta and may change without notice. """ threadClusters(variant: String): [ThreadCluster!]! """ Fetches a single thread cluster by its ID. Returns null if no cluster with that ID exists in the current workspace. This API is in beta and may change without notice. """ threadCluster(id: ID!): ThreadCluster """ Returns the cluster that the given thread currently belongs to, or null if the thread has not been assigned to any cluster. Use this to show a customer-support agent which broader topic group a specific thread falls into. This API is in beta and may change without notice. """ activeThreadCluster(threadId: ID!): ThreadCluster """ Returns a paginated list of AI-generated thread clusters for the current workspace. Use `filters` to narrow results by company, tenant, or clustering variant. Supports standard cursor-based pagination via `first`/`after` and `last`/`before`. Requires the `thread:read` permission. This API is in beta and may change without notice. """ threadClustersPaginated(first: Int, after: String, last: Int, before: String, filters: ThreadClustersFilter): ThreadClusterConnection! """ This API is in beta and may change without notice. Returns the current AI-suggested reply candidates for a thread, scoped to the most recent customer message. Returns an empty list if Plain AI suggested responses are disabled for the workspace, if no customer message exists on the thread, or if no suggestion has been generated yet. Requires the `generatedReply:read` permission. """ generatedReplies(threadId: ID!, options: [GenerateReplyOption!]): [GeneratedReply!] """ List indexed documents across your workspace. Use the `knowledgeSourceId` filter to retrieve only the documents belonging to a specific knowledge source. """ indexedDocuments(first: Int, after: String, last: Int, before: String, filters: IndexedDocumentsFilter): IndexedDocumentConnection! """ Fetch a single knowledge source by its ID. Returns null if no knowledge source with the given ID exists. """ knowledgeSource(knowledgeSourceId: ID!): KnowledgeSource """ List all knowledge sources in the workspace. Use the `type` filter to retrieve only sitemap or single-URL sources. """ knowledgeSources(first: Int, after: String, last: Int, before: String, filters: KnowledgeSourcesFilter): KnowledgeSourceConnection! """ Returns the workspace's AI tone rules, which guide how Plain's AI features phrase replies. Use the optional `isEnabled` filter to retrieve only active rules. Pagination is supported via standard `first`/`after` and `last`/`before` cursor arguments, though in practice all rules are returned in a single page. Requires the `aiToneRule:read` permission. """ aiToneRules(first: Int, after: String, last: Int, before: String, filters: AiToneRulesFilter): AiToneRuleConnection! """ Returns all currently enabled AI tone rules serialised as a plain-text instruction string that is suitable for injecting into AI prompts. Returns `null` when no rules are enabled or the feature flag is off. Requires the `aiToneRule:read` permission. """ enabledAiToneRulesText: String """ Returns all saved threads views for the workspace, ordered by creation time. Use this to list the views available in your workspace. Supports forward and backward cursor pagination. """ savedThreadsViews(first: Int, after: String, last: Int, before: String): SavedThreadsViewConnection! """ Fetches a single saved threads view by its ID. Returns null if no view with the given ID exists in the workspace. """ savedThreadsView(savedThreadsViewId: ID!): SavedThreadsView """ Searches for external entities (e.g. Jira issues) that can be linked to a thread. Filter by `sourceType` to scope the search to a specific issue tracker, and provide a free-text `searchQuery` to match against issue titles or identifiers. Returns candidates that can then be passed to `createThreadLink`. Requires a connected issue tracker integration for the specified source type. """ searchThreadLinkCandidates(filters: ThreadLinkCandidateFilter!, searchQuery: String!, first: Int, after: String, last: Int, before: String): ThreadLinkCandidateConnection! """ List all help centers in the workspace. Supports cursor-based pagination and optional filtering. """ helpCenters(first: Int, after: String, last: Int, before: String, filters: HelpCentersFilter): HelpCenterConnection! """Fetch a single help center by ID. Returns null if not found.""" helpCenter(id: ID!): HelpCenter """Fetch a single help center article by ID. Returns null if not found.""" helpCenterArticle(id: ID!): HelpCenterArticle """Fetch a single article group by ID. Returns null if not found.""" helpCenterArticleGroup(id: ID!): HelpCenterArticleGroup """ Fetch the navigation index for a help center by its ID. Returns null if not found. Use the returned hash when calling updateHelpCenterIndex to avoid clobbering concurrent edits. """ helpCenterIndex(id: ID!): HelpCenterIndex """ Fetch an article by its URL slug within a specific help center. Slugs are unique per help center. Returns null if no article matches. """ helpCenterArticleBySlug(helpCenterId: ID!, slug: String!): HelpCenterArticle """ Fetch an article group by its URL slug within a specific help center. Returns null if no group matches. """ helpCenterArticleGroupBySlug(helpCenterId: ID!, slug: String!): HelpCenterArticleGroup """ Fetch the configurable fields for a connected issue tracker (e.g. Shortcut, Rootly, incident.io, GitHub). Pass previously selected field values in `selectedFields` so that dependent fields (such as labels that depend on a chosen repository) are populated correctly. Returns the list of fields the caller must or may provide when calling `createIssueTrackerIssue`. Requires the `threadLinkCandidate:search` permission. """ issueTrackerFields(issueTrackerType: String!, selectedFields: [SelectedIssueTrackerField!]!): [IssueTrackerField!]! """ Returns a paginated list of all customer surveys configured in the workspace, ordered by their display order. Use this to build management UIs or sync survey configurations. """ customerSurveys(first: Int, after: String, last: Int, before: String): CustomerSurveyConnection! """ Fetches a single customer survey by its ID. Returns null if no survey with that ID exists. """ customerSurvey(id: ID!): CustomerSurvey """ Returns a paginated list of all escalation paths configured in the workspace. Requires the `escalationPath:read` permission. """ escalationPaths(first: Int, after: String, last: Int, before: String): EscalationPathConnection! """ Fetches a single escalation path by its ID. Returns null if no escalation path with the given ID exists. Requires the `escalationPath:read` permission. """ escalationPath(id: ID!): EscalationPath """ Returns a paginated list of internal notifications for the currently authenticated user. Use the optional `filters` argument to narrow results by read status or creation time. Supports forward and backward cursor-based pagination via `first`/`after` and `last`/`before`. Only available to human users — machine users will receive a forbidden error. """ myInternalNotifications(filters: InternalNotificationsFilter, first: Int, after: String, last: Int, before: String): InternalNotificationConnection! """ Fetch a single knowledge gap by its ID. Returns null if no gap with that ID exists in the workspace. Requires the `knowledgeGap:read` permission. """ knowledgeGap(knowledgeGapId: ID!): KnowledgeGap """ Paginated list of knowledge gaps detected in the workspace. Optionally filter by status (OPEN, RESOLVED, IGNORED) and sort by signal count or recency. Defaults to descending signal count order. Requires the `knowledgeGap:read` permission. """ knowledgeGaps(filters: KnowledgeGapsFilter, sortBy: KnowledgeGapsSort, first: Int, after: String, last: Int, before: String): KnowledgeGapConnection! } type EscalationPath { id: ID! name: String! """ The ordered sequence of escalation steps. When a thread is escalated, it advances to the next step in this list. """ steps: [EscalationPathStep!]! """ An optional human-readable description of the escalation path's purpose. """ description: String createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } """ A single step in an escalation path. Each step either targets a specific user or all owners of a label type. """ union EscalationPathStep = EscalationPathStepUser | EscalationPathStepLabelType """ An escalation step that assigns the thread the given label type, routing it to that label type's owners. """ type EscalationPathStepLabelType { id: ID! labelType: LabelType! } """ An escalation step that directly assigns the thread to a specific user. """ type EscalationPathStepUser { id: ID! user: User! } type EscalationPathEdge { cursor: String! node: EscalationPath! } type EscalationPathConnection { edges: [EscalationPathEdge!]! pageInfo: PageInfo! } input SelectedIssueTrackerField { key: String! value: String! } enum IssueTrackerFieldType { """The field accepts exactly one value chosen from `options`.""" SELECT """The field accepts one or more values chosen from `options`.""" MULTI_SELECT } type IssueTrackerField { name: String! """ Machine-readable identifier for this field, used as the `key` in `SelectedIssueTrackerField` and `IssueTrackerFieldInput`. """ key: String! type: IssueTrackerFieldType! """ Key of the parent field whose selected value controls the available options for this field (e.g. a 'labels' field whose options depend on the chosen 'repository'). Null if this field has no dependency. """ parentFieldKey: String options: [IssueTrackerFieldOption!]! """ The value from `selectedFields` that was passed when fetching this field, echoed back so UIs can pre-populate the selection. Null if no value was provided. """ selectedValue: String """ Whether this field must be provided in `fields` when calling `createIssueTrackerIssue`. """ isRequired: Boolean! } type IssueTrackerFieldOption { name: String! value: String! icon: String color: String } input ThreadLinkCandidateFilter { sourceType: String! } type ThreadLinkCandidateConnection { edges: [ThreadLinkCandidateEdge!]! pageInfo: PageInfo! } type ThreadLinkCandidateEdge { cursor: String! node: ThreadLinkCandidate! } type ThreadLinkCandidate { """ The identifier of this candidate in its external system; pass this as `sourceId` when calling `createThreadLink`. """ sourceId: String! """ The external system type for this candidate (e.g. `jira_issue`); pass this as `sourceType` when calling `createThreadLink`. """ sourceType: String! title: String! description: String url: String! status: ThreadLinkStatus! """ The granular, provider-specific status of this candidate (e.g. the raw Jira status label and color). """ sourceStatus: ThreadLinkSourceStatus } type ThreadLinkSourceStatus { """The machine-readable status key from the external system.""" key: String! """The human-readable status label from the external system.""" label: String! color: String icon: String } type GeneratedReply { id: ID! """The suggested reply content in Markdown format (max 5,000 characters).""" markdown: String! """ The ID of the customer timeline entry (message) this suggestion is attached to. Null for legacy suggestions created before this field was introduced. """ timelineEntryId: ID text: String! @deprecated(reason: "Use markdown instead.") createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } input GenerateReplyOption { key: String value: String } """ A thread paired with a semantic similarity score, returned by `relatedThreads`. """ type ThreadWithDistance { thread: Thread! """ Semantic distance to the source thread. Lower values indicate greater similarity. """ distance: Float! } type MinimalThreadWithDistance { threadId: ID! customerId: ID! tierId: ID distance: Float! } type ThreadCluster { id: ID! title: String! """ A longer AI-generated summary of the common theme shared by threads in this cluster. """ description: String! """ A broader AI-generated category label for the cluster (e.g. 'bug', 'feedback', 'other'). """ category: String! """ The overall AI-assessed sentiment of threads in this cluster: one of 'positive', 'neutral', or 'negative'. """ sentiment: String! """ A score between 0 and 1 indicating how coherent and well-defined the cluster is; null when confidence has not been computed. """ confidence: Float """ A single emoji chosen by the AI to visually represent the cluster's theme. """ emoji: String! createdAt: DateTime! createdBy: Actor! updatedAt: DateTime! updatedBy: Actor! """ The threads that belong to this cluster, each annotated with their semantic distance from the cluster centroid. """ threads: [MinimalThreadWithDistance!]! } type ThreadClusterEdge { cursor: String! node: ThreadCluster! } type ThreadClusterConnection { edges: [ThreadClusterEdge!]! pageInfo: PageInfo! } input ThreadClustersFilter { variant: String companyIds: [ID!] tenantIds: [ID!] } type Tier { id: ID! """The name of this tier.""" name: String! """ The external ID of this tier. You can use this field to store your own unique identifier for this tier. This must be unique in your workspace. """ externalId: String """ The color to assign to this tier, given by its hex code (e.g. #FABADA). This color is used in Plain's UI to represent this tier. """ color: String! """ If true, this tier will be applied to all threads that do not match any other tier. Only one tier can be the default tier. """ isDefault: Boolean! """ Any thread created in this tier will have this priority by default, unless a different priority is specified while creating it. """ defaultPriority: Int! @deprecated(reason: "Use defaultThreadPriority instead.") """ Any thread created in this tier will have this priority by default, unless a different priority is specified while creating it. """ defaultThreadPriority: Int! """ If true, this tier was automatically created based on tenant field values and should not be manually modified. """ isMachineTier: Boolean! """ The paginated list of tenant and company members that belong to this tier. """ memberships(first: Int, after: String, last: Int, before: String): TierMembershipConnection! """ The SLAs attached to this tier that define first-response and next-response time targets for threads. """ serviceLevelAgreements: [ServiceLevelAgreement!]! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } type TenantTierMembership { id: ID! tierId: ID! tenantId: ID! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } type CompanyTierMembership { id: ID! tierId: ID! companyId: ID! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } union TierMembership = TenantTierMembership | CompanyTierMembership type TierMembershipEdge { cursor: String! node: TierMembership! } type TierMembershipConnection { edges: [TierMembershipEdge!]! pageInfo: PageInfo! totalCount: Int! } """Restricts which threads an SLA applies to based on their label types.""" type ServiceLevelAgreementThreadLabelTypeIdFilter { """ The label type IDs that the thread needs to have in order for the SLA to be applied. Based on the 'requireAll' field. """ labelTypeIds: [ID!]! """ If true, the SLA will only be applied to threads that have all of the provided label types. If false, the SLA will be applied to threads that have any of the provided label types. """ requireAll: Boolean! } """ A service level agreement (SLA) attached to a tier. Every thread belonging to the tier inherits the SLA and is tracked against its time target. """ interface ServiceLevelAgreement { id: ID! """ If true, the SLA will only be tracked during your workspace's business hours. If false, the SLA will tracked 24/7. """ useBusinessHoursOnly: Boolean! """ This SLA can only be applied to a thread if it has one of these priority values. """ threadPriorityFilter: [Int!]! """ This SLA can only be applied to a thread if it has one of these label types. """ threadLabelTypeIdFilter: ServiceLevelAgreementThreadLabelTypeIdFilter """ The actions to take when the SLA is about to breach and when it breaches. """ breachActions: [BreachAction!]! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } """ An SLA that tracks the time from thread creation until a teammate sends the first reply. """ type FirstResponseTimeServiceLevelAgreement implements ServiceLevelAgreement { id: ID! """ This SLA will breach if it does not receive a first response within this many minutes. """ firstResponseTimeMinutes: Int! useBusinessHoursOnly: Boolean! threadPriorityFilter: [Int!]! threadLabelTypeIdFilter: ServiceLevelAgreementThreadLabelTypeIdFilter breachActions: [BreachAction!]! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } """ An SLA that tracks the time from each new customer message until a teammate replies. Requires a first-response-time SLA to exist on the same tier. """ type NextResponseTimeServiceLevelAgreement implements ServiceLevelAgreement { id: ID! """ This SLA will breach if it does not receive a next response within this many minutes. """ nextResponseTimeMinutes: Int! useBusinessHoursOnly: Boolean! threadPriorityFilter: [Int!]! threadLabelTypeIdFilter: ServiceLevelAgreementThreadLabelTypeIdFilter breachActions: [BreachAction!]! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } union BreachAction = BeforeBreachAction """ A breach action that triggers a notification a set number of minutes before the SLA deadline is reached. """ type BeforeBreachAction { """ How many minutes before the SLA breach time the notification is triggered. Must be strictly less than the SLA time target. """ beforeBreachMinutes: Int! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } type TierEdge { cursor: String! node: Tier! } type TierConnection { edges: [TierEdge!]! pageInfo: PageInfo! } input CustomerGroupsFilter { externalIds: [String!] } input CustomerGroupMembershipsFilter { customerGroupExternalIds: [String!] } enum MetricDimensionType { COMPANY CUSTOMER_GROUP LABEL_TYPE MESSAGE_SOURCE PRIORITY THREAD_FIELD TENANT_FIELD TIER } type MetricDimension { type: MetricDimensionType! value: String! } input SingleValueMetricFilters { userId: ID csatRating: Int csatSurveyId: ID } input SingleValueMetricOptions { """Defaults to 24 hours ago.""" from: String to: String dimension: MetricDimensionType subDimension: String filters: SingleValueMetricFilters } type SingleValueMetricValue { """ The computed metric value. Null when no data exists for this dimension bucket in the requested period. """ value: Float """ Present when a grouping dimension was requested; identifies which dimension value this bucket represents. """ dimension: MetricDimension """Present when the metric was filtered to a specific user.""" userId: ID } """ Result of a `singleValueMetric` query. Contains one value per dimension bucket, or a single entry when no dimension is used. """ type SingleValueMetric { values: [SingleValueMetricValue!]! } """ One cell of a heatmap grid, representing activity for a specific day-of-week and hour-of-day combination. """ type HeatmapHour { """ This cell's share of total volume across the entire grid, expressed as a value 0–100. """ percentage: Float! """Number of threads active in this hour/day bucket.""" total: Int! """IDs of the threads counted in this bucket.""" threadIds: [String!]! """Total number of messages sent in this bucket.""" messageCount: Int! } """ Result of a `heatmapMetric` query. Activity distributed across a 7 × 24 grid (Monday–Sunday, hour 0–23 UTC). """ type HeatmapMetric { """ 7 elements (Monday=0 … Sunday=6), each containing 24 hourly buckets (hour 0–23 UTC). """ days: [[HeatmapHour!]!]! } input HeatmapMetricFilters { userId: ID } input HeatmapMetricOptionsInput { from: String to: String dimensionType: MetricDimensionType dimensionValue: String subDimension: String filters: HeatmapMetricFilters } """ Dimension used to break down a legacy time-series or single-value metric into per-bucket series. """ enum TimeSeriesMetricDimensionType { COMPANY CUSTOMER_GROUP LABEL_TYPE """Source system of the thread's messages (e.g. EMAIL, SLACK, API).""" MESSAGE_SOURCE PRIORITY """ A custom field attached to the thread. Requires `subDimension` set to the field key. """ THREAD_FIELD """ A custom field attached to the thread's tenant. Requires `subDimension` set to the field's external ID. """ TENANT_FIELD TIER } enum TimeSeriesMetricIntervalUnit { HOUR DAY WEEK MONTH QUARTER YEAR } input TimeSeriesMetricInterval { unit: TimeSeriesMetricIntervalUnit } input TimeSeriesMetricFilters { userId: ID csatRating: Int csatSurveyId: ID } input TimeSeriesMetricOptions { """Defaults to 24 hours ago.""" from: String to: String dimension: TimeSeriesMetricDimensionType subDimension: String interval: TimeSeriesMetricInterval filters: TimeSeriesMetricFilters fetchThreadIds: Boolean } """ Result of a `timeSeriesMetric` query. Each element in `timestamps` corresponds to the same index across all series. """ type TimeSeriesMetric { """ Ordered bucket start times for the series data points, one per interval. """ timestamps: [DateTime!]! """ One series entry per dimension value (or a single entry when no dimension is requested). """ series: [TimeSeriesSeries!]! } type TimeSeriesSeries { """ Metric values aligned index-for-index with `TimeSeriesMetric.timestamps`. Null indicates no data for that bucket. """ values: [Float]! """Present when the metric was filtered to a specific user.""" userId: ID """ Present when a grouping dimension was requested; identifies which dimension value this series represents. """ dimension: TimeSeriesMetricDimension """ Thread IDs per bucket, present only when `fetchThreadIds` was set to true in the query options. """ threadIds: [[String]] } type TimeSeriesMetricDimension { type: TimeSeriesMetricDimensionType! """ The identifier for this dimension bucket (e.g. a company ID, label type ID, or the string value of a priority). """ value: String! } enum SingleValueMetricName { service_level_agreement_compliance_frt service_level_agreement_compliance_nrt threads_first_response_time_median threads_resolution_time_median threads_all_time_count_done threads_time_between_follow_up_responses_median threads_time_customer_waiting_median threads_csat__percentage threads_csat__count agent_threads_first_response_time_median agent_threads_time_between_follow_up_responses_median agent_threads_resolution_time_median agent_threads_time_customer_waiting_median agent_service_level_agreement_compliance_frt agent_service_level_agreement_compliance_nrt } enum TimeSeriesMetricName { threads_created_count threads_first_response_time__p50 threads_first_response_time__p90 threads_status_count__todo threads_status_count__done threads_status_count__snoozed threads_resolution_time__p50 threads_resolution_time__p90 threads_time_customer_waiting__p50 threads_time_customer_waiting__p90 threads_status_transitions_count__todo threads_status_transitions_count__todo_with_created threads_status_transitions_count__done threads_status_transitions_count__snoozed threads_time_between_follow_up_responses__p50 threads_time_between_follow_up_responses__p90 threads_time_between_all_responses__p50 threads_time_between_all_responses__p90 threads_csat__percentage threads_csat__count agent_messages_sent_count agent_threads_first_response_time__p50 agent_threads_time_between_follow_up_responses__p50 agent_threads_resolution_time__p50 agent_threads_assignment_transitions_count agent_threads_status_transitions_count__todo agent_threads_status_transitions_count__done agent_threads_status_transitions_count__snoozed agent_threads_csat__percentage agent_threads_csat__count } enum HeatmapMetricName { threads_created_count_heatmap agent_messages_sent_count } """Dimension by which thread metric results can be grouped.""" enum ThreadMetricGroupBy { ASSIGNEE """Users added as additional (secondary) assignees on the thread.""" ADDITIONAL_ASSIGNEE """ Messaging channel the thread originated from (e.g. email, chat, Slack). """ CHANNEL COMPANY CUSTOMER_GROUP LABEL_TYPE """Source system of the thread's messages (e.g. EMAIL, SLACK, API).""" MESSAGE_SOURCE PRIORITY STATUS """Fine-grained sub-status within the main thread status.""" STATUS_DETAIL TENANT """ A custom field attached to the thread's tenant. Requires `subKey` set to the field's external ID. """ TENANT_FIELD """ A custom field attached to the thread. Requires `subKey` set to the field key. """ THREAD_FIELD TIER } enum ThreadMetricIntervalUnit { HOUR DAY WEEK MONTH QUARTER YEAR } input ThreadMetricInterval { unit: ThreadMetricIntervalUnit! } """ Sub-key required when dimension is keyed (THREAD_FIELD key, TENANT_FIELD externalFieldId). """ input ThreadMetricGroupByInput { dimension: ThreadMetricGroupBy! subKey: String topN: Int } input ThreadMetricFilters { labelTypeIds: [ID!] companyIdentifiers: [CompanyIdentifierInput!] tenantIdentifiers: [TenantIdentifierInput!] tierIdentifiers: [TierIdentifierInput!] customerGroupIdentifiers: [CustomerGroupIdentifier!] assignedToUser: [ID!] additionalAssignedToUser: [ID!] priorities: [Int!] statuses: [ThreadStatus!] statusDetails: [StatusDetailType!] messageSource: [MessageSource!] threadFields: [ThreadFieldFilter!] tenantFields: [TenantFieldFilter!] surveyResponse: SurveyResponseFilter userId: ID and: [ThreadMetricFilters!] or: [ThreadMetricFilters!] not: ThreadMetricFilters } input ThreadTimeSeriesMetricInput { metricName: TimeSeriesMetricName! """ISO 8601 format (e.g. 2024-10-28T18:30:00Z).""" from: String! """ISO 8601 format (e.g. 2024-10-28T18:30:00Z).""" to: String! interval: ThreadMetricInterval! groupBy: [ThreadMetricGroupByInput!] filters: ThreadMetricFilters """ When true, each data point also returns the thread ids that contributed to it (in ThreadTimeSeriesSeries.threadIds). Not yet implemented. """ fetchThreadIds: Boolean } input ThreadSingleValueMetricInput { metricName: SingleValueMetricName! """ ISO 8601 format (e.g. 2024-10-28T18:30:00Z). Required for all metrics except all-time counts (e.g. threads_all_time_count_done), which reject a date range. """ from: String """ ISO 8601 format (e.g. 2024-10-28T18:30:00Z). Required for all metrics except all-time counts (e.g. threads_all_time_count_done), which reject a date range. """ to: String groupBy: [ThreadMetricGroupByInput!] filters: ThreadMetricFilters } input ThreadHeatmapMetricInput { metricName: HeatmapMetricName! """ISO 8601 format (e.g. 2024-10-28T18:30:00Z).""" from: String! """ISO 8601 format (e.g. 2024-10-28T18:30:00Z).""" to: String! filters: ThreadMetricFilters } """ Identifies which group bucket a series or value belongs to in a grouped thread metric result. """ type ThreadMetricGroup { dimension: ThreadMetricGroupBy! """Stable id for the bucket (companyId, tierId, label id, etc.).""" value: String! """Human-readable label (e.g. Stytch, Tier 1).""" label: String } """ Result of a `threadTimeSeriesMetric` query. Each element in `timestamps` aligns with the same index across all series. """ type ThreadTimeSeriesMetric { """ Ordered bucket start times for the series data points, one per interval. """ timestamps: [DateTime!]! """ One series entry per group bucket (or a single entry when no group-by is requested). """ series: [ThreadTimeSeriesSeries!]! } type ThreadTimeSeriesSeries { """ Metric values aligned index-for-index with `ThreadTimeSeriesMetric.timestamps`. Null indicates no data for that bucket. """ values: [Float]! """ The group this series belongs to. Null when no `groupBy` was requested. """ group: ThreadMetricGroup """ Thread ids contributing to each data point, aligned with values. Only populated when the request set fetchThreadIds to true. Not yet implemented. """ threadIds: [[String]] } """ Result of a `threadSingleValueMetric` query. Contains one aggregate value per group bucket. """ type ThreadSingleValueMetric { values: [ThreadSingleValueMetricValue!]! } type ThreadSingleValueMetricValue { """ The computed metric value for this group bucket. Null when no data exists in the requested period. """ value: Float """The group this value belongs to. Null when no `groupBy` was requested.""" group: ThreadMetricGroup } """ Result of a `threadHeatmapMetric` query. Activity distributed across a 7 × 24 grid (Monday–Sunday, hour 0–23 UTC). """ type ThreadHeatmapMetric { """ 7 elements (Monday=0 … Sunday=6), each containing 24 hourly buckets (hour 0–23 UTC). """ days: [[ThreadHeatmapHour!]!]! } """ One cell of a thread heatmap grid, representing activity for a specific day-of-week and hour-of-day combination. """ type ThreadHeatmapHour { """Number of threads active in this hour/day bucket.""" total: Int! """ This cell's share of total volume across the entire grid, expressed as a value 0–100. """ percentage: Float! } input TierIdentifierInput { tierId: ID externalId: String } input ThreadFieldNumberFilter { value: Float gte: Float lte: Float } input ThreadFieldDateFilter { """ Timestamps -greater or equal- than this value. ISO 8601 format (e.g. 2024-10-28T18:30:00Z). """ after: String """ Timestamps -less- than this value. ISO 8601 format (e.g. 2024-10-28T18:30:00Z). """ before: String } input ThreadFieldFilter { key: String! stringValue: String booleanValue: Boolean number: ThreadFieldNumberFilter date: ThreadFieldDateFilter } input ThreadLinkSourceFilter { sourceType: String! sourceId: ID! } input DatetimeFilter { """ Timestamps -greater or equal- than this value. ISO 8601 format (e.g. 2024-10-28T18:30:00Z). """ after: String """ Timestamps -less- than this value. ISO 8601 format (e.g. 2024-10-28T18:30:00Z). """ before: String } type DatetimeFilterOutput { """ Timestamps -greater or equal- than this value. ISO 8601 format (e.g. 2024-10-28T18:30:00Z). """ after: String """ Timestamps -less- than this value. ISO 8601 format (e.g. 2024-10-28T18:30:00Z). """ before: String } enum SentimentType { POSITIVE NEGATIVE NEUTRAL } input SurveyResponseFilter { """ Filter for threads with any survey response, regardless of the specific values """ hasResponse: Boolean sentiment: SentimentType rating: Int surveyId: ID responseAt: DatetimeFilter } type SurveyResponseFilterOutput { """ Filter for threads with any survey response, regardless of the specific values. """ hasResponse: Boolean! sentiment: SentimentType rating: Int surveyId: ID responseAt: DatetimeFilterOutput } input TasksFilter { assignedToUser: [ID!] companyIds: [ID!] isAssigned: Boolean statuses: [TaskStatus!] tenantIds: [ID!] and: [TasksFilter!] or: [TasksFilter!] not: TasksFilter } enum TasksSortField { PRIORITY STATUS CREATED_AT UPDATED_AT } input TasksSort { field: TasksSortField! direction: SortDirection! } input ThreadsFilter { threadIds: [ID!] refs: [String!] labelTypeIds: [ID!] priorities: [Int!] customerIds: [ID!] isAssigned: Boolean assignedToUser: [ID!] isMarkedAsSpam: Boolean supportEmailAddresses: [String!] customerGroupIdentifiers: [CustomerGroupIdentifier!] serviceLevelAgreements: ServiceLevelAgreementFilter tierIdentifiers: [TierIdentifierInput!] threadFields: [ThreadFieldFilter!] tenantIdentifiers: [TenantIdentifierInput!] companyIdentifiers: [CompanyIdentifierInput!] messageSource: [MessageSource!] participantIds: [ID!] statusChangedAt: DatetimeFilter statuses: [ThreadStatus!] statusDetails: [StatusDetailType!] threadLinkGroupIds: [ID!] threadLinkSources: [ThreadLinkSourceFilter!] createdAt: DatetimeFilter updatedAt: DatetimeFilter surveyResponse: SurveyResponseFilter tenantFields: [TenantFieldFilter!] agentStatus: AgentStatusFilter and: [ThreadsFilter!] or: [ThreadsFilter!] not: ThreadsFilter } input TenantFieldFilter { externalFieldId: String! stringValue: String booleanValue: Boolean dateValue: String numberValue: Float stringArrayValue: [String!] userReferenceValues: [ID!] } enum ServiceLevelAgreementType { FIRST_RESPONSE_TIME NEXT_RESPONSE_TIME } input ServiceLevelAgreementFilter { types: [ServiceLevelAgreementType!] statuses: [ServiceLevelAgreementStatus!] updatedAt: DatetimeFilter } input AgentStatusFilter { statuses: [AgentStatus!] handedOffReason: String updatedAt: DatetimeFilter } enum ThreadsSortField { STATUS_CHANGED_AT CREATED_AT CLOSEST_TO_BREACH_SLA LAST_INBOUND_MESSAGE_AT PRIORITY THREAD_FIELD } input ThreadsSort { field: ThreadsSortField! direction: SortDirection! threadFieldKey: String } type ThreadConnection { pageInfo: PageInfo! edges: [ThreadEdge!]! totalCount: Int! } type ThreadEdge { cursor: String! node: Thread! } """ A record of a thread that has been deleted, returned by the `deletedThreads` query. """ type DeletedThread { """The ID of the deleted thread.""" threadId: ID! """The timestamp when the thread was deleted.""" deletedAt: DateTime! } type DeletedThreadEdge { cursor: String! node: DeletedThread! } type DeletedThreadConnection { pageInfo: PageInfo! edges: [DeletedThreadEdge!]! } input DeletedThreadsFilter { """Filter deleted threads by deletion timestamp.""" deletedAt: DatetimeFilter } """An enum for why the mutation failed overall.""" enum MutationErrorType { """ Input validation failed, see the `fields` for details on why the input was invalid. """ VALIDATION """ The user is not authorized to do this mutation. See `message` for details on which permissions are missing. """ FORBIDDEN """ An unknown internal server error occurred. Retry the mutation and if it persists, please email help@plain.com """ INTERNAL } """An enum specific to each field, explaining why validation failed.""" enum MutationFieldErrorType { """ The field was provided, but didn't pass the requirements of the field. See `message` for details on why. """ VALIDATION """ The field is required to be provided. String inputs may be trimmed and checked for emptiness. """ REQUIRED """The input field referenced an entity that wasn't found.""" NOT_FOUND } """ A type indicating an error has occurred with a specific field in the input. """ type MutationFieldError { """The name of the field for which the error happened.""" field: String! """ An English technical description of the error. This error is usually meant to be read by a developer and not an end user. """ message: String! """ The type of the error. Can be used to display a user friendly error message. """ type: MutationFieldErrorType! } """A type indicating an error has occurred while making a mutation.""" type MutationError { """ An English technical description of the error. This error is usually meant to be read by a developer and not an end user. """ message: String! """ The type of error. Can be used to display a user friendly error message. """ type: MutationErrorType! """ A fixed error code that can be used to handle this error, see https://www.plain.com/docs/graphql/error-codes for a description of each code. """ code: String! """The array of fields that are impacted by this error.""" fields: [MutationFieldError!]! } input StringInput { value: String! } input IntInput { value: Int! } input BooleanInput { value: Boolean! } input OptionalStringInput { value: String } input OptionalBooleanInput { value: Boolean } input OptionalFloatInput { value: Float } input CreateUserAccountInput { fullName: String! publicName: String! marketingConsent: Boolean } type CreateUserAccountOutput { userAccount: UserAccount error: MutationError } input CreateWorkspaceInput { name: String! publicName: String! } type CreateWorkspaceOutput { workspace: Workspace error: MutationError } input InviteUserToWorkspaceInput { email: String! roleKey: RoleKey customRoleId: ID usingBillingRotaSeat: BooleanInput } type InviteUserToWorkspaceOutput { invite: WorkspaceInvite error: MutationError } input AcceptWorkspaceInviteInput { inviteId: ID! } type AcceptWorkspaceInviteOutput { invite: WorkspaceInvite error: MutationError } input DeleteWorkspaceInviteInput { inviteId: ID! } type DeleteWorkspaceInviteOutput { invite: WorkspaceInvite error: MutationError } input AssignRolesToUserInput { userId: ID! roleIds: [ID!] @deprecated(reason: "Use roleKey instead.") roleKey: RoleKey customRoleId: ID usingBillingRotaSeat: BooleanInput } type AssignRolesToUserOutput { error: MutationError } input CreateCustomRoleInput { name: String! description: String } type CreateCustomRoleOutput { role: CustomRole error: MutationError } input UpdateCustomRoleInput { customRoleId: ID! name: OptionalStringInput description: OptionalStringInput } type UpdateCustomRoleOutput { role: CustomRole error: MutationError } input DeleteCustomRoleInput { customRoleId: ID! } type DeleteCustomRoleOutput { deletedCustomRoleId: ID error: MutationError } """ The type of resource that a role scope applies to. Currently only threads are supported. """ enum RoleScopeResourceType { THREAD } """ The dimension of a thread used to filter visibility for a custom role scope. """ enum ThreadScopePrimitiveType { LABEL TIER """ A messaging channel such as Slack, Discord, or email (identified by channel key or email address). """ CHANNEL TENANT COMPANY } enum RoleScopeAccessMode { """See all resources regardless of this primitive (default)""" VIEW_ANY """See only resources with these specific values""" VIEW_ONLY """See all resources except those with these values""" VIEW_ALL_EXCEPT } input ScopeConditionInput { primitiveType: ThreadScopePrimitiveType! accessMode: RoleScopeAccessMode! """ IDs of the values to include/exclude (required for VIEW_ONLY and VIEW_ALL_EXCEPT) """ values: [ID!] } input UpsertRoleScopesInput { customRoleId: ID! resource: RoleScopeResourceType! """ For THREAD resource: array of scope conditions - one per primitive type """ scopes: [ScopeConditionInput!]! } type UpsertRoleScopesOutput { role: CustomRole error: MutationError } input CreateLabelTypeInput { name: String! icon: String color: String type: LabelTypeType description: String parentLabelTypeId: ID externalId: String isExcludedFromAi: Boolean } type CreateLabelTypeOutput { labelType: LabelType error: MutationError } input ArchiveLabelTypeInput { labelTypeId: ID! } type ArchiveLabelTypeOutput { labelType: LabelType error: MutationError } input UnarchiveLabelTypeInput { labelTypeId: ID! } type UnarchiveLabelTypeOutput { labelType: LabelType error: MutationError } input UpdateLabelTypeInput { labelTypeId: ID! name: StringInput icon: OptionalStringInput color: OptionalStringInput description: OptionalStringInput externalId: OptionalStringInput isExcludedFromAi: OptionalBooleanInput } type UpdateLabelTypeOutput { labelType: LabelType error: MutationError } input AddLabelsInput { labelTypeIds: [ID!]! threadId: ID! } type AddLabelsOutput { labels: [Label!]! thread: Thread error: MutationError } input AddLabelsToUserInput { labelTypeIds: [ID!]! entityId: ID! } type AddLabelsToUserOutput { labels: [Label!]! user: User error: MutationError } input RemoveLabelsInput { labelIds: [ID!]! } type RemoveLabelsOutput { thread: Thread error: MutationError } input RemoveLabelsFromUserInput { labelIds: [ID!]! } type RemoveLabelsFromUserOutput { labels: [Label!]! user: User error: MutationError } input DependsOnThreadFieldInput { threadFieldSchemaId: ID! threadFieldSchemaValue: String! } input OptionalDependsOnThreadFieldInput { value: DependsOnThreadFieldInput } input CreateThreadFieldSchemaInput { label: String! key: String! description: String! order: Int! type: ThreadFieldSchemaType! enumValues: [String!]! isRequired: Boolean! defaultStringValue: String defaultBooleanValue: Boolean defaultNumberValue: Float defaultDateValue: String isAiAutoFillEnabled: Boolean! isClientReadonly: Boolean dependsOnThreadField: DependsOnThreadFieldInput dependsOnLabelTypeIds: [ID!] } type CreateThreadFieldSchemaOutput { threadFieldSchema: ThreadFieldSchema error: MutationError } input OptionalDateTimeInput { value: String } input UpdateThreadFieldSchemaInput { threadFieldSchemaId: ID! label: StringInput description: StringInput order: Int enumValues: [String!] isRequired: Boolean defaultStringValue: OptionalStringInput defaultBooleanValue: OptionalBooleanInput defaultNumberValue: OptionalFloatInput defaultDateValue: OptionalDateTimeInput isAiAutoFillEnabled: Boolean isClientReadonly: Boolean dependsOnThreadField: OptionalDependsOnThreadFieldInput dependsOnLabelTypeIds: [ID!] } type UpdateThreadFieldSchemaOutput { threadFieldSchema: ThreadFieldSchema error: MutationError } input DeleteThreadFieldSchemaInput { threadFieldSchemaId: ID! } type DeleteThreadFieldSchemaOutput { error: MutationError } input DeleteEscalationPathInput { escalationPathId: ID! } type DeleteEscalationPathOutput { error: MutationError } input CreateEscalationPathInput { name: String! description: String steps: [EscalationPathStepInput!]! } type CreateEscalationPathOutput { escalationPath: EscalationPath error: MutationError } input UpdateEscalationPathInput { escalationPathId: ID! name: String description: String steps: [EscalationPathStepInput!] } type UpdateEscalationPathOutput { escalationPath: EscalationPath error: MutationError } input EscalationPathStepInput { type: EscalationPathStepType! userId: ID labelTypeId: ID } enum EscalationPathStepType { """ The step targets a specific user (human or machine). Provide `userId` in the step input. """ USER """ The step targets the owners of a label type. Provide `labelTypeId` in the step input. """ LABEL_TYPE } input ThreadFieldSchemaOrderInput { threadFieldSchemaId: ID! order: Int! } input ReorderThreadFieldSchemasInput { threadFieldSchemaOrders: [ThreadFieldSchemaOrderInput!]! } type ReorderThreadFieldSchemasOutput { threadFieldSchemas: [ThreadFieldSchema!] error: MutationError } input UpsertThreadFieldIdentifier { threadId: ID! key: String! } input UpsertThreadFieldInput { identifier: UpsertThreadFieldIdentifier! type: ThreadFieldSchemaType! stringValue: String booleanValue: Boolean numberValue: Float dateValue: String } input CreateThreadFieldOnThreadInput { key: String! type: ThreadFieldSchemaType! stringValue: String booleanValue: Boolean numberValue: Float dateValue: String } type UpsertThreadFieldOutput { threadField: ThreadField result: UpsertResult error: MutationError } type BulkUpsertThreadFieldResult { threadField: ThreadField result: UpsertResult } input BulkUpsertThreadFieldsInput { inputs: [UpsertThreadFieldInput!]! } type BulkUpsertThreadFieldsOutput { results: [BulkUpsertThreadFieldResult!]! error: MutationError } input DeleteThreadFieldIdentifier { threadId: ID! key: String! } input DeleteThreadFieldInput { identifier: DeleteThreadFieldIdentifier! } type DeleteThreadFieldOutput { error: MutationError } input CreateWorkflowRuleInput { name: String! """JSON-encoded payload of the rule definition.""" payload: String! } type CreateWorkflowRuleOutput { workflowRule: WorkflowRule error: MutationError } input UpdateWorkflowRuleInput { workflowRuleId: ID! name: StringInput """JSON-encoded payload of the rule definition.""" payload: StringInput order: IntInput } type UpdateWorkflowRuleOutput { workflowRule: WorkflowRule error: MutationError } input ToggleWorkflowRulePublishedInput { workflowRuleId: ID! } type ToggleWorkflowRulePublishedOutput { workflowRule: WorkflowRule error: MutationError } input DeleteWorkflowRuleInput { workflowRuleId: ID! } type DeleteWorkflowRuleOutput { error: MutationError } input TriggerWorkflowRuleInput { workflowRuleId: ID! threadId: ID! } type TriggerWorkflowRuleOutput { error: MutationError } input CreateWorkflowInput { name: String! """JSON-encoded trigger configuration.""" trigger: String! } type CreateWorkflowOutput { workflow: Workflow error: MutationError } input UpdateWorkflowInput { workflowId: ID! name: StringInput """JSON-encoded trigger configuration.""" trigger: StringInput startStepId: StringInput order: IntInput isPublished: BooleanInput } type UpdateWorkflowOutput { workflow: Workflow error: MutationError } input DeleteWorkflowInput { workflowId: ID! } type DeleteWorkflowOutput { error: MutationError } input TriggerWorkflowInput { workflowId: ID! threadId: ID! } type TriggerWorkflowOutput { workflowExecution: WorkflowExecution error: MutationError } input CreateWorkflowStepInput { workflowId: ID! """The type of step: CONDITION or ACTION.""" type: WorkflowStepType! """Optional name for the step.""" name: String """JSON-encoded payload containing the action or condition configuration.""" payload: String! """ Array of next step IDs. For ACTION: 1 element. For CONDITION: 2 elements (true, false). Use null for terminal/no branch. """ transitions: [ID]! """X position of this step on the canvas.""" positionX: Float """Y position of this step on the canvas.""" positionY: Float } type CreateWorkflowStepOutput { workflowStep: WorkflowStep error: MutationError } input UpdateWorkflowStepInput { stepId: ID! """Optional name for the step.""" name: StringInput """JSON-encoded payload containing the action or condition configuration.""" payload: StringInput """ Array of next step IDs. For ACTION: 1 element. For CONDITION: 2 elements (true, false). Use null for terminal/no branch. Full array replacement. """ transitions: [ID] """X position of this step on the canvas.""" positionX: Float """Y position of this step on the canvas.""" positionY: Float } type UpdateWorkflowStepOutput { workflowStep: WorkflowStep error: MutationError } input DeleteWorkflowStepInput { stepId: ID! } type DeleteWorkflowStepOutput { error: MutationError } """Input for a single step in the bulk upsert operation.""" input BulkUpsertWorkflowStepInput { """ Optional step ID - if provided and exists in this workflow, step will be updated. Otherwise created with new ID. """ stepId: ID """The type of step: CONDITION or ACTION.""" type: WorkflowStepType! """Optional name for the step.""" name: String """JSON-encoded payload containing the action or condition configuration.""" payload: String! """ Transition step IDs. ACTION steps: [nextStepId], CONDITION steps: [trueStepId, falseStepId]. Use null for terminal/no branch. """ transitions: [ID]! """X position of this step on the canvas.""" positionX: Float """Y position of this step on the canvas.""" positionY: Float } input BulkUpsertWorkflowStepsInput { workflowId: ID! """ The complete list of steps. Steps with existing IDs are updated, new IDs are created, missing IDs are deleted. """ steps: [BulkUpsertWorkflowStepInput!]! """ Optional: Set this as the workflow's startStepId. Use null to clear. If not provided, startStepId is unchanged. """ startStepId: ID """ Optional: JSON-encoded trigger configuration. If not provided, trigger is unchanged. """ trigger: StringInput } enum BulkUpsertWorkflowStepResult { CREATED UPDATED NOOP } type BulkUpsertWorkflowStepResultItem { workflowStep: WorkflowStep! result: BulkUpsertWorkflowStepResult! } type BulkUpsertWorkflowStepsOutput { """Results for each step in the input (same order as input).""" results: [BulkUpsertWorkflowStepResultItem!]! """IDs of steps that were deleted (existed before but not in input).""" deletedStepIds: [ID!]! """Updated workflow after operation.""" workflow: Workflow error: MutationError } input WorkspaceFileInput { workspaceFileId: ID } input CreateChatAppInput { name: String! logo: WorkspaceFileInput } type CreateChatAppOutput { chatApp: ChatApp error: MutationError } input UpdateChatAppInput { chatAppId: ID! name: StringInput logo: WorkspaceFileInput } type UpdateChatAppOutput { chatApp: ChatApp error: MutationError } input DeleteChatAppInput { chatAppId: ID! } type DeleteChatAppOutput { error: MutationError } input CreateChatAppSecretInput { chatAppId: ID! } type CreateChatAppSecretOutput { chatAppSecret: ChatAppSecret error: MutationError } input DeleteChatAppSecretInput { chatAppId: ID! } type DeleteChatAppSecretOutput { error: MutationError } """ Represents a simplified, high-level status of a thread link which can be used for filtering and sorting. Statuses from different external providers (e.g. Linear, Jira, Incident.io, Notion... etc) are mapped to one of these values. """ enum ThreadLinkStatus { """ Indicates that the linked entity is in pre-start state. This includes granular statuses like "Backlog", "Triage", "Unstarted", "Draft", "Planned", ...etc """ TODO """ Indicates that the linked entity is in a post-start state, but not yet finished. This includes granular statuses like "Started", "In Progress", "In Review", "Blocked", ...etc """ IN_PROGRESS """ Indicates that the linked issue is in a state that is considered finished. This includes granular statuses like "Completed", "Done", "Resolved", "Cancelled", ...etc. """ DONE """Unknown or unsupported future statuses from external providers.""" UNKNOWN } enum ThreadLinkLinkType { """ The thread is related to the linked entity without implying precedence or merge. """ RELATED_TO """ The thread has been merged into the linked Plain thread; the source thread is marked as done. """ MERGED_INTO """ The linked entity was superseded by this thread (system-managed; cannot be created directly). """ SUCCEEDED_BY } interface ThreadLink { id: ID! threadId: ID! """ The identifier of the linked entity in the external system (e.g. the Linear issue ID or Jira issue ID). """ sourceId: String! """ The type of the external system this link points to (e.g. `linear_issue`, `jira_issue`, `plain_thread`, `plain_task`). """ sourceType: String! title: String! description: String url: String! status: ThreadLinkStatus! """Describes the relationship between the thread and the linked entity.""" linkType: ThreadLinkLinkType! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } """ Represents the possible states of a Linear issue, sourced from the Linear API. Reference: https://studio.apollographql.com/public/Linear-API/variant/current/schema/reference/objects/WorkflowState#type """ enum LinearIssueStateType { TRIAGE BACKLOG UNSTARTED STARTED COMPLETED CANCELLED """Placeholder for unknown or unsupported future states from Linear.""" UNKNOWN } type LinearIssueState { type: LinearIssueStateType! label: String! color: String! } type LinearIssueThreadLink implements ThreadLink { id: ID! title: String! description: String url: String! status: ThreadLinkStatus! linkType: ThreadLinkLinkType! threadId: ID! sourceId: String! sourceType: String! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! linearIssueId: ID! """ The short human-readable identifier for the Linear issue (e.g. `ENG-123`). """ linearIssueIdentifier: String! linearIssueState: LinearIssueState! linearIssueCreatedAt: DateTime! linearIssueUpdatedAt: DateTime! linearIssueUrl: String! @deprecated(reason: "Use url instead.") } type JiraIssueType { name: String! iconUrl: String! } type JiraIssueThreadLink implements ThreadLink { id: ID! title: String! description: String url: String! status: ThreadLinkStatus! threadId: ID! sourceId: String! sourceType: String! linkType: ThreadLinkLinkType! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! jiraIssueId: ID! """The human-readable Jira issue key (e.g. `PROJ-456`).""" jiraIssueKey: String! jiraIssueType: JiraIssueType! } type PlainThreadThreadLink implements ThreadLink { id: ID! title: String! description: String url: String! status: ThreadLinkStatus! threadId: ID! sourceId: String! sourceType: String! linkType: ThreadLinkLinkType! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! plainThreadId: ID! """ The detailed status of the linked Plain thread at the time it was last synced. """ plainThreadStatusDetailType: StatusDetailType! } type PlainTaskThreadLink implements ThreadLink { id: ID! title: String! description: String url: String! status: ThreadLinkStatus! threadId: ID! sourceId: String! sourceType: String! linkType: ThreadLinkLinkType! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! plainTaskId: ID! } type GenericThreadLink implements ThreadLink { id: ID! title: String! description: String url: String! status: ThreadLinkStatus! threadId: ID! sourceId: String! sourceType: String! """ The granular, provider-specific status for this link, present when the issue tracker provides richer status data beyond the normalized `status` field. """ sourceStatus: ThreadLinkSourceStatus linkType: ThreadLinkLinkType! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } input LinearIssueThreadLinkInput { linearIssueId: ID! linearIssueUrl: String! } input PlainThreadLinkInput { plainThreadId: ID! linkType: ThreadLinkLinkType } input PlainTaskThreadLinkInput { plainTaskId: ID! } input JiraIssueThreadLinkInput { jiraIssueId: ID! } input CreateThreadLinkInput { threadId: ID! sourceId: String sourceType: String linearIssue: LinearIssueThreadLinkInput plainThread: PlainThreadLinkInput plainTask: PlainTaskThreadLinkInput jiraIssue: JiraIssueThreadLinkInput } type CreateThreadLinkOutput { threadLink: ThreadLink error: MutationError } input DeleteThreadLinkInput { threadLinkId: ID! } type DeleteThreadLinkOutput { error: MutationError } type ThreadLinkEdge { cursor: String! node: ThreadLink! } type ThreadLinkConnection { edges: [ThreadLinkEdge!]! pageInfo: PageInfo! totalCount: Int! } type ThreadLinkGroupAggregateMetrics { totalCount: Int! } type ThreadLinkGroupSingleTierMetrics { tier: Tier! metrics: ThreadLinkGroupAggregateMetrics! } type ThreadLinkGroupSingleCompanyMetrics { company: Company! metrics: ThreadLinkGroupAggregateMetrics! } type ThreadLinkGroupTierMetrics { byTier: [ThreadLinkGroupSingleTierMetrics!]! """Metrics when the thread is not associated with any tier.""" noTier: ThreadLinkGroupAggregateMetrics! } type ThreadLinkGroupCompanyMetrics { byCompany: [ThreadLinkGroupSingleCompanyMetrics!]! """Metrics when the thread is not associated with any company.""" noCompany: ThreadLinkGroupAggregateMetrics! } type ThreadLinkGroup { id: ID! """All threads that are linked to this group's external entity.""" threads(first: Int, after: String, last: Int, before: String): ThreadConnection! """The individual thread link records that belong to this group.""" threadLinks(first: Int, after: String, last: Int, before: String): ThreadLinkConnection! """ Breakdown of linked thread counts by tier, including threads with no tier. """ tierMetrics: ThreadLinkGroupTierMetrics! """ Breakdown of linked thread counts by company, including threads with no company. """ companyMetrics: ThreadLinkGroupCompanyMetrics! """ The default rank of the thread link group which takes into account only active groups. This rank is not affected by input filters. """ defaultViewRank: Int """ The current rank of the thread link group considering groups which match the non-ID input filters. """ currentViewRank: Int! } type ThreadLinkGroupEdge { cursor: String! node: ThreadLinkGroup! } type ThreadLinkGroupConnection { edges: [ThreadLinkGroupEdge!]! pageInfo: PageInfo! } input ThreadLinkGroupFilter { """Defaults to [TODO, IN_PROGRESS]""" statuses: [ThreadLinkStatus!] threadLinkGroupIds: [ID!] companyIds: [ID!] tierIds: [ID!] } input CreateNoteInput { customerId: ID! threadId: ID text: String! markdown: String attachmentIds: [ID!] } type CreateNoteOutput { note: Note error: MutationError } input DeleteNoteInput { noteId: ID! } type DeleteNoteOutput { note: Note error: MutationError } input UpdateNoteInput { noteId: ID! text: String! markdown: String attachmentIds: [ID!] } type UpdateNoteOutput { note: Note error: MutationError } input ThreadsDisplayOptionsInput { hasStatus: Boolean! hasCustomer: Boolean! hasCompany: Boolean! hasPreviewText: Boolean! hasTier: Boolean! hasCustomerGroups: Boolean! hasLabels: Boolean! hasLinearIssues: Boolean @deprecated(reason: "Use hasIssueTrackerIssues instead") hasJiraIssues: Boolean @deprecated(reason: "Use hasIssueTrackerIssues instead") hasLinkedThreads: Boolean! hasServiceLevelAgreements: Boolean! hasChannels: Boolean! hasLastUpdated: Boolean! hasAssignees: Boolean! hasRef: Boolean! hasIssueTrackerIssues: Boolean hasTasks: Boolean hasThreadFieldKeys: [String!] hasTenants: Boolean hasTenantFieldExternalIds: [ID!] } input SavedThreadsViewFilterInput { statuses: [ThreadStatus!]! statusDetails: [StatusDetailType!]! priorities: [Int!]! assignedToUser: [ID!]! participants: [ID!]! customerGroups: [ID!]! companies: [ID!]! tenants: [ID!]! tiers: [ID!]! labelTypeIds: [ID!]! messageSource: [MessageSource!]! supportEmailAddresses: [String!]! slaTypes: [String!]! slaStatuses: [String!]! threadFields: [ThreadFieldFilter!]! tenantFields: [TenantFieldFilter!]! threadLinkGroupIds: [ID!]! threadLinkSources: [ThreadLinkSourceFilter!]! createdAtFilter: DatetimeFilter surveyResponse: SurveyResponseFilter sort: ThreadsSort! displayOptions: ThreadsDisplayOptionsInput! groupBy: ThreadsGroupBy! layout: ThreadsLayout! and: [SavedThreadsViewFilterInput!] or: [SavedThreadsViewFilterInput!] not: SavedThreadsViewFilterInput } input CreateSavedThreadsViewInput { name: String! icon: String! color: String! threadsFilter: SavedThreadsViewFilterInput! isHidden: Boolean } type CreateSavedThreadsViewOutput { savedThreadsView: SavedThreadsView error: MutationError } input DeleteSavedThreadsViewInput { savedThreadsViewId: ID! } type DeleteSavedThreadsViewOutput { error: MutationError } input UpdateSavedThreadsViewInput { savedThreadsViewId: ID! name: StringInput icon: StringInput color: StringInput threadsFilter: SavedThreadsViewFilterInput isHidden: BooleanInput } type UpdateSavedThreadsViewOutput { savedThreadsView: SavedThreadsView error: MutationError } input CreateMyFavoritePageInput { key: String! } type CreateMyFavoritePageOutput { favoritePage: FavoritePage error: MutationError } input DeleteMyFavoritePageInput { favoritePageId: ID! } type DeleteMyFavoritePageOutput { error: MutationError } input CreateSnippetInput { name: String! text: String! markdown: String """Used to group snippets, only accepts alphanumeric characters""" path: String } type CreateSnippetOutput { snippet: Snippet error: MutationError } input DeleteSnippetInput { snippetId: ID! } type DeleteSnippetOutput { snippet: Snippet error: MutationError } input UpdateSnippetInput { snippetId: ID! name: StringInput text: StringInput markdown: StringInput """Used to group snippets, only accepts alphanumeric characters""" path: OptionalStringInput } type UpdateSnippetOutput { snippet: Snippet error: MutationError } input UpsertTeamSettingsInput { labelTypeId: ID! isRoundRobinEnabled: Boolean roundRobinMaxCapacity: Int } type UpsertTeamSettingsOutput { teamSettings: TeamSettings error: MutationError } """Only one of the fields can be set.""" input CreateTaskAssignedToInput { userId: ID machineUserId: ID } input CreateTaskInput { title: String! description: String status: TaskStatus priority: Int companyId: ID tenantId: ID assignedTo: CreateTaskAssignedToInput } type CreateTaskOutput { task: Task error: MutationError } """Only one of the fields can be set.""" input UpdateTaskAssignedToInput { userId: ID machineUserId: ID } input UpdateTaskInput { taskId: ID! title: String description: String status: TaskStatus priority: Int """ Only one of companyId or tenantId can be set. Cannot specify both. Setting companyId will unset tenantId if it already exists. """ companyId: ID """ Only one of companyId or tenantId can be set. Cannot specify both. Setting tenantId will unset companyId if it already exists. """ tenantId: ID assignedTo: UpdateTaskAssignedToInput } type UpdateTaskOutput { task: Task error: MutationError } input DeleteTaskInput { taskId: ID! } type DeleteTaskOutput { task: Task error: MutationError } input SendChatInput { customerId: ID! text: String attachmentIds: [ID!] threadId: ID """ When provided, this will override the timestamp of the chat. Useful when backfilling messages. Must be in ISO 8601 format (e.g. 2024-10-28T18:30:00Z). """ timestamp: String } type SendChatOutput { chat: Chat error: MutationError } input SendCustomerChatInput { customerId: ID! text: String attachmentIds: [ID!] threadId: ID! """ When provided, this will override the timestamp of the chat. Useful when backfilling messages. Must be in ISO 8601 format (e.g. 2024-10-28T18:30:00Z). """ timestamp: String } type SendCustomerChatOutput { chat: Chat error: MutationError } input ChangeUserStatusInput { userId: ID! status: UserStatus! } type ChangeUserStatusOutput { user: User error: MutationError } input UpdateMyUserInput { fullName: OptionalStringInput publicName: OptionalStringInput avatar: WorkspaceFileInput } type UpdateMyUserOutput { user: User error: MutationError } input UpdateUserDefaultSavedThreadsViewInput { userId: ID! savedViewId: ID } type UpdateUserDefaultSavedThreadsViewOutput { user: User error: MutationError } input UpdateWorkspaceInput { publicName: StringInput name: StringInput logo: WorkspaceFileInput domainNames: [String!] } type UpdateWorkspaceOutput { workspace: Workspace error: MutationError } input DeleteUserInput { userId: ID! } type DeleteUserOutput { error: MutationError } type DnsRecord { type: String! name: String! value: String! isVerified: Boolean! verifiedAt: DateTime lastCheckedAt: DateTime } type WorkspaceEmailDomainSettings { domainName: String! supportEmailAddress: String! """ The list of alternate email addresses that can be used to send emails to and from the workspace. Limited to 5. e.g. [info@plain.com, help@plain.com]. """ alternateSupportEmailAddresses: [String!]! """ Whether the inbound email forwarding rule has been confirmed as set up. """ isForwardingConfigured: Boolean! """ The Plain-provided email address that inbound emails should be forwarded to in order to reach this workspace. """ inboundForwardingEmail: String! """ Whether the DKIM and return-path DNS records have been verified as correctly configured. """ isDomainConfigured: Boolean! """ The DKIM DNS TXT record that must be added to the domain to authenticate outbound emails. """ dkimDnsRecord: DnsRecord! """ The return-path (bounce) DNS CNAME record that must be added to the domain to handle bounces and complaints. """ returnPathDnsRecord: DnsRecord! } type WorkspaceEmailSettings { """Whether the email channel is enabled for this workspace.""" isEnabled: Boolean! workspaceEmailDomainSettings: WorkspaceEmailDomainSettings """ Email addresses that are automatically BCC'd on every outbound email sent from this workspace. """ bccEmailAddresses: [String!]! } input AddWorkspaceAlternateSupportEmailAddressInput { alternateSupportEmailAddress: String! } type AddWorkspaceAlternateSupportEmailAddressOutput { workspaceEmailDomainSettings: WorkspaceEmailDomainSettings error: MutationError } input RemoveWorkspaceAlternateSupportEmailAddressInput { alternateSupportEmailAddress: String! } type RemoveWorkspaceAlternateSupportEmailAddressOutput { workspaceEmailDomainSettings: WorkspaceEmailDomainSettings error: MutationError } input CreateWorkspaceEmailDomainSettingsInput { supportEmailAddress: String! } type CreateWorkspaceEmailDomainSettingsOutput { workspaceEmailDomainSettings: WorkspaceEmailDomainSettings error: MutationError } type DeleteWorkspaceEmailDomainSettingsOutput { error: MutationError } input VerifyWorkspaceEmailForwardingSettingsInput { isForwardingConfigured: Boolean! } type VerifyWorkspaceEmailForwardingSettingsOutput { workspaceEmailDomainSettings: WorkspaceEmailDomainSettings error: MutationError } type VerifyWorkspaceEmailDnsSettingsOutput { workspaceEmailDomainSettings: WorkspaceEmailDomainSettings error: MutationError } input UpdateWorkspaceEmailSettingsInput { isEnabled: Boolean bccEmailAddresses: [String!] } type UpdateWorkspaceEmailSettingsOutput { workspaceEmailSettings: WorkspaceEmailSettings error: MutationError } """Workspace-level settings controlling the chat channel.""" type WorkspaceChatSettings { """Whether the live-chat channel is enabled for this workspace.""" isEnabled: Boolean! } input EmailParticipantInput { name: String email: String! } input SendNewEmailInput { customerId: ID! subject: String! textContent: String! markdownContent: String attachmentIds: [ID!] additionalRecipients: [EmailParticipantInput!] hiddenRecipients: [EmailParticipantInput!] """ Optional field for alternate from email address. If provided, it will be used as the from address in the email. It must match one of the workspace support email addresses (default or alternate). """ fromAlternateSupportEmail: EmailParticipantInput """ If provided this will add the new email to an existing thread. If not provided, a new thread will be created. """ threadId: ID """ If true, the email will be categorized as USER_HIDDEN, meaning it will not be visible to the user in the UI. """ isHiddenFromUser: Boolean } input SendBulkEmailInput { threadIds: [ID!]! textContent: String! markdownContent: String } input ReplyToEmailInput { customerId: ID @deprecated(reason: "Use inReplyToEmailId instead.") inReplyToEmailId: ID! subject: String textContent: String! markdownContent: String attachmentIds: [ID!] additionalRecipients: [EmailParticipantInput!] hiddenRecipients: [EmailParticipantInput!] """ Optional field for alternate from email address. If provided, it will be used as the from address in the email. It must match one of the workspace support email addresses (default or alternate). """ fromAlternateSupportEmail: EmailParticipantInput """ If true, the email will be categorized as USER_HIDDEN, meaning it will not be visible to the user in the UI. """ isHiddenFromUser: Boolean } enum EmailCategory { MESSAGING CUSTOMER_SURVEY UNREAD_CHAT_MESSAGES THREAD_DISCUSSION USER_HIDDEN } type Email { id: ID! thread: Thread customer: Customer! inReplyToEmailId: ID from: EmailParticipant! to: EmailParticipant! subject: String textContent: String markdownContent: String attachments: [Attachment!]! additionalRecipients: [EmailParticipant!]! hiddenRecipients: [EmailParticipant!]! category: EmailCategory! threadDiscussionId: ID createdAt: DateTime! createdBy: Actor! updatedAt: DateTime! updatedBy: Actor! } type SendNewEmailOutput { email: Email error: MutationError } type ReplyToEmailOutput { email: Email error: MutationError } type SendBulkEmailOutput { error: MutationError } input CreateEmailPreviewUrlInput { emailId: ID! customerId: ID! } type EmailPreviewUrl { previewUrl: String! expiresAt: DateTime! } type CreateEmailPreviewUrlOutput { emailPreviewUrl: EmailPreviewUrl error: MutationError } input CreateAttachmentDownloadUrlInput { attachmentId: ID! } type AttachmentDownloadUrl { attachment: Attachment! downloadUrl: String! expiresAt: DateTime! } enum AttachmentVirusScanResult { """The attachment is clean and safe to download.""" CLEAN """The attachment is infected and should not be downloaded.""" INFECTED """The virus scan failed.""" FAILED """The virus scan is still pending.""" PENDING } type CreateAttachmentDownloadUrlOutput { attachmentDownloadUrl: AttachmentDownloadUrl """ The result of the virus scan on this attachment. If this is null, it means that your workspace does not have virus scan checks enabled. """ attachmentVirusScanResult: AttachmentVirusScanResult error: MutationError } enum AttachmentType { EMAIL """ Attachment for a custom timeline entry created via `createCustomerEvent` or `createThreadEvent`. """ CUSTOM_TIMELINE_ENTRY CHAT SLACK """ Attachment for a message in a thread discussion (internal team discussion). """ THREAD_DISCUSSION MS_TEAMS DISCORD NOTE } input CreateAttachmentUploadUrlInput { customerId: ID! fileName: String! fileSizeBytes: Int! attachmentType: AttachmentType! } type UploadFormData { key: String! value: String! } type AttachmentUploadUrl { attachment: Attachment! uploadFormUrl: String! uploadFormData: [UploadFormData!]! expiresAt: DateTime! } type CreateAttachmentUploadUrlOutput { attachmentUploadUrl: AttachmentUploadUrl error: MutationError } input ComponentTextInput { """ The text content, with markdown support. Must be between 1 and 10000 characters. """ text: String! textSize: ComponentTextSize textColor: ComponentTextColor color: ComponentTextColor @deprecated(reason: "use textColor instead") size: ComponentTextSize @deprecated(reason: "use textSize instead") } input ComponentPlainTextInput { """ The text content, without markdown support. Must be between 1 and 10000 characters. """ plainText: String! plainTextSize: ComponentPlainTextSize plainTextColor: ComponentPlainTextColor } input ComponentLinkButtonInput { """Must be a valid URL.""" linkButtonUrl: String """The label of the button. Maximum 500 characters.""" linkButtonLabel: String url: String @deprecated(reason: "use linkButtonUrl instead") label: String @deprecated(reason: "use linkButtonLabel instead") } input ComponentWorkflowButtonWorkflowIdentifierInput { workflowId: ID workflowKey: String } input ComponentWorkflowButtonInput { workflowButtonWorkflowIdentifier: ComponentWorkflowButtonWorkflowIdentifierInput! """The label of the button. Maximum 500 characters.""" workflowButtonLabel: String! } input ComponentBadgeInput { """The label of the badge. Maximum 500 characters.""" badgeLabel: String! badgeColor: ComponentBadgeColor } input ComponentCopyButtonInput { """The value to be copied. Maximum 1000 characters.""" copyButtonValue: String! """The tooltip label. Maximum 500 characters.""" copyButtonTooltipLabel: String } input ComponentDateTimeInput { dateTimeIso8601: String! } """ Identifies a Plain user or machine user. Exactly one field must be set. """ input UserIdentifierInput { userId: ID emailAddress: String machineUserId: ID } input ComponentUserInput { userIdentifier: UserIdentifierInput! } input ComponentRowInput { """Must contain at least one component.""" rowMainContent: [ComponentRowContentInput!]! rowAsideContent: [ComponentRowContentInput!]! } input ComponentContainerInput { """Must contain at least one component.""" containerContent: [ComponentContainerContentInput!]! } input ComponentDividerInput { dividerSpacingSize: ComponentDividerSpacingSize spacingSize: ComponentDividerSpacingSize @deprecated(reason: "use dividerSpacingSize instead") } input ComponentSpacerInput { """ Required input, will be made required after deprecated fields are removed. """ spacerSize: ComponentSpacerSize size: ComponentSpacerSize @deprecated(reason: "use spacerSize instead") } input ComponentContainerContentInput { componentText: ComponentTextInput componentPlainText: ComponentPlainTextInput componentDivider: ComponentDividerInput componentLinkButton: ComponentLinkButtonInput componentWorkflowButton: ComponentWorkflowButtonInput componentSpacer: ComponentSpacerInput componentBadge: ComponentBadgeInput componentCopyButton: ComponentCopyButtonInput componentDateTime: ComponentDateTimeInput componentUser: ComponentUserInput componentRow: ComponentRowInput } input ComponentRowContentInput { componentText: ComponentTextInput componentPlainText: ComponentPlainTextInput componentDivider: ComponentDividerInput componentLinkButton: ComponentLinkButtonInput componentWorkflowButton: ComponentWorkflowButtonInput componentSpacer: ComponentSpacerInput componentBadge: ComponentBadgeInput componentCopyButton: ComponentCopyButtonInput componentDateTime: ComponentDateTimeInput componentUser: ComponentUserInput } input ComponentInput { componentText: ComponentTextInput componentPlainText: ComponentPlainTextInput componentDivider: ComponentDividerInput componentLinkButton: ComponentLinkButtonInput componentWorkflowButton: ComponentWorkflowButtonInput componentSpacer: ComponentSpacerInput componentBadge: ComponentBadgeInput componentCopyButton: ComponentCopyButtonInput componentDateTime: ComponentDateTimeInput componentUser: ComponentUserInput componentRow: ComponentRowInput componentContainer: ComponentContainerInput } input EventComponentInput { componentText: ComponentTextInput componentPlainText: ComponentPlainTextInput componentDivider: ComponentDividerInput componentLinkButton: ComponentLinkButtonInput componentWorkflowButton: ComponentWorkflowButtonInput componentSpacer: ComponentSpacerInput componentBadge: ComponentBadgeInput componentCopyButton: ComponentCopyButtonInput componentDateTime: ComponentDateTimeInput componentUser: ComponentUserInput componentRow: ComponentRowInput componentContainer: ComponentContainerInput } enum UpsertResult { UPDATED CREATED NOOP } input EmailAddressInput { email: String! isVerified: Boolean! } input UpsertCustomerIdentifierInput { externalId: ID emailAddress: String customerId: ID } input UpsertCustomerOnCreateInput { externalId: ID fullName: String! shortName: String email: EmailAddressInput! customerGroupIdentifiers: [CustomerGroupIdentifier!] tenantIdentifiers: [TenantIdentifierInput!] } input UpsertCustomerOnUpdateInput { externalId: OptionalStringInput fullName: StringInput shortName: OptionalStringInput email: EmailAddressInput } input UpsertCustomerInput { identifier: UpsertCustomerIdentifierInput! onCreate: UpsertCustomerOnCreateInput! onUpdate: UpsertCustomerOnUpdateInput! } type UpsertCustomerOutput { result: UpsertResult customer: Customer error: MutationError } input CreateMachineUserInput { publicName: String! fullName: String! description: String """The type of machine user. Defaults to API_USER if not specified.""" type: MachineUserType avatar: WorkspaceFileInput } type CreateMachineUserOutput { machineUser: MachineUser error: MutationError } input DeleteMachineUserInput { machineUserId: ID! } type DeleteMachineUserOutput { machineUser: MachineUser error: MutationError } input UpdateMachineUserInput { machineUserId: ID! fullName: StringInput publicName: StringInput description: StringInput """The type of machine user. Defaults to API_USER if not specified.""" type: MachineUserType avatar: WorkspaceFileInput } type UpdateMachineUserOutput { machineUser: MachineUser error: MutationError } input CreateApiKeyInput { machineUserId: ID! description: String permissions: [String!]! } type CreateApiKeyOutput { apiKey: ApiKey """ The plaintext API key secret (format: `plainApiKey_…`). This value is only returned once at creation time and cannot be retrieved later — store it securely immediately. """ apiKeySecret: String error: MutationError } input UpdateApiKeyInput { apiKeyId: ID! description: String permissions: [String!]! } type UpdateApiKeyOutput { apiKey: ApiKey error: MutationError } input DeleteApiKeyInput { apiKeyId: ID! } type DeleteApiKeyOutput { apiKey: ApiKey error: MutationError } input DeleteCustomerInput { customerId: ID! } type DeleteCustomerOutput { error: MutationError } input DeleteThreadInput { threadId: ID! } type DeleteThreadOutput { error: MutationError } input CreateCustomerEventInput { """The customer id of the customer that the event is for.""" customerIdentifier: CustomerIdentifierInput! """ The external ID of this event. You can use this field to store your own unique identifier for this event. This must be unique. """ externalId: ID """The title of the event.""" title: String! """The components used to create the event's contents.""" components: [EventComponentInput!]! """ When provided, this will override the timestamp of the event. Useful when backfilling events. Must be in ISO 8601 format (e.g. 2024-10-28T18:30:00Z). """ timestamp: String """ Whether this event should be rendered collapsed by default in the timeline. Defaults to false. """ isCollapsed: Boolean } type CreateCustomerEventOutput { customerEvent: CustomerEvent error: MutationError } input CreateThreadEventInput { """The thread id of the thread that the event is for.""" threadId: ID! """ The external ID of this event. You can use this field to store your own unique identifier for this event. This must be unique. """ externalId: ID """The title of the event.""" title: String! """The components used to create the event's contents.""" components: [EventComponentInput!]! """ When provided, this will override the timestamp of the event. Useful when backfilling events. Must be in ISO 8601 format (e.g. 2024-10-28T18:30:00Z). """ timestamp: String """ Whether this event should be rendered collapsed by default in the timeline. Defaults to false. """ isCollapsed: Boolean } type CreateThreadEventOutput { threadEvent: ThreadEvent error: MutationError } """ The channel through which an inbound thread was created. Used to restrict which autoresponders are eligible to fire. """ enum AutoresponderMessageSource { """Thread was created from an inbound email.""" EMAIL """ Thread was created via the Plain API (e.g. a contact form or programmatic integration). """ API """Thread was created via a Plain Chat widget.""" CHAT """Thread was created from a Slack channel integration.""" SLACK """Thread was created from a Microsoft Teams integration.""" MS_TEAMS """Thread was created from a Discord integration.""" DISCORD } input AutoresponderConditionInput { tierId: ID isOutsideBusinessHours: Boolean supportEmailAddresses: [String!] labelTypeIds: [ID!] priorities: [Int!] } input CreateAutoresponderInput { name: String! order: Int! textContent: String! markdownContent: String isEnabled: Boolean! messageSources: [AutoresponderMessageSource!]! conditions: [AutoresponderConditionInput!]! responseDelaySeconds: Int } type CreateAutoresponderOutput { autoresponder: Autoresponder error: MutationError } input UpdateAutoresponderInput { autoresponderId: ID! name: StringInput order: IntInput textContent: StringInput markdownContent: OptionalStringInput isEnabled: BooleanInput messageSources: [AutoresponderMessageSource!] conditions: [AutoresponderConditionInput!] responseDelaySeconds: IntInput } type UpdateAutoresponderOutput { autoresponder: Autoresponder error: MutationError } input DeleteAutoresponderInput { autoresponderId: ID! } type DeleteAutoresponderOutput { autoresponder: Autoresponder error: MutationError } input AutoresponderOrderInput { autoresponderId: ID! order: Int! } input ReorderAutorespondersInput { autorespondersOrder: [AutoresponderOrderInput!]! } type ReorderAutorespondersOutput { autoresponders: [Autoresponder!] error: MutationError } """Condition that matches threads belonging to a specific tier.""" type AutoresponderTierCondition { """ The ID of the tier that must be assigned to the thread for this condition to be satisfied. """ tierId: ID! } """ Condition that matches threads based on whether the thread was created outside configured business hours. """ type AutoresponderBusinessHoursCondition { """ When true, the autoresponder fires only outside business hours; when false, only during business hours. """ isOutsideBusinessHours: Boolean! } """ Condition that restricts the autoresponder to threads received on specific support email addresses. """ type AutoresponderSupportEmailsCondition { """ One or more workspace support email addresses. The thread's inbound email must have been sent to one of these addresses for the condition to be satisfied. """ supportEmailAddresses: [String!]! } """ Condition that matches threads that have at least one of the specified labels applied. """ type AutoresponderLabelCondition { """IDs of the label types that must be present on the thread.""" labelTypeIds: [ID!]! } """ Condition that matches threads at one of the specified priority levels. """ type AutoresponderPrioritiesCondition { """ Numeric priority values that the thread's priority must match. Priority integers correspond to the ThreadPriority enum values. """ priorities: [Int!]! } union AutoresponderCondition = AutoresponderTierCondition | AutoresponderBusinessHoursCondition | AutoresponderSupportEmailsCondition | AutoresponderLabelCondition | AutoresponderPrioritiesCondition type Autoresponder { id: ID! name: String! """ Priority order used to determine which autoresponder fires first when multiple autoresponders match a thread. Lower values are evaluated first. """ order: Int! """ The message sources (e.g. EMAIL, CHAT) this autoresponder applies to. A thread must be created from one of these sources for the autoresponder to be eligible. """ messageSources: [AutoresponderMessageSource!]! """ Additional conditions that must all be satisfied for the autoresponder to fire, such as the thread being in a specific tier, outside business hours, or having a particular label. """ conditions: [AutoresponderCondition!]! """Plain-text version of the reply body sent to the customer.""" textContent: String! """ Optional Markdown version of the reply body. When present, it is used instead of textContent for channels that support rich formatting. """ markdownContent: String """ Whether this autoresponder is active. Disabled autoresponders are stored but never triggered. """ isEnabled: Boolean! """ Number of seconds to wait after thread creation before sending the auto-reply. A value of 0 means the reply is sent immediately. """ responseDelaySeconds: Int! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } type AutoresponderConnection { edges: [AutoresponderEdge!]! pageInfo: PageInfo! } type AutoresponderEdge { cursor: String! node: Autoresponder! } type CsatCustomerSurveyTemplate { """The survey type identifier. Currently always `CSAT`.""" type: String! """The question text displayed to the customer when the survey is sent.""" questionText: String! } union CustomerSurveyTemplate = CsatCustomerSurveyTemplate type CustomerSurvey { id: ID! name: String! """ The survey template that defines the type and question text sent to the customer (e.g. a CSAT template). """ template: CustomerSurveyTemplate! """ Targeting conditions that restrict which threads this survey is sent on. When empty, the survey applies to all threads. """ conditions: [CustomerSurveyCondition!]! """ Whether the survey is active and will be sent to customers. Disabled surveys are never triggered. """ isEnabled: Boolean! """ How many minutes after a thread is resolved before the survey is sent to the customer. """ responseDelayMinutes: Int! """ Minimum number of days that must pass before the same customer can be sent this survey again. """ customerIntervalDays: Int! """ Zero-based integer controlling the display and evaluation order of surveys relative to one another. """ order: Int! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } type CustomerSurveyTiersCondition { tierIds: [ID!]! } type CustomerSurveySupportEmailsCondition { supportEmailAddresses: [String!]! } type CustomerSurveyLabelCondition { labelTypeIds: [ID!]! } type CustomerSurveyPrioritiesCondition { priorities: [Int!]! } type CustomerSurveyMessageSourceCondition { messageSource: [MessageSource!]! } union CustomerSurveyCondition = CustomerSurveyTiersCondition | CustomerSurveySupportEmailsCondition | CustomerSurveyLabelCondition | CustomerSurveyPrioritiesCondition | CustomerSurveyMessageSourceCondition input CustomerSurveyConditionInput { tierIds: [ID!] supportEmailAddresses: [String!] labelTypeIds: [ID!] priorities: [Int!] messageSource: [MessageSource!] } input CsatCustomerSurveyTemplateInput { type: String! questionText: String! } input CustomerSurveyTemplateInput { csatTemplate: CsatCustomerSurveyTemplateInput } input CreateCustomerSurveyInput { name: String! conditions: [CustomerSurveyConditionInput!] template: CustomerSurveyTemplateInput! isEnabled: Boolean! responseDelayMinutes: Int customerIntervalDays: Int order: Int } type CreateCustomerSurveyOutput { customerSurvey: CustomerSurvey error: MutationError } input UpdateCustomerSurveyInput { customerSurveyId: ID! name: StringInput conditions: [CustomerSurveyConditionInput!] template: CustomerSurveyTemplateInput order: Int isEnabled: BooleanInput responseDelayMinutes: IntInput customerIntervalDays: IntInput } type UpdateCustomerSurveyOutput { customerSurvey: CustomerSurvey error: MutationError } input DeleteCustomerSurveyInput { customerSurveyId: ID! } type DeleteCustomerSurveyOutput { error: MutationError } input CustomerSurveyOrderInput { customerSurveyId: ID! order: Int! } input ReorderCustomerSurveysInput { customerSurveyOrders: [CustomerSurveyOrderInput!]! } type ReorderCustomerSurveysOutput { customerSurveys: [CustomerSurvey!] error: MutationError } input UpdateInternalNotificationsInput { notificationIds: [ID!]! readAt: OptionalStringInput archivedAt: StringInput } type UpdateInternalNotificationsOutput { internalNotifications: [InternalNotification!]! error: MutationError } type Mutation { """ Creates or updates the user account for the currently authenticated user. Idempotent: calling this multiple times with the same identity will update the existing account rather than creating a duplicate. Used during the initial onboarding flow to set the user's display names. """ createUserAccount(input: CreateUserAccountInput!): CreateUserAccountOutput! """ Manually set a workspace member's availability status (ONLINE, OFFLINE, or AWAY). This overrides any automatic status derived from working hours until the next scheduled working-hours transition. Requires the `userStatus:edit` permission. """ changeUserStatus(input: ChangeUserStatusInput!): ChangeUserStatusOutput! """ Updates profile fields (display name, short name, or avatar) for the currently authenticated human user. Only the fields provided are changed; omitted fields are left unchanged. Not available to machine users. """ updateMyUser(input: UpdateMyUserInput!): UpdateMyUserOutput! """ Sets or clears the default saved threads view for a specific user. Pass null for savedViewId to remove the default. This controls which thread view the user sees when they first open the inbox. """ updateUserDefaultSavedThreadsView(input: UpdateUserDefaultSavedThreadsViewInput!): UpdateUserDefaultSavedThreadsViewOutput! """ Create a new Plain workspace. The authenticated user becomes the owner of the new workspace. Requires a unique internal name and a public-facing display name. """ createWorkspace(input: CreateWorkspaceInput!): CreateWorkspaceOutput! """ Update workspace settings such as the display name, logo, or allowed email domain names. At least one field must be provided. Domain names are stored in lowercase. """ updateWorkspace(input: UpdateWorkspaceInput!): UpdateWorkspaceOutput! """ Send a workspace invitation to a user by email. Specify either a built-in roleKey (e.g. SUPPORT, ADMIN) or a customRoleId to control the permissions they receive on joining. Sends an invitation email to the provided address. """ inviteUserToWorkspace(input: InviteUserToWorkspaceInput!): InviteUserToWorkspaceOutput! """ Accept a workspace invitation using its ID. The authenticated user joins the workspace and is assigned the role specified in the invite. The invite is marked as accepted and can no longer be used. """ acceptWorkspaceInvite(input: AcceptWorkspaceInviteInput!): AcceptWorkspaceInviteOutput! """ Cancel and delete a pending workspace invite. The invited user will no longer be able to accept it. The deleted invite is returned. """ deleteWorkspaceInvite(input: DeleteWorkspaceInviteInput!): DeleteWorkspaceInviteOutput! """ Permanently remove a workspace member. The user's threads remain but are unassigned. Returns an error if the user is the only owner of the workspace — another owner must exist before deletion is allowed. Requires the `user:delete` permission. """ deleteUser(input: DeleteUserInput!): DeleteUserOutput! """ Assigns a role to a user in the workspace. Supply exactly one of `roleKey` (for built-in roles) or `customRoleId` (for custom roles) — not both. You can also set `usingBillingRotaSeat` to control whether the user occupies a billable seat on the billing rota. Requires the `roles:assign` permission. """ assignRolesToUser(input: AssignRolesToUserInput!): AssignRolesToUserOutput! """ Creates a new custom role in the workspace. Custom roles are created with the Support permissions preset by default and can be further configured using `upsertRoleScopes`. Requires the `roles:create` permission and the `custom_roles` billing entitlement. """ createCustomRole(input: CreateCustomRoleInput!): CreateCustomRoleOutput! """ Updates the name and/or description of an existing custom role. Only the fields provided in the input are changed. Requires the `roles:edit` permission. """ updateCustomRole(input: UpdateCustomRoleInput!): UpdateCustomRoleOutput! """ Permanently deletes a custom role by its ID. Returns the ID of the deleted role on success. Requires the `roles:edit` permission. """ deleteCustomRole(input: DeleteCustomRoleInput!): DeleteCustomRoleOutput! """ Sets the thread-visibility scopes for a custom role, replacing any previously configured scopes for the given resource. Each scope condition limits which threads users with this role can see, based on a primitive type (label, tier, channel, tenant, or company) and an access mode. Requires the `roles:edit` permission and the `custom_roles` billing entitlement. """ upsertRoleScopes(input: UpsertRoleScopesInput!): UpsertRoleScopesOutput! """ Creates a new label type in the workspace. Label types define the labels available to apply to threads and users. Requires the `labelType:create` permission. """ createLabelType(input: CreateLabelTypeInput!): CreateLabelTypeOutput! """ Archives a label type so it can no longer be applied to threads, while preserving it on threads that already have it. To apply an archived label type again, unarchive it first. Requires the `labelType:edit` permission. """ archiveLabelType(input: ArchiveLabelTypeInput!): ArchiveLabelTypeOutput! """ Restores an archived label type so it can be applied to threads again. Requires the `labelType:edit` permission. """ unarchiveLabelType(input: UnarchiveLabelTypeInput!): UnarchiveLabelTypeOutput! """ Updates properties of an existing label type. Uses field-level wrapper inputs: pass `{ value: ... }` to change a field, or omit the field entirely to leave it unchanged. At least one field besides `labelTypeId` must be provided. Requires the `labelType:edit` permission. """ updateLabelType(input: UpdateLabelTypeInput!): UpdateLabelTypeOutput! """ Changes the position of a label type in the ordered list, or moves it to a different parent. Supply `afterLabelTypeId` or `beforeLabelTypeId` to place the label type relative to a sibling, and optionally `parentLabelTypeId` to nest it under a parent. Requires the `labelType:edit` permission. """ moveLabelType(input: MoveLabelTypeInput!): MoveLabelTypeOutput! """ Add one or more labels to a thread. Labels that are already present on the thread are silently skipped. Archived label types are rejected with error code `cannot_add_label_using_archived_label_type`. Requires the `label:create` permission. """ addLabels(input: AddLabelsInput!): AddLabelsOutput! """ Add one or more team labels to a Plain user (agent). Only label types with type `TEAM` may be applied to users. Label types already present on the user are silently skipped. Requires the `label:create` permission. """ addLabelsToUser(input: AddLabelsToUserInput!): AddLabelsToUserOutput! """ Remove one or more labels from a thread by label ID. All provided label IDs must belong to the same thread; mixing labels from different threads returns an error. Requires the `label:delete` permission. """ removeLabels(input: RemoveLabelsInput!): RemoveLabelsOutput! """ Remove one or more labels from a Plain user (agent) by label ID. Returns the remaining labels still applied to the user after the removal. Requires the `label:delete` permission. """ removeLabelsFromUser(input: RemoveLabelsFromUserInput!): RemoveLabelsFromUserOutput! """ Links a thread to an external entity such as a Linear issue, Jira ticket, another Plain thread, or a Plain task. Provide exactly one of `linearIssue`, `jiraIssue`, `plainThread`, `plainTask`, or `sourceId`/`sourceType` for generic issue trackers. Returns an error with code `thread_link_already_exists` if the same link already exists for the thread. """ createThreadLink(input: CreateThreadLinkInput!): CreateThreadLinkOutput! """ Removes an existing thread link by its ID. If the link type was `MERGED_INTO`, the previously merged thread will be marked as done automatically. """ deleteThreadLink(input: DeleteThreadLinkInput!): DeleteThreadLinkOutput! """ Creates an internal note visible only to your team. Notes appear in the thread timeline alongside customer messages and are useful for sharing context, reminders, or annotations. Provide `threadId` to pin the note to a specific thread; omit it to attach the note to the customer so it appears across all of their threads. Requires the `note:create` permission. """ createNote(input: CreateNoteInput!): CreateNoteOutput! """ Updates the text, markdown, or attachments of an existing note. Requires the `note:edit` permission. """ updateNote(input: UpdateNoteInput!): UpdateNoteOutput! """ Soft-deletes a note. The note is marked as deleted but its record is retained. Requires the `note:delete` permission. """ deleteNote(input: DeleteNoteInput!): DeleteNoteOutput! """ Creates a new saved threads view in the workspace. The view stores a named filter configuration (including sort order, grouping, layout, and display options) that can be applied when querying threads. Requires the savedThreadsView:create permission; depending on workspace settings, only admins or all members may be allowed to create views. """ createSavedThreadsView(input: CreateSavedThreadsViewInput!): CreateSavedThreadsViewOutput! """ Updates an existing saved threads view. All fields in the input are optional; only the fields you provide will be changed. Depending on workspace settings, only the view's creator or workspace admins may be allowed to edit a view they did not create. """ updateSavedThreadsView(input: UpdateSavedThreadsViewInput!): UpdateSavedThreadsViewOutput! """ Permanently deletes a saved threads view. Returns an error if the view does not exist. Depending on workspace settings, only the view's creator or workspace admins may be allowed to delete a view they did not create. """ deleteSavedThreadsView(input: DeleteSavedThreadsViewInput!): DeleteSavedThreadsViewOutput! """ Saves a page as a favorite for the currently authenticated user. If the caller has already favorited a page with the same key, the existing record is returned unchanged (idempotent). Requires the `favoritePage:create` permission. """ createMyFavoritePage(input: CreateMyFavoritePageInput!): CreateMyFavoritePageOutput! """ Removes a favorite page for the currently authenticated user. If the specified favorite page does not exist or belongs to a different user, the mutation succeeds silently with no error. Requires the `favoritePage:delete` permission. """ deleteMyFavoritePage(input: DeleteMyFavoritePageInput!): DeleteMyFavoritePageOutput! """ Creates a new snippet in the workspace. The `name` is used to search for the snippet when composing a reply. Provide `markdown` in addition to `text` to supply a rich-text version used in channels that support markdown. The optional `path` (alphanumeric only) places the snippet in a folder in the Plain app. Requires the `snippet:create` permission. """ createSnippet(input: CreateSnippetInput!): CreateSnippetOutput! """ Soft-deletes a snippet. Deleted snippets are hidden from the snippet picker but remain fetchable by ID with `isDeleted: true`, preserving the history of replies that referenced them. Requires the `snippet:delete` permission. """ deleteSnippet(input: DeleteSnippetInput!): DeleteSnippetOutput! """ Updates one or more fields of an existing snippet. Each field uses a wrapper input — pass `{ value: "..." }` to set it or omit it entirely to leave it unchanged. To remove the snippet's `path` (un-group it) pass `{ value: null }`. Requires the `snippet:edit` permission. """ updateSnippet(input: UpdateSnippetInput!): UpdateSnippetOutput! """ Create or update the settings for a team (label type of kind TEAM). If settings do not yet exist for the given team, they are created with defaults (round-robin disabled, max capacity 5); otherwise the existing settings are updated with the supplied values. The labelTypeId must refer to a label type of type TEAM — passing any other label type ID returns a validation error. Requires the `labelType:edit` permission. """ upsertTeamSettings(input: UpsertTeamSettingsInput!): UpsertTeamSettingsOutput! """ Create a new task. Only `title` is required; `description`, `status`, `priority`, assignee, and a parent `companyId` or `tenantId` are all optional. A task may be linked to either a company or a tenant, but not both. Requires the `task:create` permission. """ createTask(input: CreateTaskInput!): CreateTaskOutput! """ Update an existing task. Only the fields you include are changed — omitted fields are left as-is. Setting `companyId` clears any existing `tenantId` and vice versa, since a task may be linked to at most one. Requires the `task:edit` permission. """ updateTask(input: UpdateTaskInput!): UpdateTaskOutput! """ Soft-delete a task. The task is removed from the active task list but remains queryable by ID with `isDeleted: true`. All thread links attached to the task are also deleted. Requires the `task:delete` permission. """ deleteTask(input: DeleteTaskInput!): DeleteTaskOutput! """ Creates or updates a customer identified by email address, external ID, or Plain customer ID. Supply `onCreate` fields for values to set when creating and `onUpdate` fields for values to apply when the customer already exists. The output's `result` field indicates whether the customer was `CREATED`, `UPDATED`, or `NOOP` (existed and no values changed). Requires the `customer:create` and `customer:edit` permissions. """ upsertCustomer(input: UpsertCustomerInput!): UpsertCustomerOutput! """ Assigns a customer to a different company, or clears their company association. Requires the `customer:edit` permission. """ updateCustomerCompany(input: UpdateCustomerCompanyInput!): UpdateCustomerCompanyOutput! """ Permanently deletes a customer and all associated data (threads, timeline entries, etc.). Deletion is asynchronous and cannot be reversed. Requires the `customer:delete` permission. """ deleteCustomer(input: DeleteCustomerInput!): DeleteCustomerOutput! """ Flags a customer as spam, hiding their threads from the inbox and excluding them from metrics. The operation is idempotent — marking an already-spam customer leaves their `markedAsSpamAt` timestamp unchanged. Requires the `customer:edit` permission. """ markCustomerAsSpam(input: MarkCustomerAsSpamInput!): MarkCustomerAsSpamOutput! """ Clears the spam flag from a customer, restoring their threads to the inbox and metrics. The `markedAsSpamAt` timestamp is cleared. Requires the `customer:edit` permission. """ unmarkCustomerAsSpam(input: UnmarkCustomerAsSpamInput!): UnmarkCustomerAsSpamOutput! """ Create or update a customer group identified by `customerGroupId`, `customerGroupKey`, or `externalId`. If a matching group is found it is updated and the result is `UPDATED`; if data is unchanged the result is `NOOP`; otherwise a new group is created and the result is `CREATED`. Note: using `customerGroupId` as the identifier returns an error if the group does not exist. Requires `customerGroup:create` and `customerGroup:edit` permissions. """ upsertCustomerGroup(input: UpsertCustomerGroupInput!): UpsertCustomerGroupOutput! """ Create a new customer group with a unique key, display name, and color. Use `upsertCustomerGroup` instead if you need idempotent create-or-update behaviour. Requires `customerGroup:create` permission. """ createCustomerGroup(input: CreateCustomerGroupInput!): CreateCustomerGroupOutput! """ Update the name, key, color, or external ID of an existing customer group. At least one field must be provided in the input. Requires `customerGroup:edit` permission. """ updateCustomerGroup(input: UpdateCustomerGroupInput!): UpdateCustomerGroupOutput! """ Permanently delete a customer group by ID. This will fail if the group still has members — remove all customers from the group first. Requires `customerGroup:delete` permission. """ deleteCustomerGroup(input: DeleteCustomerGroupInput!): DeleteCustomerGroupOutput! """ Add a customer to one or more customer groups (up to 25 at once), identified by group ID, key, or external ID. Memberships that already exist are silently skipped, making this operation safe to call repeatedly. Returns only the newly created memberships. Requires `customerGroupMembership:create` permission. """ addCustomerToCustomerGroups(input: AddCustomerToCustomerGroupsInput!): AddCustomerToCustomerGroupsOutput! """ Remove a customer from one or more customer groups (up to 25 at once), identified by group ID, key, or external ID. Returns an error if the customer is not currently a member of any of the specified groups. Requires `customerGroupMembership:delete` permission. """ removeCustomerFromCustomerGroups(input: RemoveCustomerFromCustomerGroupsInput!): RemoveCustomerFromCustomerGroupsOutput! """ Creates a new thread field schema, defining a custom field that can be attached to threads. The `key` must be unique within the workspace and cannot be changed after creation. Requires the `threadFieldSchema:create` permission. """ createThreadFieldSchema(input: CreateThreadFieldSchemaInput!): CreateThreadFieldSchemaOutput! """ Updates an existing thread field schema. All fields except `key` and `type` are mutable. String fields use wrapper inputs (e.g. `{ value: "..." }`) to distinguish a deliberate null from an omitted value. Requires the `threadFieldSchema:edit` permission. """ updateThreadFieldSchema(input: UpdateThreadFieldSchemaInput!): UpdateThreadFieldSchemaOutput! """ Permanently deletes a thread field schema and removes all field values stored against it on every thread. This action cannot be undone. Requires the `threadFieldSchema:delete` permission. """ deleteThreadFieldSchema(input: DeleteThreadFieldSchemaInput!): DeleteThreadFieldSchemaOutput! """ Updates the display order of thread field schemas. You only need to include schemas whose order is changing — omitted schemas are left unchanged. """ reorderThreadFieldSchemas(input: ReorderThreadFieldSchemasInput!): ReorderThreadFieldSchemasOutput! """ Sets (or updates) a single thread field value on a thread, identified by thread ID and field key. Creates the field if it does not exist, or overwrites the existing value if it does. Requires the `threadField:create` and `threadField:update` permissions. """ upsertThreadField(input: UpsertThreadFieldInput!): UpsertThreadFieldOutput! """ Upserts up to 25 thread field values in a single call — useful when setting multiple fields on one or more threads at once. Each entry is independently created or updated; a validation error on the batch as a whole is returned if any entry is invalid. Requires the `threadField:create` and `threadField:update` permissions. """ bulkUpsertThreadFields(input: BulkUpsertThreadFieldsInput!): BulkUpsertThreadFieldsOutput! """ Removes a stored thread field value from a thread, identified by thread ID and field key. Has no effect if the field has no value. Requires the `threadField:delete` permission. """ deleteThreadField(input: DeleteThreadFieldInput!): DeleteThreadFieldOutput! """ Creates a new escalation path with the given name, optional description, and an ordered list of steps (up to 20). Each step routes the thread to either a specific user or the owners of a label type. Steps must not contain duplicates. Requires the `escalationPath:create` permission and the escalation paths feature entitlement. """ createEscalationPath(input: CreateEscalationPathInput!): CreateEscalationPathOutput! """ Updates an existing escalation path. All fields are optional — only the fields provided will be changed. If `steps` is provided, it replaces the full list of steps; omit it to leave the current steps unchanged. Returns an error if the escalation path does not exist. Requires the `escalationPath:edit` permission. """ updateEscalationPath(input: UpdateEscalationPathInput!): UpdateEscalationPathOutput! """ Permanently deletes an escalation path by ID. Returns an error if the escalation path does not exist. Threads that were attached to this escalation path will lose their escalation path association. Requires the `escalationPath:delete` permission. """ deleteEscalationPath(input: DeleteEscalationPathInput!): DeleteEscalationPathOutput! """ Create a new workflow rule. The rule starts unpublished; use `toggleWorkflowRulePublished` to activate it. The `payload` field must be a valid JSON-encoded rule definition. """ createWorkflowRule(input: CreateWorkflowRuleInput!): CreateWorkflowRuleOutput! """ Update the name, payload, or display order of an existing workflow rule. """ updateWorkflowRule(input: UpdateWorkflowRuleInput!): UpdateWorkflowRuleOutput! """ Toggle the published state of a workflow rule. Published rules are active and will fire automatically; unpublished rules are drafts. """ toggleWorkflowRulePublished(input: ToggleWorkflowRulePublishedInput!): ToggleWorkflowRulePublishedOutput! """Permanently delete a workflow rule. This action cannot be undone.""" deleteWorkflowRule(input: DeleteWorkflowRuleInput!): DeleteWorkflowRuleOutput! """ Manually trigger a workflow rule against a specific thread, bypassing its automatic trigger conditions. Useful for testing rules or applying them on demand. """ triggerWorkflowRule(input: TriggerWorkflowRuleInput!): TriggerWorkflowRuleOutput! """ Create a new workflow. The workflow starts unpublished with no steps; use `bulkUpsertWorkflowSteps` to add steps and `updateWorkflow` to publish it. The `trigger` field must be a valid JSON-encoded trigger configuration. """ createWorkflow(input: CreateWorkflowInput!): CreateWorkflowOutput! """ Update a workflow's name, trigger configuration, entry step, display order, or published status. """ updateWorkflow(input: UpdateWorkflowInput!): UpdateWorkflowOutput! """ Permanently delete a workflow and all its steps. Existing executions are retained for audit purposes. """ deleteWorkflow(input: DeleteWorkflowInput!): DeleteWorkflowOutput! """ Manually trigger a workflow against a specific thread. Returns the resulting WorkflowExecution so you can track the run's status and step results. """ triggerWorkflow(input: TriggerWorkflowInput!): TriggerWorkflowOutput! """ Add a single step to a workflow. For bulk changes to a workflow's step graph, prefer `bulkUpsertWorkflowSteps`. """ createWorkflowStep(input: CreateWorkflowStepInput!): CreateWorkflowStepOutput! """ Update a single workflow step's configuration, transitions, or canvas position. Replacing `transitions` is a full replacement of the array. """ updateWorkflowStep(input: UpdateWorkflowStepInput!): UpdateWorkflowStepOutput! """ Delete a single step from a workflow. You are responsible for updating any steps that reference this step in their transitions. """ deleteWorkflowStep(input: DeleteWorkflowStepInput!): DeleteWorkflowStepOutput! """ Atomically replace all steps in a workflow. Steps with a matching `stepId` are updated; steps without an ID (or with a new ID) are created; steps that existed before but are absent from the input are deleted. Optionally updates `startStepId` and the trigger configuration in the same call. Maximum 60 steps per workflow. """ bulkUpsertWorkflowSteps(input: BulkUpsertWorkflowStepsInput!): BulkUpsertWorkflowStepsOutput! """ Send a chat message from a support agent (or machine user) to a customer on a CHAT-channel thread. Either `text` or at least one `attachmentIds` entry must be provided. If `threadId` is omitted a new thread is created; if supplied the message is appended to the existing thread. An optional `timestamp` (ISO 8601, must be in the past) allows backdating messages when backfilling historical data. Requires the `chat:create` permission and the live-chat billing entitlement. """ sendChat(input: SendChatInput!): SendChatOutput! """ Send a chat message on behalf of a customer into an existing CHAT-channel thread. Use this in a headless portal integration to forward messages written by the customer in your own UI into Plain. Unlike `sendChat`, `threadId` is required and the caller must be authenticated as a machine user. An optional `timestamp` (ISO 8601, must be in the past) allows backdating messages when backfilling. Requires the `chat:create` permission and the headless-portal billing entitlement. """ sendCustomerChat(input: SendCustomerChatInput!): SendCustomerChatOutput! """ Create a new chat app, which represents a source of chat messages (e.g. a specific in-product widget or integration). Optionally supply a `logo` using a previously uploaded workspace file ID. The logo must be a public, image-type workspace file. Requires the `chatApp:create` permission and the live-chat billing entitlement. """ createChatApp(input: CreateChatAppInput!): CreateChatAppOutput! """ Update the name or logo of an existing chat app. Only the fields provided in the input are changed. Requires the `chatApp:edit` permission. """ updateChatApp(input: UpdateChatAppInput!): UpdateChatAppOutput! """ Permanently delete a chat app. This action cannot be undone. Requires the `chatApp:delete` permission. """ deleteChatApp(input: DeleteChatAppInput!): DeleteChatAppOutput! """ Generate a signing secret for a chat app used to authenticate incoming webhook requests. If a secret already exists for the given chat app it is deleted and replaced — the new secret is the only time the raw value is returned. Store it securely immediately after creation. Requires the `chatAppSecret:create` permission and the live-chat billing entitlement. """ createChatAppSecret(input: CreateChatAppSecretInput!): CreateChatAppSecretOutput! """ Delete the signing secret associated with a chat app. After deletion, webhook signature verification for that chat app will fail until a new secret is created. Requires the `chatAppSecret:delete` permission. """ deleteChatAppSecret(input: DeleteChatAppSecretInput!): DeleteChatAppSecretOutput! """ Sends a Microsoft Teams message on a thread. The thread must be associated with a connected MS Teams channel and the workspace must have an active MS Teams integration. """ sendMSTeamsMessage(input: SendMSTeamsMessageInput!): SendMSTeamsMessageOutput! """ Sends a Slack message on a thread. The message appears in the thread's associated Slack channel as a reply or new message. """ sendSlackMessage(input: SendSlackMessageInput!): SendSlackMessageOutput! """ Sends a direct Slack message to a Plain user sharing a link to the given thread. Useful for notifying a teammate about a thread directly in Slack. """ shareThreadToUserInSlack(input: ShareThreadToUserInSlackInput!): ShareThreadToUserInSlackOutput! """ Sends an outbound Discord message on a thread. Requires both a workspace-level Discord channel integration and a personal user auth integration for the sending user. The message is posted into the Discord thread associated with the Plain thread. """ sendDiscordMessage(input: SendDiscordMessageInput!): SendDiscordMessageOutput! """Adds or removes a reaction from a slack message timeline entry.""" toggleSlackMessageReaction(input: ToggleSlackMessageReactionInput!): ToggleSlackMessageReactionOutput! """ Create a new thread by forking from a specific timeline entry in an existing thread. The forked thread starts at the chosen message, allowing you to split a conversation into a separate support case. Requires the `thread:create` permission. """ forkThread(input: ForkThreadInput!): ForkThreadOutput! """ Updates the settings of a connected Slack channel, such as its channel type (customer or discussion) or whether it is enabled. """ updateConnectedSlackChannel(input: UpdateConnectedSlackChannelInput!): UpdateConnectedSlackChannelOutput! """ Applies many connected-slack-channel updates in a single request. Each update is applied independently with the same semantics as `updateConnectedSlackChannel`; partial failures are reported per-item. """ bulkUpdateConnectedSlackChannels(input: BulkUpdateConnectedSlackChannelsInput!): BulkUpdateConnectedSlackChannelsOutput! """ Instructs the Plain Slack bot to join all Slack channels it has access to for the given workspace Slack channel integration. Use this after first installing the integration to connect existing channels. """ bulkJoinSlackChannels(input: BulkJoinSlackChannelsInput!): BulkJoinSlackChannelsOutput! """ Replaces all auto-join rules for a workspace slack channel integration. The provided rules become the complete set of rules for the integration. """ setSlackAutoJoinRules(input: SetSlackAutoJoinRulesInput!): SetSlackAutoJoinRulesOutput! """ Creates a customer event that appears in the timeline of every thread belonging to the customer. Use this to surface important product activity (e.g. a failed payment, a deleted API key) so your team has full context when helping the customer. The event layout is defined using Plain UI components. Requires the `customerEvent:create` permission. """ createCustomerEvent(input: CreateCustomerEventInput!): CreateCustomerEventOutput! """ Creates a thread event that appears only in the timeline of the specified thread. Use this when an activity is specific to a single conversation rather than the customer as a whole. The event layout is defined using Plain UI components. Thread events are visible only to your team and are never shown to the customer. Requires the `threadEvent:create` and `threadEvent:read` permissions. """ createThreadEvent(input: CreateThreadEventInput!): CreateThreadEventOutput! """ Configures a custom email domain for the workspace using the provided support email address. After creation, complete setup by verifying email forwarding with verifyWorkspaceEmailForwardingSettings and DNS records with verifyWorkspaceEmailDnsSettings. """ createWorkspaceEmailDomainSettings(input: CreateWorkspaceEmailDomainSettingsInput!): CreateWorkspaceEmailDomainSettingsOutput! """ Removes the workspace's custom email domain configuration. After deletion, email will no longer be routed through the custom domain. """ deleteWorkspaceEmailDomainSettings: DeleteWorkspaceEmailDomainSettingsOutput! """ Marks whether email forwarding has been configured on the custom domain. Call this after setting up the forwarding rule in your DNS/email provider to update Plain's record of the forwarding status. """ verifyWorkspaceEmailForwardingSettings(input: VerifyWorkspaceEmailForwardingSettingsInput!): VerifyWorkspaceEmailForwardingSettingsOutput! """ Triggers Plain to re-check whether the DKIM and return-path DNS records are correctly configured for the workspace's custom email domain. Returns the updated domain settings with the latest verification status. """ verifyWorkspaceEmailDnsSettings: VerifyWorkspaceEmailDnsSettingsOutput! """ Updates workspace-level email settings such as whether email is enabled and the list of BCC addresses that are copied on all outbound emails. """ updateWorkspaceEmailSettings(input: UpdateWorkspaceEmailSettingsInput!): UpdateWorkspaceEmailSettingsOutput! """ Adds an alternate support email address to the workspace's custom domain configuration, allowing emails to be sent from and received at that address. A workspace can have up to 5 alternate addresses. """ addWorkspaceAlternateSupportEmailAddress(input: AddWorkspaceAlternateSupportEmailAddressInput!): AddWorkspaceAlternateSupportEmailAddressOutput! """ Removes an alternate support email address from the workspace's custom domain configuration. """ removeWorkspaceAlternateSupportEmailAddress(input: RemoveWorkspaceAlternateSupportEmailAddressInput!): RemoveWorkspaceAlternateSupportEmailAddressOutput! """ Sends a new outbound email to a customer, creating a new thread by default. Use threadId to attach the email to an existing thread instead. Supports CC and BCC recipients (up to 49 combined), file attachments, and an optional alternate from address. Requires the email:create and email:read permissions. """ sendNewEmail(input: SendNewEmailInput!): SendNewEmailOutput! """ Replies to an existing email in a thread, threading the response correctly in email clients. Use inReplyToEmailId to identify the email being replied to. Supports CC and BCC recipients (up to 49 combined) and file attachments. Requires the email:create and email:read permissions. """ replyToEmail(input: ReplyToEmailInput!): ReplyToEmailOutput! """ Generates a short-lived URL that can be used to preview the rendered HTML of an email. Useful for displaying email content in external tools or dashboards. """ createEmailPreviewUrl(input: CreateEmailPreviewUrlInput!): CreateEmailPreviewUrlOutput! """ Sends the same email body to multiple threads at once (up to 100 thread IDs per call). Useful for broadcasting updates to a set of customers, for example notifying affected users of an incident. """ sendBulkEmail(input: SendBulkEmailInput!): SendBulkEmailOutput! """ Generate a short-lived download URL for an existing attachment. The returned URL expires after 3 minutes. If your workspace has virus scanning enabled, the response also includes an `attachmentVirusScanResult` indicating whether the file is safe to download. Requires the `attachment:download` permission. """ createAttachmentDownloadUrl(input: CreateAttachmentDownloadUrlInput!): CreateAttachmentDownloadUrlOutput! """ Generate a presigned upload URL for an attachment. Use the returned `uploadFormUrl` and `uploadFormData` fields to POST the file as multipart/form-data directly to storage. The upload URL expires after 2 hours; the resulting `attachment.id` can then be referenced in mutations such as `createThread`, `replyToEmail`, or `createNote`. Requires the `attachment:create` permission. Attachments that are never referenced in a message are automatically deleted after 24 hours. """ createAttachmentUploadUrl(input: CreateAttachmentUploadUrlInput!): CreateAttachmentUploadUrlOutput! """ Creates a new machine user in the workspace. Each machine user can hold multiple API keys and optionally has a public-facing name shown to customers. Only one machine user of type AI_AGENT is allowed per workspace. Requires the `machineUser:create` permission. """ createMachineUser(input: CreateMachineUserInput!): CreateMachineUserOutput! """ Deletes a machine user and all of its API keys. The deleted machine user is returned in the response. Requires the `machineUser:delete` permission. """ deleteMachineUser(input: DeleteMachineUserInput!): DeleteMachineUserOutput! """ Updates the name, description, or avatar of an existing machine user. At least one of `fullName`, `publicName`, `description`, or `avatar` must be provided. Requires the `machineUser:edit` permission. """ updateMachineUser(input: UpdateMachineUserInput!): UpdateMachineUserOutput! """ Create an API key for a machine user. The plaintext secret is returned only once in `apiKeySecret` and cannot be retrieved again — store it immediately. You can only grant permissions that you yourself hold; attempting to escalate privileges returns an error. Requires the `apiKey:create` permission. """ createApiKey(input: CreateApiKeyInput!): CreateApiKeyOutput! """ Update the description and/or permissions of an existing API key. Permissions are replaced in full — pass the complete desired set. You cannot assign permissions you do not hold. Requires the `apiKey:edit` permission. """ updateApiKey(input: UpdateApiKeyInput!): UpdateApiKeyOutput! """ Permanently revoke an API key. The deleted key is returned for confirmation. System-managed keys cannot be deleted. Requires the `apiKey:delete` permission. """ deleteApiKey(input: DeleteApiKeyInput!): DeleteApiKeyOutput! """ Connects the current user's personal Slack notifications integration using an OAuth auth code obtained from the Slack OAuth flow. Get the installation URL from mySlackInstallationInfo. """ createMySlackIntegration(input: CreateMySlackIntegrationInput!): CreateMySlackIntegrationOutput! """ Disconnects the current user's personal Slack notifications integration. """ deleteMySlackIntegration: DeleteMySlackIntegrationOutput! """ Connects the current user's user-auth Slack integration using an OAuth auth code. This integration allows Plain to send Slack messages as the authenticated user rather than as a bot. Get the installation URL from userAuthSlackInstallationInfo. """ createUserAuthSlackIntegration(input: CreateUserAuthSlackIntegrationInput!): CreateUserAuthSlackIntegrationOutput! """ Disconnects the current user's user-auth Slack integration for the specified Slack workspace. """ deleteUserAuthSlackIntegration(input: DeleteUserAuthSlackIntegrationInput!): DeleteUserAuthSlackIntegrationOutput! """ Connects a workspace-level Slack notifications integration using an OAuth auth code. This integration enables Plain to post notifications to a Slack channel on behalf of the workspace. Get the installation URL from workspaceSlackInstallationInfo. """ createWorkspaceSlackIntegration(input: CreateWorkspaceSlackIntegrationInput!): CreateWorkspaceSlackIntegrationOutput! """ Disconnects and removes a workspace-level Slack notifications integration. """ deleteWorkspaceSlackIntegration(input: DeleteWorkspaceSlackIntegrationInput!): DeleteWorkspaceSlackIntegrationOutput! """ Connects a workspace Slack channel integration using an OAuth auth code. This integration allows Plain to monitor Slack channels and create threads from messages. Get the installation URL from workspaceSlackChannelInstallationInfo. """ createWorkspaceSlackChannelIntegration(input: CreateWorkspaceSlackChannelIntegrationInput!): CreateWorkspaceSlackChannelIntegrationOutput! """ Re-authorizes an existing workspace Slack channel integration with a new OAuth auth code. Use this when isReinstallRequired is true on the integration to restore access. """ refreshWorkspaceSlackChannelIntegration(input: RefreshWorkspaceSlackChannelIntegrationInput!): RefreshWorkspaceSlackChannelIntegrationOutput! """ Disconnects and removes a workspace Slack channel integration. Connected channels for this integration will no longer be monitored. """ deleteWorkspaceSlackChannelIntegration(input: DeleteWorkspaceSlackChannelIntegrationInput!): DeleteWorkspaceSlackChannelIntegrationOutput! """ Installs Plain's Sidekick AI agent into the workspace's Slack. Exchanges the OAuth authorization code (obtained by sending the user through the URL from workspaceSlackSidekickInstallationInfo) for a bot token, creates or reuses the #ask-plain Slack channel, and persists the integration. Only one Sidekick integration is allowed per workspace and per Slack team; calling this when one already exists returns an error. Requires the workspaceSlackSidekickIntegration:create permission and the ai_agent billing entitlement. """ createWorkspaceSlackSidekickIntegration(input: CreateWorkspaceSlackSidekickIntegrationInput!): CreateWorkspaceSlackSidekickIntegrationOutput! """ Re-authorizes an existing workspace Slack Sidekick integration with a new OAuth auth code. Use this when isReinstallRequired is true on the integration to restore access. Requires the workspaceSlackSidekickIntegration:update permission. """ refreshWorkspaceSlackSidekickIntegration(input: RefreshWorkspaceSlackSidekickIntegrationInput!): RefreshWorkspaceSlackSidekickIntegrationOutput! """ Removes Plain's Sidekick AI agent from the workspace's Slack. Returns the integration that was deleted. Returns an error if no Sidekick integration exists. Requires the workspaceSlackSidekickIntegration:delete permission. """ deleteWorkspaceSlackSidekickIntegration: DeleteWorkspaceSlackSidekickIntegrationOutput! """ Updates configuration for the workspace's Slack Sidekick integration. Currently supports setting custom operating instructions that guide Sidekick's behavior. Omitting operatingInstructions is a no-op; passing null or an empty string clears any existing instructions. Requires the workspaceSlackSidekickIntegration:update permission. """ updateSidekickSlackConfig(input: UpdateSidekickSlackConfigInput!): UpdateSidekickSlackConfigOutput! """ Connects a Discord server (guild) to this workspace using an OAuth authorization code. Complete the OAuth flow via `workspaceDiscordChannelInstallationInfo` first to obtain the `authCode`. Once connected, Plain can receive inbound Discord messages and users can send replies via `sendDiscordMessage`. """ createWorkspaceDiscordChannelIntegration(input: CreateWorkspaceDiscordChannelIntegrationInput!): CreateWorkspaceDiscordChannelIntegrationOutput! """ Removes a workspace Discord channel integration, disconnecting the Discord guild from the workspace. Existing threads and messages are not deleted. """ deleteWorkspaceDiscordChannelIntegration(input: DeleteWorkspaceDiscordChannelIntegrationInput!): DeleteWorkspaceDiscordChannelIntegrationOutput! """ Links the current user's personal Discord account to a specific Discord guild. Users must complete this in addition to the workspace-level integration before they can send Discord messages. Obtain the `authCode` by directing the user through the URL returned by `userAuthDiscordChannelInstallationInfo`. """ createUserAuthDiscordChannelIntegration(input: CreateUserAuthDiscordChannelIntegrationInput!): CreateUserAuthDiscordChannelIntegrationOutput! """ Removes the current user's personal Discord authentication for the specified integration, preventing them from sending Discord messages until they re-authenticate. """ deleteUserAuthDiscordChannelIntegration(input: DeleteUserAuthDiscordChannelIntegrationInput!): DeleteUserAuthDiscordChannelIntegrationOutput! """ Syncs the list of Discord channels for a guild from the Discord API into Plain. Call this after channels are added or renamed in Discord so that `connectedDiscordChannels` reflects the latest state. """ refreshConnectedDiscordChannels(input: RefreshConnectedDiscordChannelsInput!): RefreshConnectedDiscordChannelsOutput! """ Updates settings for a connected Discord channel, such as enabling or disabling it. Disabled channels will not receive new messages from Plain. """ updateConnectedDiscordChannel(input: UpdateConnectedDiscordChannelInput!): UpdateConnectedDiscordChannelOutput! """ Creates a webhook-based Discord integration for this workspace using an incoming webhook URL from Discord. This is the simpler webhook integration (as opposed to the full OAuth channel integration) and is used to post notifications to a Discord channel. The `webhookUrl` must be a valid Discord incoming webhook URL. """ createWorkspaceDiscordIntegration(input: CreateWorkspaceDiscordIntegrationInput!): CreateWorkspaceDiscordIntegrationOutput! """Removes a webhook-based workspace Discord integration by its ID.""" deleteWorkspaceDiscordIntegration(input: DeleteWorkspaceDiscordIntegrationInput!): DeleteWorkspaceDiscordIntegrationOutput! """ Connects the current user's Plain account to their Linear account by exchanging an OAuth authorization code. Obtain the authorization code by directing the user through the URL returned by myLinearInstallationInfo. The redirectUrl must match the one used when generating the installation URL. Each workspace can only be connected to one Linear organisation; attempting to connect a second organisation returns an error. """ createMyLinearIntegration(input: CreateMyLinearIntegrationInput!): CreateMyLinearIntegrationOutput! """ Disconnects the current user's Linear integration. If no integration exists this is a no-op and succeeds. """ deleteMyLinearIntegration: DeleteMyLinearIntegrationOutput! createLinearAppIntegration(input: CreateLinearAppIntegrationInput!): CreateLinearAppIntegrationOutput! deleteLinearAppIntegration: DeleteLinearAppIntegrationOutput! """ Connects the currently authenticated user's Plain account to their GitHub account using a Nango OAuth session. Requires the 'githubUserAuthIntegration:create' permission. """ createGithubUserAuthIntegration(input: CreateGithubUserAuthIntegrationInput!): CreateGithubUserAuthIntegrationOutput! """ Disconnects the currently authenticated user's GitHub integration, removing their stored credentials. Requires the 'githubUserAuthIntegration:delete' permission. """ deleteGithubUserAuthIntegration: DeleteGithubUserAuthIntegrationOutput! """ Creates a Cursor integration for the workspace using the provided API token. Only one integration can be active at a time; call `deleteWorkspaceCursorIntegration` before creating a new one. Requires the `workspaceCursorIntegration:create` permission. """ createWorkspaceCursorIntegration(input: CreateWorkspaceCursorIntegrationInput!): CreateWorkspaceCursorIntegrationOutput! """ Removes the workspace's Cursor integration. If no integration with the given ID exists, the operation succeeds and returns a null `id`. Requires the `workspaceCursorIntegration:delete` permission. """ deleteWorkspaceCursorIntegration(id: ID!): DeleteWorkspaceCursorIntegrationOutput! """ Connects the current user's personal Microsoft Teams account to Plain using the OAuth authorization code obtained after completing the installation flow from `myMSTeamsInstallationInfo`. """ createMyMSTeamsIntegration(input: CreateMyMSTeamsIntegrationInput!): CreateMyMSTeamsIntegrationOutput! """ Disconnects the current user's personal Microsoft Teams integration from Plain. """ deleteMyMSTeamsIntegration: DeleteMyMSTeamsIntegrationOutput! """ Creates a workspace-level Microsoft Teams integration by linking Plain to an Azure AD tenant. Each workspace can only have one integration, and each tenant can only be connected to one workspace. Call `workspaceMSTeamsInstallationInfo` first to obtain the admin-consent URL, then pass the resulting tenant ID here. """ createWorkspaceMSTeamsIntegration(input: CreateWorkspaceMSTeamsIntegrationInput!): CreateWorkspaceMSTeamsIntegrationOutput! """ Removes a workspace-level Microsoft Teams integration. Pass the integration's `id` as `integrationId`. Returns the deleted integration. """ deleteWorkspaceMSTeamsIntegration(input: DeleteWorkspaceMSTeamsIntegrationInput!): DeleteWorkspaceMSTeamsIntegrationOutput! """ Create or overwrite a named setting at the given scope. Provide exactly one value field in `SettingValueInput` (boolean, string, number, or stringArray) that matches the expected type for the setting code. Returns the stored setting on success. To clear a value and revert to the scope hierarchy default, use `deleteSetting` instead. """ updateSetting(input: UpdateSettingInput!): UpdateSettingOutput! """ Delete a stored setting value at the given scope, causing the effective value to revert to the next level in the scope hierarchy (for example, removing a per-channel override exposes the workspace-level default). Returns the value that was removed in `previousSetting`, or null if no explicit value was stored at that scope. This operation is idempotent — deleting a setting that does not exist returns null without an error. """ deleteSetting(input: DeleteSettingInput!): DeleteSettingOutput! """ Applies many Slack per-channel setting writes in a single request. Each update targets the `WORKSPACE_SLACK_CONNECTED_CHANNEL` scope and is written with the same semantics as `updateSetting`. The writes are attempted independently; partial failures are reported per-item. """ bulkUpdateSlackChannelSettings(input: BulkUpdateSlackChannelSettingsInput!): BulkUpdateSlackChannelSettingsOutput! """ Creates a new customer card config. New configs are placed at the bottom of the list (order 100000) by default. A maximum of 25 card configs can be created per workspace. Returns a `too_many_customer_card_configs` error if the limit is reached. The `key` must be unique within the workspace and is used in the request payload sent to your API URL. """ createCustomerCardConfig(input: CreateCustomerCardConfigInput!): CreateCustomerCardConfigOutput! """ Partially updates a customer card config. Only fields that are provided in the input will be changed; omitted fields are left unchanged. Updating `apiUrl` or `apiHeaders` requires the `customerCardConfigApiDetails:edit` permission. Changing the `key` must still result in a unique key within the workspace. """ updateCustomerCardConfig(input: UpdateCustomerCardConfigInput!): UpdateCustomerCardConfigOutput! """ Permanently deletes a customer card config and stops loading that card for all customers. """ deleteCustomerCardConfig(input: DeleteCustomerCardConfigInput!): DeleteCustomerCardConfigOutput! """ Updates the display order of one or more customer card configs. Only the configs listed in the input are updated; other configs keep their current order. This allows you to swap two configs or move a single card without specifying the full list. Duplicate order values are permitted and ties are broken by ID. """ reorderCustomerCardConfigs(input: ReorderCustomerCardConfigsInput!): ReorderCustomerCardConfigsOutput! """ Forces a fresh load of a customer card instance, bypassing the cache. Use this when you know your external data has changed and you want to immediately fetch updated card content from your API URL. The response will be a `CustomerCardInstanceLoading`; subscribe to `customerCardInstanceChanges` to receive the loaded or errored result. """ reloadCustomerCardInstance(input: ReloadCustomerCardInstanceInput!): ReloadCustomerCardInstanceOutput! """ Register a new HTTP endpoint to receive Plain webhook events. You must specify the URL, a human-readable description, the webhook schema version to pin to, whether deliveries should start immediately, and the list of event types to subscribe to. Use `subscriptionEventTypes` to discover valid event type identifiers. Requires the `webhookTarget:create` permission. """ createWebhookTarget(input: CreateWebhookTargetInput!): CreateWebhookTargetOutput! """ Update an existing webhook target. You can change the URL, pause or resume deliveries via `isEnabled`, switch to a newer schema version, or change which event types are subscribed to. Scalar fields use a `{ value: ... }` wrapper so you can omit fields you do not want to change; `eventSubscriptions` is a full replacement of the previous list, so include every event type the target should receive. At least one field must be provided. Requires the `webhookTarget:edit` permission. """ updateWebhookTarget(input: UpdateWebhookTargetInput!): UpdateWebhookTargetOutput! """ Delete a webhook target, stopping all future deliveries to that endpoint and removing it from the workspace. Existing delivery attempt history is retained until normal retention expiry. Requires the `webhookTarget:delete` permission. """ deleteWebhookTarget(input: DeleteWebhookTargetInput!): DeleteWebhookTargetOutput! """ Create a new thread for a customer. Commonly used when a customer submits a contact form or when you want to start a proactive support interaction from your own product. The thread is created in `TODO` status. You can optionally set a title, priority, assignee, labels, thread fields, tenant, and an `externalId` for later lookup. Requires the `thread:create` and `thread:read` permissions. """ createThread(input: CreateThreadInput!): CreateThreadOutput! """ Import a thread from an external system using its historical creation timestamp. Unlike createThread, this mutation does not trigger SLAs or autoresponders, making it suitable for backfilling historical support data. The externalId is used for idempotency — re-importing the same ID returns NOOP. Requires the thread:import permission. """ importThread(input: ImportThreadInput!): ImportThreadOutput! """ Backfill up to 25 historical messages onto an existing thread. Each message requires an externalId for idempotency — re-importing the same ID returns NOOP for that message. INBOUND messages must be authored by a customer; OUTBOUND and NOTE messages must be authored by a user. Requires the thread:import permission. """ importThreadMessages(input: ImportThreadMessagesInput!): ImportThreadMessagesOutput! """ Assign a thread to a specific user or machine user, replacing any existing primary assignee. Requires the `thread:assign` and `thread:read` permissions. """ assignThread(input: AssignThreadInput!): AssignThreadOutput! """ Remove the primary assignee from a thread, leaving it unassigned. Requires the `thread:unassign` and `thread:read` permissions. """ unassignThread(input: UnassignThreadInput!): UnassignThreadOutput! """ Add one or more users or machine users as additional (secondary) assignees on a thread. Additional assignees are looped in but are not the primary person responsible. Requires the `thread:assign` and `thread:read` permissions. """ addAdditionalAssignees(input: AddAdditionalAssigneesInput!): AddAdditionalAssigneesOutput! """ Remove one or more additional assignees from a thread. Does not affect the primary assignee. Requires the `thread:unassign` and `thread:read` permissions. """ removeAdditionalAssignees(input: RemoveAdditionalAssigneesInput!): RemoveAdditionalAssigneesOutput! """ Snooze a thread for a number of seconds or until the customer replies (set `statusDetail` to `WAITING_FOR_CUSTOMER`). A snoozed thread is automatically unsnoozed when new activity arrives or when the timer expires. Requires the `thread:edit` and `thread:read` permissions. """ snoozeThread(input: SnoozeThreadInput!): SnoozeThreadOutput! """ Mark a thread as Done, indicating there is nothing left for the support team to do right now. The thread will automatically revert to Todo when new activity arrives. Requires the `thread:edit` and `thread:read` permissions. """ markThreadAsDone(input: MarkThreadAsDoneInput!): MarkThreadAsDoneOutput! """ Explicitly move a thread back to Todo status. Use this to unsnooze a thread early or to reopen a thread that was incorrectly marked as Done. Requires the `thread:edit` and `thread:read` permissions. """ markThreadAsTodo(input: MarkThreadAsTodoInput!): MarkThreadAsTodoOutput! """ Reassign a thread to a different customer. The original customer retains all their other threads. Requires the `thread:edit` permission. """ changeThreadCustomer(input: ChangeThreadCustomerInput!): ChangeThreadCustomerOutput! """ Set the priority of a thread. Priority is an integer from 0 (urgent) to 3 (low). Requires the `thread:edit` permission. """ changeThreadPriority(input: ChangeThreadPriorityInput!): ChangeThreadPriorityOutput! """Update the title of a thread. Requires the `thread:edit` permission.""" updateThreadTitle(input: UpdateThreadTitleInput!): UpdateThreadTitleOutput! """ Update the external ID of a thread. Pass `externalId: null` to clear it. Requires the `thread:edit` permission. """ updateThreadExternalId(input: UpdateThreadExternalIdInput!): UpdateThreadExternalIdOutput! """ Move a thread to a different tenant, or pass `tenantIdentifier: null` to detach it from its current tenant. Requires the `thread:edit` permission. """ updateThreadTenant(input: UpdateThreadTenantInput!): UpdateThreadTenantOutput! """ Assign a thread to a tier, which governs the SLAs applied to it. Pass `tierIdentifier: null` to detach the thread from its current tier. Requires the `thread:edit` permission. """ updateThreadTier(input: UpdateThreadTierInput!): UpdateThreadTierOutput! """ Attach a thread to a specific escalation path, or pass `escalationPathId: null` to detach it. An escalation path defines the sequence of users or teams the thread escalates through if nobody responds in time. Requires the `thread:edit` permission. """ updateThreadEscalationPath(input: UpdateThreadEscalationPathInput!): UpdateThreadEscalationPathOutput! """ Lock a thread to prevent further replies or changes by non-admin users. Use this when a resolution is final and you want to freeze the conversation. Requires the `thread:edit` permission. """ lockThread(input: LockThreadInput!): LockThreadOutput! """ Update the AI agent status of a thread (`IN_PROGRESS`, `HANDED_OFF`, or `HANDLED`). Use this to signal that an AI agent has taken over, handed off to a human, or fully resolved the thread. Requires the `thread:edit` permission. """ updateThreadAgentStatus(input: UpdateThreadAgentStatusInput!): UpdateThreadAgentStatusOutput! """ Accept or dismiss a specific AI-suggested action on a thread's catchup summary. Pass the `suggestedActionId` from the thread's `catchupDetail` and the new `status`. Requires the `thread:edit` permission. """ updateThreadSuggestedActionStatus(input: UpdateThreadSuggestedActionStatusInput!): UpdateThreadSuggestedActionStatusOutput! """ Permanently delete a thread and all its associated data from Plain. This action is irreversible — use with caution. Requires the `thread:delete` permission. """ deleteThread(input: DeleteThreadInput!): DeleteThreadOutput! """ Start a new discussion on a Plain thread. Supports Slack (posts a new thread in a connected Slack channel), Email (sends an outbound email chain), and Cursor workspace background agent discussions. The markdownContent field is sent as the opening message; for Slack discussions you may also supply slackBlocks (JSON-encoded Block Kit array) for rich Slack formatting while markdownContent serves as the Plain UI fallback. Prefer createDiscussion for new integrations. """ createThreadDiscussion(input: CreateThreadDiscussionInput!): CreateThreadDiscussionOutput! """ Import an existing external conversation as a discussion on a Plain thread, backfilling all messages. Currently supports Slack threads via permalink — the entire thread (root message and all replies) is imported. If the Slack channel is not yet connected to Plain, the bot will automatically join and register it as a DISCUSSION channel (requires the connectedSlackChannel:edit permission). Private channels must have the Plain bot invited via /invite @Plain first. The call is idempotent: re-importing the same Slack thread onto the same Plain thread resumes from where the previous import left off; importing onto a different thread returns an error. Threads with more than 800 messages cannot be imported. """ importThreadDiscussion(input: ImportThreadDiscussionInput!): ImportThreadDiscussionOutput! """ Create a new discussion. Supersedes createThreadDiscussion and additionally supports Sidekick AGENT_SESSION discussions (which can be started without a thread). For Slack, Email, and Cursor discussion types, a threadId is required. For AGENT_SESSION discussions, threadId is optional and the discussion may be associated with a source entity (company, tenant, etc.) or page instead. """ createDiscussion(input: CreateDiscussionInput!): CreateDiscussionOutput! """ Send a reply to an existing Slack or email discussion. The message is posted in the original channel (as a Slack thread reply or email reply) and recorded in Plain. For Slack discussions you may optionally provide slackBlocks, set unfurlLinks, or broadcast the reply into the parent channel via replyBroadcast (replyBroadcast cannot be combined with attachmentIds). Prefer sendDiscussionMessage for new integrations. """ sendThreadDiscussionMessage(input: SendThreadDiscussionMessageInput!): SendThreadDiscussionMessageOutput! """ Send a message in an existing discussion. Supersedes sendThreadDiscussionMessage and additionally supports Sidekick AGENT_SESSION discussions. For Slack and email discussions, use attachmentIds for customer-scoped file attachments. For AGENT_SESSION discussions, use workspaceFileIds instead. The message is delivered to the underlying channel (Slack thread reply, email reply, or agent session). """ sendDiscussionMessage(input: SendDiscussionMessageInput!): SendDiscussionMessageOutput! """ Mark a discussion as resolved, setting its status to RESOLVED and recording the resolution timestamp. """ markThreadDiscussionAsResolved(input: MarkThreadDiscussionAsResolvedInput!): MarkThreadDiscussionAsResolvedOutput! """ Clear the unread flag on a discussion. Use this when the user has viewed a discussion so that the isUnread indicator is reset. Returns the updated discussion. """ markThreadDiscussionRead(input: MarkThreadDiscussionReadInput!): MarkThreadDiscussionReadOutput! """ Permanently delete a discussion from Plain. Only Slack-channel discussions are currently supported. This removes the discussion record from Plain but does not delete the underlying Slack thread. """ deleteThreadDiscussion(input: DeleteThreadDiscussionInput!): DeleteThreadDiscussionOutput! """ Approves or denies a pending Sidekick tool-use approval request identified by its leaseId. When approved, the agent session automatically resumes and executes the approved tools. When denied, the agent is notified so it can explain the denial to the user. An optional reviewerNote is passed back to the agent in both cases. """ resolveAgentApproval(input: ResolveAgentApprovalInput!): ResolveAgentApprovalOutput! """ Removes a user message that is waiting in a Sidekick AGENT_SESSION queue. Rejects with `queued_agent_session_message_already_dispatched` if the agent has already picked the message up — the frontend should reload to reflect the new state. """ deleteQueuedAgentSessionMessage(input: DeleteQueuedAgentSessionMessageInput!): DeleteQueuedAgentSessionMessageOutput! """ Edits the text of a user message that is waiting in a Sidekick AGENT_SESSION queue. Attachments are not editable in this flow — delete and re-send to change them. Rejects with `queued_agent_session_message_already_dispatched` if the agent has already picked the message up. """ editQueuedAgentSessionMessage(input: EditQueuedAgentSessionMessageInput!): EditQueuedAgentSessionMessageOutput! """ Send a reply to the customer on a thread using the most appropriate channel automatically. Supports threads where the last inbound message is an email, a Slack message, or a form submission. If the thread has no messages yet, an email is sent to the customer. Requires the `thread:reply` (or channel-specific send) permission. """ replyToThread(input: ReplyToThreadInput!): ReplyToThreadOutput! """ Issues a short-lived RS256-signed JWT (60-second TTL) that a Plain embed iframe passes to your backend so you can verify the calling user's identity and context. Verify the token by fetching the public keys from the JWKS endpoint returned in `EmbedToken.jwksUrl`. The token carries claims identifying the Plain user, the thread, and the customer, plus the `plain_embed_id` you supplied for audit logging. Each call produces a unique token; call this mutation immediately before your embed needs to make an authenticated request to your backend. """ mintEmbedToken(input: MintEmbedTokenInput!): MintEmbedTokenOutput! """ Creates or updates the email signature for the currently authenticated user. The signature is appended automatically to outbound emails sent by that user. """ upsertMyEmailSignature(input: UpsertMyEmailSignatureInput!): UpsertMyEmailSignatureOutput! """ Create a new autoresponder that automatically sends a reply when a thread is created from a matching message source and satisfies all specified conditions. A workspace can have at most 25 autoresponders. Requires the `autoresponder:create` permission. """ createAutoresponder(input: CreateAutoresponderInput!): CreateAutoresponderOutput! """ Update one or more fields on an existing autoresponder. Only fields provided in the input are changed; omitted fields retain their current values. Requires the `autoresponder:edit` permission. """ updateAutoresponder(input: UpdateAutoresponderInput!): UpdateAutoresponderOutput! """ Permanently delete an autoresponder. The deleted autoresponder is returned in the response. Requires the `autoresponder:delete` permission. """ deleteAutoresponder(input: DeleteAutoresponderInput!): DeleteAutoresponderOutput! """ Set new order values for one or more autoresponders. You must pass a unique `order` integer and a unique autoresponder ID for each entry; duplicate IDs or duplicate order values are rejected. Only the autoresponders included in the input are repositioned — others are left unchanged. Requires the `autoresponder:edit` permission. """ reorderAutoresponders(input: ReorderAutorespondersInput!): ReorderAutorespondersOutput! """ Creates a new tenant or updates an existing one identified by `externalId` or `tenantId`. Use this to keep Plain's tenant records in sync with the groups or organisations in your own product. The `result` field on the output indicates whether a record was created or updated. Requires the `tenant:read` and `tenant:create` permissions. """ upsertTenant(input: UpsertTenantInput!): UpsertTenantOutput! """ Permanently deletes a tenant, unlinking it from all customers and removing its fields. Threads that were associated with the tenant retain a tombstone reference but are no longer routed through it. The tenant can be identified by either its Plain `tenantId` or its `externalId`. Requires the `tenant:delete` permission. """ deleteTenant(input: DeleteTenantInput!): DeleteTenantOutput! """ Adds a customer to one or more tenants. The customer can be identified by their Plain ID, external ID, or email address. If the customer is already a member of a given tenant the operation is a no-op for that tenant. Requires the `customer:edit` and `customerTenantMembership:create` permissions. """ addCustomerToTenants(input: AddCustomerToTenantsInput!): AddCustomerToTenantsOutput! """ Removes a customer from one or more tenants. The customer can be identified by their Plain ID, external ID, or email address. If the customer is not currently a member of a specified tenant the operation is a no-op for that tenant. Requires the `customer:edit` and `customerTenantMembership:delete` permissions. """ removeCustomerFromTenants(input: RemoveCustomerFromTenantsInput!): RemoveCustomerFromTenantsOutput! """ Replaces the full set of tenant memberships for a customer in a single call. Any tenants not included in the input are removed; any new ones are added. Use this when syncing tenant membership from your own system rather than tracking individual add/remove changes. Requires the `customer:edit`, `customerTenantMembership:create`, and `customerTenantMembership:delete` permissions. """ setCustomerTenants(input: SetCustomerTenantsInput!): SetCustomerTenantsOutput! """ Creates or updates one or more tenant field schemas in a single call. Each schema is identified by the combination of `source` and `externalFieldId` — if a schema with that pair already exists it is updated, otherwise a new one is created. Requires `tenantFieldSchema:create` or `tenantFieldSchema:edit` permission. """ upsertTenantFieldSchema(input: UpsertTenantFieldSchemaInput!): UpsertTenantFieldSchemaOutput! """ Permanently deletes a tenant field schema and all field values stored against it across all tenants. This action cannot be undone. Requires `tenantFieldSchema:delete` permission. """ deleteTenantFieldSchema(input: DeleteTenantFieldSchemaInput!): DeleteTenantFieldSchemaOutput! """ Sets or updates a field value for a specific tenant. Identify the target field using `tenantFieldIdentifier` (tenant ID + external field ID) and pass exactly one value argument matching the schema's `type` (e.g. `stringValue`, `numberValue`, `booleanValue`, `arrayValue`, `dateValue`, or `userReferenceValues`). Requires `tenant:edit` permission. """ upsertTenantField(input: UpsertTenantFieldInput!): UpsertTenantFieldOutput! """ Clears a tenant's value for a specific field without removing the field schema itself. Use this to unset a field value while keeping the schema available for other tenants. Requires `tenant:edit` permission. """ deleteTenantField(input: DeleteTenantFieldInput!): DeleteTenantFieldOutput! """ Maps a tenant field schema to a built-in Plain concept such as `TIER`, enabling automatic tier assignment based on the field's value. Only one schema can be mapped to a given concept at a time. """ setupTenantFieldSchemaMapping(input: SetupTenantFieldSchemaMappingInput!): SetupTenantFieldSchemaMappingOutput! """ Removes the mapping between a tenant field schema and a built-in Plain concept, disabling any automatic behaviour (such as tier assignment) driven by that field. """ removeTenantFieldSchemaMapping(input: RemoveTenantFieldSchemaMappingInput!): RemoveTenantFieldSchemaMappingOutput! """ Creates a new company or updates an existing one identified by `companyId` or `companyDomainName`. The output includes a `result` field of either `CREATED` or `UPDATED` so you can tell which happened. You can pass a bare domain (e.g. `plain.com`) or a full URL and Plain will extract the domain. Requires the `company:create` and `company:edit` permissions. """ upsertCompany(input: UpsertCompanyInput!): UpsertCompanyOutput! """ Deletes a company identified by `companyId` or `companyDomainName`. Deleting a company unlinks it from all of its customers — the customers themselves are not deleted. Requires the `company:delete` permission. """ deleteCompany(input: DeleteCompanyInput!): DeleteCompanyOutput! """ Begin the OAuth authorization flow for a third-party service integration. Returns connection details including a serviceAuthorizationId and an HMAC digest that must be passed to the authorization URL to securely link the callback back to this workspace. After the user completes the OAuth flow in the third-party service, call completeServiceAuthorization to finalize the connection. """ startServiceAuthorization(input: StartServiceAuthorizationInput!): StartServiceAuthorizationOutput! """ Finalize a service authorization after the user has completed the OAuth flow in the third-party service. For most services, pass the serviceAuthorizationId returned by startServiceAuthorization. For Jira, also provide the jira field with the refreshToken and siteId obtained from Atlassian's OAuth callback. On success, the authorization transitions to CONNECTED status and is ready for use. """ completeServiceAuthorization(input: CompleteServiceAuthorizationInput!): CompleteServiceAuthorizationOutput! """ Delete a workspace-level service authorization and revoke the associated credentials. Any import job definitions linked to this authorization are also disabled. This operation is permanent; to reconnect the service, start a new authorization flow. """ deleteServiceAuthorization(input: DeleteServiceAuthorizationInput!): DeleteServiceAuthorizationOutput! """ Delete the current user's personal service authorization credentials (currently supported for Jira only). This removes the user's personal OAuth token but leaves the workspace-level Jira authorization intact. The primary Jira token cannot be deleted; another user must first become the primary before this user's token can be removed. """ deleteMyServiceAuthorization(input: DeleteMyServiceAuthorizationInput!): DeleteMyServiceAuthorizationOutput! """ Updates which GitHub repositories Sidekick has access to and the operating instructions (workspace-level and per-repo) that guide its behavior. Replaces the full repo selection — omitting a repo removes it. Requires the GitHub service authorization to exist first. """ updateSidekickGithubConfig(input: UpdateSidekickGithubConfigInput!): UpdateSidekickGithubConfigOutput! """ Updates the operating instructions that guide how Sidekick uses a connected service (Datadog, Sentry, Grafana, Linear, Notion, incident.io, Attio, HubSpot, Jira, Granola, LaunchDarkly or Grain), identified by its serviceAuthorizationId. Pass null or an empty string to clear the instructions. GitHub and PostHog have their own configuration shapes — use updateSidekickGithubConfig / updateSidekickPosthogConfig instead. """ updateSidekickServiceConfig(input: UpdateSidekickServiceConfigInput!): UpdateSidekickServiceConfigOutput! """ Updates the operating instructions and default project that guide how Sidekick uses the PostHog integration. Pass null or an empty string to clear either field. """ updateSidekickPosthogConfig(input: UpdateSidekickPosthogConfigInput!): UpdateSidekickPosthogConfigOutput! """ Updates workspace-level Sidekick settings, such as the custom prompt appended to every session's system prompt. Pass null or an empty string for customPrompt to clear any existing value. """ updateSidekickSettings(input: UpdateSidekickSettingsInput!): UpdateSidekickSettingsOutput! """ Set the approval mode for a single Sidekick tool in this workspace. Persisted as a sparse override; setting a tool to its factory default clears the override. """ updateAgentSandboxToolPolicy(input: UpdateAgentSandboxToolPolicyInput!): UpdateAgentSandboxToolPolicyOutput! """ Create an import job definition that continuously syncs tenant field schemas, tenants, and customers from a connected external service (Attio, HubSpot, or Salesforce). Only one enabled import job definition can exist per service authorization at a time; calling this when one is already enabled returns an error. Use updateImportJobDefinition to disable an existing definition before creating a new one. """ createImportSync(input: CreateImportSyncInput!): CreateImportSyncOutput! """ Fetches tenant field schemas from a connected external service (identified by `serviceIntegrationKey`) and upserts them into Plain. The service must already be authorized and connected. Returns the full list of schemas that were imported. """ importTenantFieldSchemasFromService(input: ImportTenantFieldSchemasFromServiceInput!): ImportTenantFieldSchemasFromServiceOutput! """ Disable the active import job definition for a service integration. This is the only supported update — pass isEnabled: false to stop future sync runs. Returns an error if no enabled definition exists for the given service. """ updateImportJobDefinition(input: UpdateImportJobDefinitionInput!): UpdateImportJobDefinitionOutput! """ Creates a new tier. You can optionally add tenant and company members at creation time. Requires the `tier:create` permission. """ createTier(input: CreateTierInput!): CreateTierOutput! """ Updates the name, color, external ID, default priority, or default flag of an existing tier. Only the fields you provide are changed. Requires the `tier:update` permission. """ updateTier(input: UpdateTierInput!): UpdateTierOutput! """ Deletes a tier permanently. All tenant and company memberships in that tier are also removed. Requires the `tier:delete` permission. """ deleteTier(input: DeleteTierInput!): DeleteTierOutput! """ Create a service level agreement (SLA) for a tier. Each SLA commits your team to responding within a specified time window and can be scoped by thread priority and/or label type. Provide either `firstResponseTimeMinutes` (time from thread creation to first reply) or `nextResponseTimeMinutes` (time from each customer message to the next reply), but not both in a single call. A `nextResponseTimeMinutes` SLA can only be created if a `firstResponseTimeMinutes` SLA already exists on the same tier. Requires the `serviceLevelAgreement:create` permission. """ createServiceLevelAgreement(input: CreateServiceLevelAgreementInput!): CreateServiceLevelAgreementOutput! """ Update an existing SLA's time target, priority or label filter, business-hours setting, or breach actions. You cannot change an SLA's type (first-response vs. next-response) after creation. Use the field-level wrapper inputs (e.g. `{ "value": 60 }`) for the fields you want to change and omit the rest. Requires the `serviceLevelAgreement:edit` permission. """ updateServiceLevelAgreement(input: UpdateServiceLevelAgreementInput!): UpdateServiceLevelAgreementOutput! """ Delete an SLA from a tier. A first-response-time SLA cannot be deleted while a next-response-time SLA still exists on the same tier — delete the next-response SLA first. Returns the deleted SLA on success. Requires the `serviceLevelAgreement:delete` permission. """ deleteServiceLevelAgreement(input: DeleteServiceLevelAgreementInput!): DeleteServiceLevelAgreementOutput! """ Adds one or more tenants or companies to a tier (up to 25 per call). Because each tenant or company can belong to only one tier at a time, adding a member that already belongs to another tier will move it to this tier. Requires the `tierMembership:read` and `tierMembership:create` permissions. """ addMembersToTier(input: AddMembersToTierInput!): AddMembersToTierOutput! """ Removes one or more tenants or companies from their current tier (up to 25 per call). After removal the member has no tier. Requires the `tierMembership:read` and `tierMembership:delete` permissions. """ removeMembersFromTier(input: RemoveMembersFromTierInput!): RemoveMembersFromTierOutput! """ Sets the tier for a single company, identified by its Plain ID or domain name. Pass a null `tierIdentifier` to remove the company from its current tier. Use `addMembersToTier` when you need to move multiple companies at once. Requires the `tierMembership:read` and `tierMembership:create` permissions. """ updateCompanyTier(input: UpdateCompanyTierInput!): UpdateCompanyTierOutput! """ Sets the tier for a single tenant, identified by its Plain ID or external ID. Pass a null `tierIdentifier` to remove the tenant from its current tier. Use `addMembersToTier` when you need to move multiple tenants at once. Requires the `tierMembership:read` and `tierMembership:create` permissions. """ updateTenantTier(input: UpdateTenantTierInput!): UpdateTenantTierOutput! """ Create or update the workspace's business hours using a per-weekday schedule. Deprecated — use syncBusinessHoursSlots instead, which supports multiple timezones and overlapping slots. """ upsertBusinessHours(input: UpsertBusinessHoursInput!): UpsertBusinessHoursOutput! @deprecated(reason: "Use syncBusinessHoursSlots instead.") """ Delete the workspace's business hours configuration. Deprecated — call syncBusinessHoursSlots with an empty slots array to clear business hours instead. """ deleteBusinessHours: DeleteBusinessHoursOutput! @deprecated(reason: "Use syncBusinessHoursSlots instead.") """ Replace the workspace's business hours with the provided set of slots. Each slot defines a weekday, a timezone, and open/close times (HH:MM format). Passing an empty array clears all business hours. This mutation is idempotent and completely replaces any existing slots, including any previously set via the deprecated upsertBusinessHours mutation. Requires the `businessHours:edit` permission. """ syncBusinessHoursSlots(input: SyncBusinessHoursSlotsInput!): SyncBusinessHoursSlotsOutput! """ Replace a user's working hours schedule in full. When `isEnabled` is true, the user's status is automatically switched to ONLINE at the start of each slot and to OFFLINE or AWAY (depending on workspace settings) at the end. Omit `userId` to configure the currently authenticated user's own schedule. """ syncUserWorkingHours(input: SyncUserWorkingHoursInput!): SyncUserWorkingHoursOutput! """ Creates a Hyperline-hosted checkout session for upgrading to a paid plan. Returns a URL that you redirect the user to in order to complete payment. Requires the `billing:edit` permission. """ createHyperlineCheckoutSession(input: CreateHyperlineCheckoutSessionInput!): CreateHyperlineCheckoutSessionOutput! """ Creates a Hyperline billing portal session and returns a URL where the user can manage their subscription, invoices, and payment methods. Requires the `billing:edit` permission. """ createHyperlineBillingPortalSession: CreateHyperlineBillingPortalSessionOutput! """ Creates a short-lived authentication token for embedding Hyperline billing UI components directly in your own interface. Requires the `billing:edit` permission. """ createHyperlineComponentsAuthToken: CreateHyperlineComponentsAuthTokenOutput! """ Cancels the workspace's active Hyperline subscription. The subscription remains active until the end of the current billing period. Requires the `billing:edit` permission. """ cancelHyperlineSubscription: CancelHyperlineSubscriptionOutput! """ Calculates the billing cost delta of changing a user's role or seat type, including prorated and full-period amounts. Use this before making a role change to show the user an accurate cost preview. """ calculateRoleChangeCost(input: CalculateRoleChangeCostInput!): CalculateRoleChangeCostOutput! """ Moves a user onto the active billing rota, marking them as currently consuming an eng-rota seat. The user must already hold a billing rota seat. Requires the `billingSeat:edit` permission. """ addUserToActiveBillingRota(input: AddUserToActiveBillingRotaInput!): AddUserToActiveBillingRotaOutput! """ Moves a user off the active billing rota, freeing their eng-rota seat for another team member. The user must already hold a billing rota seat. Requires the `billingSeat:edit` permission. """ removeUserFromActiveBillingRota(input: RemoveUserFromActiveBillingRotaInput!): RemoveUserFromActiveBillingRotaOutput! """ Atomically adds and removes multiple users from the active billing rota in a single call. At least one user must be added or removed; the same user cannot appear in both lists. Requires the `billingSeat:edit` permission. """ updateActiveBillingRota(input: UpdateActiveBillingRotaInput!): UpdateActiveBillingRotaOutput! """ Switches the workspace to a different billing plan. Use `previewBillingPlanChange` first to show the user the cost impact before applying the change. """ changeBillingPlan(input: ChangeBillingPlanInput!): ChangeBillingPlanOutput! """ Returns a cost preview for switching to a different billing plan, including the immediate charge and the earliest date the change can take effect. No changes are made to the subscription. """ previewBillingPlanChange(input: PreviewBillingPlanChangeInput!): PreviewBillingPlanChangeOutput! """ Purchases a top-up credit bundle for AI features. The count must match the unit count of a configured top-up bundle. Returns the updated top-up credit balance after purchase. Requires the `billing:edit` permission. """ purchaseCredits(input: PurchaseCreditsInput!): PurchaseCreditsOutput! """ Generates a new HMAC secret for the workspace, replacing any existing secret. Plain includes this secret as a signature header on outbound HTTP requests (e.g. workflow rule HTTP steps) so your server can verify the request originated from Plain. Requires the `workspaceHmac:edit` permission. """ regenerateWorkspaceHmac: RegenerateWorkspaceHmacOutput! """ Manually add a single URL as an indexed document within an existing knowledge source. The document is fetched and queued for indexing asynchronously. Requires Plain AI to be enabled on the workspace. """ createIndexedDocument(input: CreateIndexedDocumentInput!): CreateIndexedDocumentOutput! """ Record teammate feedback (thumbs up / thumbs down and an optional comment) on a generated reply. Use this to signal whether a suggestion was helpful so Plain can improve future suggestions. Requires the `generatedReply:edit` permission. """ updateGeneratedReply(input: UpdateGeneratedReplyInput!): UpdateGeneratedReplyOutput! """ Create a new knowledge source that Plain AI will index and use when generating replies. Use type `SITEMAP` to crawl all URLs listed in a sitemap XML file, or type `URL` to index a single page. Requires Plain AI to be enabled on the workspace. """ createKnowledgeSource(input: CreateKnowledgeSourceInput!): CreateKnowledgeSourceOutput! """ Delete a knowledge source and remove it from Plain AI's index. This is idempotent — deleting a knowledge source that does not exist returns no error. """ deleteKnowledgeSource(input: DeleteKnowledgeSourceInput!): DeleteKnowledgeSourceOutput! """ Trigger an immediate re-index of a knowledge source. Use this when the source content has changed and you do not want to wait for the automatic weekly re-index. The knowledge source status is reset to pending and indexing is queued asynchronously. Requires Plain AI to be enabled on the workspace. """ reindexKnowledgeSource(input: ReindexKnowledgeSourceInput!): ReindexKnowledgeSourceOutput! """ Associates a connected Slack channel with a company or tenant so that threads from that channel are automatically routed to the correct customer context. Requires either a company or tenant identifier (or both). Returns the existing association if the channel is already linked to the same target; returns an error if the channel is already linked to a different target. Requires the `threadChannelAssociation:create` permission. """ createThreadChannelAssociation(input: CreateThreadChannelAssociationInput!): CreateThreadChannelAssociationOutput! """ Removes a thread channel association, unlinking the connected Slack channel from its associated company or tenant. Requires the `threadChannelAssociation:delete` permission. """ deleteThreadChannelAssociation(input: DeleteThreadChannelAssociationInput!): DeleteThreadChannelAssociationOutput! """ Begin a two-step file upload. Returns a pre-signed S3 form URL and form data fields that you POST the raw file bytes to directly from the client. The upload URL expires in 2 hours. Files must be 50 MB or smaller; certain executable file extensions (e.g. .exe, .bat) are rejected. Requires the `workspaceFile:create` permission. """ createWorkspaceFileUploadUrl(input: CreateWorkspaceFileUploadUrlInput!): CreateWorkspaceFileUploadUrlOutput! """ Generate a fresh download URL for a workspace file. For PRIVATE files, returns a short-lived pre-signed URL (valid for 3 minutes); for PUBLIC files, returns a permanent CDN URL with no expiry. Call this whenever you need to give a user access to a file rather than caching URLs, as private URLs expire quickly. """ createWorkspaceFileDownloadUrl(input: CreateWorkspaceFileDownloadUrlInput!): CreateWorkspaceFileDownloadUrlOutput! """ Delete a workspace file by ID. Requires the `workspaceFile:create` permission. """ deleteWorkspaceFile(input: DeleteWorkspaceFileInput!): DeleteWorkspaceFileOutput! """ Finds or creates a Plain customer associated with a Slack channel, using the Slack users in that channel to identify the customer. Useful for bootstrapping threads from Slack channels. """ resolveCustomerForSlackChannel(input: ResolveCustomerForSlackChannelInput!): ResolveCustomerForSlackChannelOutput! """ Finds or creates a Plain customer associated with the Microsoft Teams users in the given channel, using the workspace's connected MS Teams integration. Useful for bootstrapping a support thread from a Teams channel conversation. """ resolveCustomerForMSTeamsChannel(input: ResolveCustomerForMSTeamsChannelInput!): ResolveCustomerForMSTeamsChannelOutput! """ Create a new help center with a subdomain, branding, and access settings. Requires the helpCenter:edit permission. """ createHelpCenter(input: CreateHelpCenterInput!): CreateHelpCenterOutput! """ Update the settings, branding, or access configuration of an existing help center. Only fields that are provided are updated. Requires the helpCenter:edit permission. """ updateHelpCenter(input: UpdateHelpCenterInput!): UpdateHelpCenterOutput! """ Permanently delete a help center and all its articles and article groups. This action cannot be undone. Requires the helpCenter:edit permission. """ deleteHelpCenter(input: DeleteHelpCenterInput!): DeleteHelpCenterOutput! """ Set or clear the custom domain name for a help center. After setting, use verifyHelpCenterCustomDomainName to confirm DNS propagation. Requires the helpCenter:edit permission. """ updateHelpCenterCustomDomainName(input: UpdateHelpCenterCustomDomainNameInput!): UpdateHelpCenterCustomDomainNameOutput! """ Trigger a DNS verification check for the custom domain configured on the help center. Returns an error if the expected TXT record is not yet visible. Requires the helpCenter:edit permission. """ verifyHelpCenterCustomDomainName(input: VerifyHelpCenterCustomDomainNameInput!): VerifyHelpCenterCustomDomainNameOutput! """ Replace the navigation index (sidebar order and hierarchy) of a help center in a single call. You must supply the hash returned by the helpCenterIndex query — if the index has changed since you read it, the call will fail so you can re-fetch and re-apply your changes. Requires the helpCenter:edit permission. """ updateHelpCenterIndex(input: UpdateHelpCenterIndexInput!): UpdateHelpCenterIndexOutput! """ Create or update a help center article. Omit helpCenterArticleId to create a new article; provide it to update an existing one. The slug is normalized to lowercase and must be unique within the help center. contentHtml is rendered directly in the help center. status defaults to DRAFT when not provided. Requires the helpCenter:edit permission. """ upsertHelpCenterArticle(input: UpsertHelpCenterArticleInput!): UpsertHelpCenterArticleOutput! """ Permanently delete a help center article. Requires the helpCenter:edit permission. """ deleteHelpCenterArticle(input: DeleteHelpCenterArticleInput!): DeleteHelpCenterArticleOutput! """ Use AI to generate one or more draft help center articles from the content of a support thread. Returns the generated articles in DRAFT status for review before publishing. Requires the helpCenter:edit permission. """ generateHelpCenterArticle(input: GenerateHelpCenterArticleInput!): GenerateHelpCenterArticleOutput! """ Create a new article group (folder) within a help center. Pass parentHelpCenterArticleGroupId to nest the group inside an existing group. Requires the helpCenter:edit permission. """ createHelpCenterArticleGroup(input: CreateHelpCenterArticleGroupInput!): CreateHelpCenterArticleGroupOutput! """ Update the name of an existing article group. Requires the helpCenter:edit permission. """ updateHelpCenterArticleGroup(input: UpdateHelpCenterArticleGroupInput!): UpdateHelpCenterArticleGroupOutput! """ Delete an article group. Articles that belonged to the group are not deleted — they become ungrouped. To remove articles entirely, use deleteHelpCenterArticle. Requires the helpCenter:edit permission. """ deleteHelpCenterArticleGroup(input: DeleteHelpCenterArticleGroupInput!): DeleteHelpCenterArticleGroupOutput! """ Create a new issue in a connected issue tracker (Shortcut, Rootly, incident.io, or GitHub) and return a `ThreadLinkCandidate` that can immediately be used with `createThreadLink` to link it to a thread. The `fields` array must include all required fields for the chosen tracker (use `issueTrackerFields` to discover which fields are required and their allowed values). For GitHub, the acting user must have a personal GitHub user auth integration set up via `createGithubUserAuthIntegration`; the issue is created on their behalf. Requires the `threadLink:create` permission. """ createIssueTrackerIssue(input: CreateIssueTrackerIssueInput!): CreateIssueTrackerIssueOutput! """ Creates a new customer survey (e.g. a CSAT survey) in the workspace. Requires the `customerSurvey:create` permission and a billing entitlement for customer surveys. You must supply a template (currently only `csatTemplate` is supported) and can optionally specify targeting conditions, an enabled state, a send delay, and a per-customer cooldown interval. """ createCustomerSurvey(input: CreateCustomerSurveyInput!): CreateCustomerSurveyOutput! """ Updates an existing customer survey. At least one field must be provided. Partial updates are supported — only the fields you include are changed. Requires the `customerSurvey:edit` permission. """ updateCustomerSurvey(input: UpdateCustomerSurveyInput!): UpdateCustomerSurveyOutput! """ Permanently deletes a customer survey. Returns an error if the survey does not exist. Requires the `customerSurvey:delete` permission. """ deleteCustomerSurvey(input: DeleteCustomerSurveyInput!): DeleteCustomerSurveyOutput! """ Updates the display order of one or more customer surveys in a single call. Pass an array of survey ID / order-index pairs; only surveys whose order actually changes are updated. Duplicate survey IDs in the input are rejected. Requires the `customerSurvey:edit` permission. """ reorderCustomerSurveys(input: ReorderCustomerSurveysInput!): ReorderCustomerSurveysOutput! """ Programmatically add a suggested reply to a thread. The reply is surfaced to teammates in Plain so they can review, edit, and send it — the customer sees nothing until a teammate explicitly sends it. The `timelineEntryId` must reference a customer message on the thread. The `markdown` field is capped at 5,000 characters. Requires the `generatedReply:create` permission. """ addGeneratedReply(input: AddGeneratedReplyInput!): AddGeneratedReplyOutput! """ Advance a thread to the next step in its escalation path. The thread must already have an escalation path attached (via `updateThreadEscalationPath`) — if not, this call returns an error. Requires the `thread:edit` permission. """ escalateThread(input: EscalateThreadInput!): EscalateThreadOutput! """ Submit feedback on an AI feature result (e.g. an AI agent reply or a thread catchup). Deprecated — use `createAiFeedback` instead. """ createAiFeatureFeedback(input: CreateAiFeatureFeedbackInput!): CreateAiFeatureFeedbackOutput! @deprecated(reason: "Use createAiFeedback instead") """ Submit feedback on any Plain AI feature result. Pass a JSON-encoded object in the `feedback` field whose `type` discriminates the feature (`AI_AGENT`, `THREAD_CATCHUP`, `THREAD_CLUSTERS`, `TONE_RULE`, or `KNOWLEDGE_GAP`) and includes the relevant context fields and an optional `sentiment` (`POSITIVE`, `NEGATIVE`, or `NEUTRAL`). """ createAiFeedback(input: CreateAiFeedbackInput!): CreateAiFeedbackOutput! """ Uses AI to generate a set of tone rules from a free-text description of the desired communication style (up to 5000 characters). The generated rules are saved to the workspace in a disabled state so you can review and selectively enable them. Returns the newly created rules. Requires the `aiToneRule:create` permission. """ generateAiToneRulesFromDescription(input: GenerateAiToneRulesFromDescriptionInput!): GenerateAiToneRulesFromDescriptionOutput! """ Creates a single AI tone rule with the given category and description. Rules are enabled by default; pass `isEnabled: false` to create the rule in a disabled state. A workspace may have at most 7 simultaneously enabled rules across all categories. Requires the `aiToneRule:create` permission. """ createAiToneRule(input: CreateAiToneRuleInput!): CreateAiToneRuleOutput! """ Updates up to 10 AI tone rules in a single call. Each update item must include the rule ID and at least one field to change (`description` or `isEnabled`). Enabling a rule that would push the total number of enabled rules above 7 returns an error. Returns the full updated rule objects. Requires the `aiToneRule:edit` permission. """ updateAiToneRules(input: UpdateAiToneRulesInput!): UpdateAiToneRulesOutput! """ Permanently deletes one or more AI tone rules by ID. If any supplied ID does not exist the entire operation fails and no rules are deleted. Requires the `aiToneRule:delete` permission. """ deleteAiToneRules(input: DeleteAiToneRulesInput!): DeleteAiToneRulesOutput! """ Marks one or more internal notifications as read, unread, or archived. Pass the notification IDs to update along with the desired `readAt` and/or `archivedAt` timestamps. Set `readAt` to null to mark a notification as unread; set `archivedAt` to a timestamp to archive it. Returns the updated notifications. Requires the `user:read` permission. """ updateInternalNotifications(input: UpdateInternalNotificationsInput!): UpdateInternalNotificationsOutput! """ Upsert up to 50 tenant field schemas from an external system using their externalFieldId for idempotency. Set isDeleted: true on a schema to mark it as deleted in Plain. Returns an ImportResult with added, updated, and skipped counts. """ importTenantFieldSchemas(input: ImportTenantFieldSchemasInput!): ImportTenantFieldSchemasOutput! """ Upsert up to 25 tenants from an external system using their externalId for idempotency. Optionally include tenant field values to sync custom fields alongside the tenant record. Returns an ImportResult with added, updated, and skipped counts. """ importTenants(input: ImportTenantsInput!): ImportTenantsOutput! """ Upsert up to 25 customers from an external system using their externalId for idempotency. Each customer can include an optional list of tenants to associate with. Returns an ImportResult with added, updated, and skipped counts. """ importCustomers(input: ImportCustomersInput!): ImportCustomersOutput! """ Update the status of a knowledge gap to OPEN, RESOLVED, or IGNORED. This operation is idempotent: if the gap already has the requested status, it is returned without modification and no event is emitted. Requires the `knowledgeGap:edit` permission. """ changeKnowledgeGapStatus(input: ChangeKnowledgeGapStatusInput!): ChangeKnowledgeGapStatusOutput! } input CreateAiFeatureFeedbackInput { feature: String! aiAgentFeedback: AiAgentFeedbackDetailsInput threadCatchupFeedback: ThreadCatchupFeedbackDetailsInput } input AiAgentFeedbackDetailsInput { reason: String! comment: String threadId: ID! timelineEntryId: ID! } input ThreadCatchupFeedbackDetailsInput { reason: String! comment: String threadId: ID! catchupDetailGeneratedAt: String } type CreateAiFeatureFeedbackOutput { aiFeatureFeedback: AiFeatureFeedbackOutput error: MutationError } enum AiFeedbackSentiment { POSITIVE NEGATIVE NEUTRAL } type AiAgentFeedbackDetails { reason: String! comment: String sentiment: AiFeedbackSentiment threadId: ID! timelineEntryId: ID! } type ThreadCatchupFeedbackDetails { reason: String! comment: String sentiment: AiFeedbackSentiment threadId: ID! } type ThreadClustersFeedbackDetails { reason: String! comment: String sentiment: AiFeedbackSentiment clusterId: ID! } type ToneRuleFeedbackDetails { reason: String! comment: String sentiment: AiFeedbackSentiment toneRuleId: ID! toneRuleInput: String! toneRuleDescription: String! } type KnowledgeGapFeedbackDetails { reason: String! comment: String sentiment: AiFeedbackSentiment knowledgeGapId: ID! } union AiFeatureFeedbackDetails = AiAgentFeedbackDetails | ThreadCatchupFeedbackDetails | ThreadClustersFeedbackDetails | ToneRuleFeedbackDetails | KnowledgeGapFeedbackDetails type AiFeatureFeedbackOutput { id: ID! feature: String! details: AiFeatureFeedbackDetails! } input CreateAiFeedbackInput { feedback: String! } type CreateAiFeedbackOutput { error: MutationError } type AiToneRule { id: ID! """ The aspect of communication style this rule governs. One of `EMPATHY`, `LANGUAGE`, `FORMALITY`, `PERSONALITY`, or `WARMTH`. """ category: String! """ The plain-text instruction that Plain's AI will follow when composing replies (up to 5000 characters). """ description: String! """ Whether this rule is currently active and will be applied to AI-generated replies. A workspace may have at most 7 enabled rules at a time. """ isEnabled: Boolean! createdAt: DateTime! """The user or machine user who created this rule.""" createdBy: InternalActor! updatedAt: DateTime! """The user or machine user who last modified this rule.""" updatedBy: InternalActor! } input AiToneRulesFilter { isEnabled: Boolean } type AiToneRuleConnection { edges: [AiToneRuleEdge!]! pageInfo: PageInfo! } type AiToneRuleEdge { cursor: String! node: AiToneRule! } input GenerateAiToneRulesFromDescriptionInput { description: String! } type GenerateAiToneRulesFromDescriptionOutput { aiToneRules: [AiToneRule!]! error: MutationError } input CreateAiToneRuleInput { category: String! description: String! isEnabled: Boolean } type CreateAiToneRuleOutput { aiToneRule: AiToneRule error: MutationError } input UpdateAiToneRulesInput { updates: [AiToneRuleUpdate!]! } input AiToneRuleUpdate { aiToneRuleId: ID! description: String isEnabled: Boolean } type UpdateAiToneRulesOutput { aiToneRules: [AiToneRule!]! error: MutationError } input DeleteAiToneRulesInput { aiToneRuleIds: [ID!]! } type DeleteAiToneRulesOutput { error: MutationError } input EscalateThreadInput { threadId: ID! } type EscalateThreadOutput { thread: Thread error: MutationError } input MoveLabelTypeInput { labelTypeId: ID! """ Move the label type immediately after the label type with the given ID. Required if beforeLabelTypeId is not provided. """ afterLabelTypeId: ID """ Move the label type immediately before the label type with the given ID. Required if afterLabelTypeId is not provided. """ beforeLabelTypeId: ID """ Move the label type to be a child of the specified parent. When provided alone, the label will be moved to the end of the parent's children. When provided with afterLabelTypeId or beforeLabelTypeId, validates that the reference label has the same parent. """ parentLabelTypeId: ID } type MoveLabelTypeOutput { labelType: LabelType error: MutationError } input GenerateHelpCenterArticleInput { threadId: ID! helpCenterId: ID! } type GenerateHelpCenterArticleOutput { helpCenterArticles: [HelpCenterArticle!]! error: MutationError } type DeleteCompanyOutput { company: Company error: MutationError } input DeleteCompanyInput { companyIdentifier: CompanyIdentifierInput! } input HelpCenterIndexItemInput { type: HelpCenterIndexItemType! entityId: ID! parentId: ID """Inline label for HEADING items. Ignored for ARTICLE / ARTICLE_GROUP.""" title: String } input UpdateHelpCenterIndexInput { helpCenterId: ID! hash: String! helpCenterIndex: [HelpCenterIndexItemInput!]! } type UpdateHelpCenterIndexOutput { helpCenterIndex: HelpCenterIndex error: MutationError } input UpdateHelpCenterCustomDomainNameInput { helpCenterId: ID! customDomainName: OptionalStringInput! } input VerifyHelpCenterCustomDomainNameInput { helpCenterId: ID! } type UpdateHelpCenterCustomDomainNameOutput { helpCenter: HelpCenter error: MutationError } type VerifyHelpCenterCustomDomainNameOutput { helpCenter: HelpCenter error: MutationError } enum GeneratedReplyFeedbackType { POSITIVE NEGATIVE UNKNOWN } input OptionalGeneratedReplyFeedbackInput { value: GeneratedReplyFeedbackType } input AddGeneratedReplyInput { threadId: ID! timelineEntryId: ID! markdown: String! } type AddGeneratedReplyOutput { generatedReply: GeneratedReply error: MutationError } input GeneratedReplyFeedbackInput { type: OptionalGeneratedReplyFeedbackInput comment: String } input UpdateGeneratedReplyInput { generatedReplyId: ID! feedback: GeneratedReplyFeedbackInput } type UpdateGeneratedReplyOutput { generatedReply: GeneratedReply error: MutationError } input CreateIndexedDocumentInput { url: String! labelTypeIds: [ID!] knowledgeSourceId: ID! } type CreateIndexedDocumentOutput { error: MutationError indexedDocument: IndexedDocument } type IndexedDocument { id: ID! url: String! """ Label types associated with this document, used to scope AI search results to specific topics. """ labelTypes: [LabelType!]! """ The current indexing status of this document: pending (queued), indexed (ready for AI), or failed. """ status: IndexedDocumentStatus! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } union IndexedDocumentStatus = IndexedDocumentStatusIndexed | IndexedDocumentStatusPending | IndexedDocumentStatusFailed type IndexedDocumentStatusIndexed { indexedAt: DateTime! indexedBy: InternalActor } type IndexedDocumentStatusPending { startedAt: DateTime! } type IndexedDocumentStatusFailed { reason: String! failedAt: DateTime! } type IndexedDocumentConnection { edges: [IndexedDocumentEdge!]! pageInfo: PageInfo! } type IndexedDocumentEdge { cursor: String! node: IndexedDocument! } input IndexedDocumentsFilter { knowledgeSourceId: ID } input CreateKnowledgeSourceInput { url: String! labelTypeIds: [ID!] type: KnowledgeSourceType! } input DeleteKnowledgeSourceInput { knowledgeSourceId: ID! } type DeleteKnowledgeSourceOutput { error: MutationError } input ReindexKnowledgeSourceInput { knowledgeSourceId: ID! } type ReindexKnowledgeSourceOutput { knowledgeSource: KnowledgeSource error: MutationError } enum KnowledgeSourceType { """ A knowledge source backed by a sitemap XML file. Plain crawls and indexes every URL listed in the sitemap. """ SITEMAP """ A knowledge source backed by a single URL. Plain fetches and indexes the content of that page. """ URL } type CreateKnowledgeSourceOutput { knowledgeSource: KnowledgeSource error: MutationError } input KnowledgeSourcesFilter { type: KnowledgeSourceType } type KnowledgeSourceConnection { edges: [KnowledgeSourceEdge!]! pageInfo: PageInfo! } type KnowledgeSourceEdge { cursor: String! node: KnowledgeSource! } union KnowledgeSource = KnowledgeSourceSitemap | KnowledgeSourceUrl type KnowledgeSourceSitemap { id: ID! type: KnowledgeSourceType! url: String! """ Label types associated with this knowledge source, used to scope AI search results to specific topics. """ labelTypes: [LabelType!]! """ The current indexing status of this knowledge source: pending (queued), indexed (ready for AI), or failed. """ status: IndexingStatus! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } type KnowledgeSourceUrl { id: ID! type: KnowledgeSourceType! url: String! """ Label types associated with this knowledge source, used to scope AI search results to specific topics. """ labelTypes: [LabelType!]! """ The current indexing status of this knowledge source: pending (queued), indexed (ready for AI), or failed. """ status: IndexingStatus! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } union IndexingStatus = IndexingStatusPending | IndexingStatusIndexed | IndexingStatusFailed type IndexingStatusPending { startedAt: DateTime! } type IndexingStatusIndexed { indexedAt: DateTime! indexedBy: InternalActor } type IndexingStatusFailed { reason: String! failedAt: DateTime! } enum KnowledgeSourceSearchResultType { """A result from a manually indexed document or sitemap-crawled page.""" INDEXED_DOCUMENT """A result from a published help center article.""" HELP_CENTER_ARTICLE } input SearchKnowledgeSourcesOptions { """ Restrict results to indexed documents that have at least one of these label types (or no labels). Has no effect on help center article results. """ labelTypeIds: [ID!] """ Restrict results to specific content types. Omit to search both indexed documents and help center articles. """ types: [KnowledgeSourceSearchResultType!] """ When true, includes help center articles from help centers that are not configured as AI-accessible. Defaults to false. """ includeNonCustomerFacing: Boolean } union KnowledgeSourceSearchResult = IndexedDocumentSearchResult | HelpCenterArticleSearchResult type IndexedDocumentSearchResult { """ The extracted text content of the matched document chunk, as indexed by Plain AI. """ content: String! indexedDocument: IndexedDocument! } type HelpCenterArticleSearchResult { """ The extracted text content of the matched article chunk, as indexed by Plain AI. """ content: String! helpCenterArticle: HelpCenterArticle! helpCenter: HelpCenter! } input ThreadDiscussionSlackDetailsInput { connectedSlackChannelId: ID! } input ThreadDiscussionEmailDetailsInput { toAddresses: [String!]! ccAddresses: [String!] } input ThreadDiscussionCursorDetailsInput { type: String! repositoryUrl: String } enum ThreadDiscussionType { EMAIL SLACK CURSOR_WORKSPACE_BACKGROUND_AGENT AGENT_SESSION } enum CreateThreadDiscussionType { EMAIL SLACK CURSOR_WORKSPACE_BACKGROUND_AGENT } enum DiscussionType { EMAIL SLACK CURSOR_WORKSPACE_BACKGROUND_AGENT AGENT_SESSION } enum DiscussionSourceEntityType { COMPANY TENANT LABEL CHANNEL TIER CUSTOMER_GROUP CUSTOMER } enum ThreadDiscussionMessageType { """ A message received from an external participant (e.g. a Slack reply from someone outside Plain). """ INBOUND """A message sent by a Plain workspace member.""" OUTBOUND } input CreateThreadDiscussionInput { threadId: ID! """ The markdown content of the first message. When slackBlocks are provided, this is used as the fallback content rendered in Plain's UI. """ markdownContent: String! attachmentIds: [ID!] type: CreateThreadDiscussionType! slackDetails: ThreadDiscussionSlackDetailsInput emailDetails: ThreadDiscussionEmailDetailsInput cursorDetails: ThreadDiscussionCursorDetailsInput unfurlLinks: Boolean """ Slack Block Kit blocks (https://api.slack.com/block-kit) to send as the first message of the discussion. Only valid when type is SLACK. Provided as a JSON string holding an array of blocks. In Plain, the UI will fall back to the markdownContent property but will render Slack blocks in Slack. """ slackBlocks: String } input CreateDiscussionInput { threadId: ID markdownContent: String! """ Customer-scoped attachments. Used by EMAIL / SLACK / CURSOR discussions. Sidekick AGENT_SESSION discussions use workspaceFileIds instead. """ attachmentIds: [ID!] """ Workspace-scoped files. Used by Sidekick AGENT_SESSION discussions, which are not customer-scoped. Other discussion types ignore this field. """ workspaceFileIds: [ID!] type: DiscussionType! slackDetails: ThreadDiscussionSlackDetailsInput emailDetails: ThreadDiscussionEmailDetailsInput cursorDetails: ThreadDiscussionCursorDetailsInput """ The entity ID where this discussion was started (e.g. a company or customer ID). Used to provide context to agent sessions started outside of a thread. """ sourceEntityId: ID """ The entity type for sourceEntityId. Required when sourceEntityId is provided. """ sourceEntityType: DiscussionSourceEntityType """ The page path where this discussion was started (e.g. '/insights', '/settings'). Used to provide context when the user is on a page without a specific entity. """ sourcePageLink: String """ Free-form context from the UI (e.g. active filters on the current page) injected into the agent's prompt, so it knows what data the user is currently looking at. """ additionalContext: String } type CreateDiscussionOutput { discussion: ThreadDiscussion error: MutationError } type CreateThreadDiscussionOutput { threadDiscussion: ThreadDiscussion error: MutationError } enum ImportThreadDiscussionType { SLACK } input ImportThreadDiscussionInput { """The ID of the Plain thread to attach the discussion to.""" threadId: ID! """ The type of external source the discussion is imported from. Determines which *Details field on this input is required. """ type: ImportThreadDiscussionType! """Required when type is SLACK.""" slackDetails: ImportThreadDiscussionSlackDetailsInput } input ImportThreadDiscussionSlackDetailsInput { """ A Slack message permalink, e.g. https://workspace.slack.com/archives/C123ABC/p1234567890123456 The link can point to either the root message of a thread or any reply within it — in either case the entire Slack thread (root + all replies) is imported. """ slackMessageUrl: String! } type ImportThreadDiscussionOutput { threadDiscussion: ThreadDiscussion error: MutationError } input SendThreadDiscussionMessageInput { threadDiscussionId: ID! markdownContent: String! attachmentIds: [ID!] """ Channel-specific details for the message. Discriminated by type. Currently only SLACK is supported. """ channelDetails: SendThreadDiscussionMessageChannelDetailsInput } input SendThreadDiscussionMessageChannelDetailsInput { type: ThreadDiscussionType! slack: SendThreadDiscussionMessageSlackChannelDetailsInput } input SendThreadDiscussionMessageSlackChannelDetailsInput { """ JSON-encoded Slack Block Kit blocks. markdownContent is still required as the text fallback. """ slackBlocks: String """Slack should unfurl links in the reply. Defaults to true.""" unfurlLinks: Boolean """ The reply should also broadcast message into the parent Slack channel. Defaults to false. Cannot be combined with attachmentIds — Slack's file upload API has no broadcast equivalent, so the combination is rejected with input_validation. """ replyBroadcast: Boolean } type SendThreadDiscussionMessageOutput { threadDiscussionMessage: ThreadDiscussionMessage error: MutationError } input SendDiscussionMessageInput { discussionId: ID! markdownContent: String! """ Customer-scoped attachments. Used by EMAIL / SLACK / CURSOR discussions. Sidekick AGENT_SESSION discussions use workspaceFileIds instead. """ attachmentIds: [ID!] """Workspace-scoped files. Used by Sidekick AGENT_SESSION discussions.""" workspaceFileIds: [ID!] } type SendDiscussionMessageOutput { discussionMessage: ThreadDiscussionMessage error: MutationError } input MarkThreadDiscussionAsResolvedInput { threadDiscussionId: ID! } type MarkThreadDiscussionAsResolvedOutput { error: MutationError } input MarkThreadDiscussionReadInput { threadDiscussionId: ID! } type MarkThreadDiscussionReadOutput { threadDiscussion: ThreadDiscussion error: MutationError } input DeleteThreadDiscussionInput { threadDiscussionId: ID! } type DeleteThreadDiscussionOutput { error: MutationError } input ResolveAgentApprovalInput { leaseId: ID! decision: AgentApprovalDecision! reviewerNote: String } type ResolveAgentApprovalOutput { error: MutationError } input UpdateCompanyTierInput { tierIdentifier: TierIdentifierInput companyIdentifier: CompanyIdentifierInput! } type UpdateCompanyTierOutput { companyTierMembership: CompanyTierMembership error: MutationError } input ChangeThreadCustomerInput { threadId: ID! customerId: ID! } type ChangeThreadCustomerOutput { thread: Thread error: MutationError } input UpdateTenantTierInput { tierIdentifier: TierIdentifierInput tenantIdentifier: TenantIdentifierInput! } type UpdateTenantTierOutput { tenantTierMembership: TenantTierMembership error: MutationError } input AddMembersToTierInput { tierIdentifier: TierIdentifierInput! memberIdentifiers: [TierMemberIdentifierInput!]! } type AddMembersToTierOutput { memberships: [TierMembership!]! error: MutationError } input RemoveMembersFromTierInput { memberIdentifiers: [TierMemberIdentifierInput!]! } type RemoveMembersFromTierOutput { memberships: [TierMembership!]! error: MutationError } input CreateTierInput { """The name of this tier.""" name: String! """ The external ID of this tier. You can use this field to store your own unique identifier for this tier. This must be unique in your workspace. """ externalId: String! """ The color to assign to this tier, given by its hex code (e.g. #FABADA). This color is used in Plain's UI to represent this tier. """ color: String! """ Any thread created in this tier will have this priority by default, unless a different priority is specified while creating it. If not provided, it defaults to 2 (normal priority). """ defaultThreadPriority: Int memberIdentifiers: [TierMemberIdentifierInput!]! """ If set to true, this tier will be applied to all threads that do not match any other tier. Only one tier can be the default tier. Default: false """ isDefault: Boolean } input ServiceLevelAgreementThreadLabelTypeIdFilterInput { """ The label type IDs that the thread needs to have in order for the SLA to be applied. Based on the 'requireAll' field. """ labelTypeIds: [ID!]! """ If true, the SLA will only be applied to threads that have all of the provided label types. If false, the SLA will be applied to threads that have any of the provided label types. """ requireAll: Boolean! } input ServiceLevelAgreementInput { """Set this to configure the firt response time SLA.""" firstResponseTimeMinutes: Int """Set this to configure an SLA for next responses.""" nextResponseTimeMinutes: Int """ This SLA can only be applied to a thread if it has one of these priority values. If not provided, it defaults to all priorities (0, 1, 2 and 3). """ threadPriorityFilter: [Int!] """ This SLA can only be applied to a thread if it has one or all of these label types. If not provided, the filter is not applied. """ threadLabelTypeIdFilter: ServiceLevelAgreementThreadLabelTypeIdFilterInput """ If true, the SLA will only be tracked during your workspace's business hours. If false, the SLA will tracked 24/7. """ useBusinessHoursOnly: Boolean! """ The actions to take when the SLA is about to breach and when it breaches. """ breachActions: [BreachActionInput!]! } input BreachActionInput { beforeBreachAction: BeforeBreachActionInput } input BeforeBreachActionInput { beforeBreachMinutes: Int! } input TierMemberIdentifierInput { companyId: ID tenantId: ID } type CreateTierOutput { tier: Tier error: MutationError } input UpdateTierInput { tierId: ID! name: StringInput externalId: OptionalStringInput color: StringInput defaultThreadPriority: IntInput isDefault: BooleanInput } type UpdateTierOutput { tier: Tier error: MutationError } input DeleteTierInput { tierId: ID! } type DeleteTierOutput { tier: Tier error: MutationError } input CreateServiceLevelAgreementInput { tierId: ID! serviceLevelAgreement: ServiceLevelAgreementInput! } type CreateServiceLevelAgreementOutput { serviceLevelAgreement: ServiceLevelAgreement error: MutationError } input IntArrayInput { value: [Int!]! } input UpdateServiceLevelAgreementInput { """The ID of the SLA to update.""" serviceLevelAgreementId: ID! """ This SLA will breach if it does not receive a first response within this many minutes. May only be provided if the service level agreement is a first response time SLA. """ firstResponseTimeMinutes: IntInput """ This SLA will breach if it does not receive a next response within this many minutes. May only be provided if the service level agreement is a next response time SLA. """ nextResponseTimeMinutes: IntInput """ This SLA can only be applied to a thread if it has one of these priority values. If not provided, it defaults to all priorities (0, 1, 2 and 3). """ threadPriorityFilter: IntArrayInput """ This SLA can only be applied to a thread if it has one or all of these label types. If not provided, the filter is not applied. """ threadLabelTypeIdFilter: ServiceLevelAgreementThreadLabelTypeIdFilterInput """ If true, the SLA will only be tracked during your workspace's business hours. If false, the SLA will tracked 24/7. """ useBusinessHoursOnly: BooleanInput """ The actions to take when the SLA is about to breach and when it breaches. """ breachActions: [BreachActionInput!] } type UpdateServiceLevelAgreementOutput { serviceLevelAgreement: ServiceLevelAgreement error: MutationError } input DeleteServiceLevelAgreementInput { serviceLevelAgreementId: ID! } type DeleteServiceLevelAgreementOutput { serviceLevelAgreement: ServiceLevelAgreement error: MutationError } input UpsertCompanyInput { identifier: CompanyIdentifierInput! name: String! domainName: String! contractValue: Int accountOwnerUserId: ID } type UpsertCompanyOutput { company: Company result: UpsertResult error: MutationError } enum TenantSource { """Tenant was created directly via the Plain API.""" API """Tenant was synced from a Salesforce integration.""" SALESFORCE """Tenant was synced from a HubSpot integration.""" HUBSPOT } type Tenant { id: ID! name: String! """ Your system's identifier for this tenant, set when the tenant was created via the API or an integration. Use this to map back to the corresponding record in your own database. """ externalId: String! """ An optional URL linking to the tenant in your own product or admin panel. """ url: String """ Indicates how this tenant was created: directly via the API, synced from Salesforce, or synced from HubSpot. """ source: TenantSource! """ The support tier assigned to this tenant, used to apply SLAs and prioritisation rules to threads belonging to this tenant. """ tier: Tier """ Slack (or other channel) associations configured for this tenant, used to route new threads to the appropriate channel automatically. """ threadChannelAssociations: [ThreadChannelAssociation!]! """ Custom field values stored on this tenant, corresponding to the workspace's tenant field schemas. """ tenantFields: [TenantField!]! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } """ The data type of a tenant field schema, determining which value field to use when upserting a tenant field. """ enum TenantFieldType { """A plain text string value.""" STRING_TYPE """A floating-point numeric value.""" NUMBER_TYPE """A true/false boolean value.""" BOOLEAN_TYPE """An array of string values.""" STRING_ARRAY """An ISO 8601 date-time value.""" DATETIME_TYPE """One or more references to Plain users by ID.""" USER_REFERENCE_TYPE } enum TenantFieldMappingConcept { TIER } type TenantFieldSchema { id: ID! """ The origin of this schema, typically the service integration key or 'api' for manually created schemas. """ source: String! """ Your identifier for this field. Together with `source` this uniquely identifies the schema and is used as the upsert key. """ externalFieldId: ID! label: String! type: TenantFieldType! """ For enum-style fields, the list of allowed string values. Null for all other field types. """ options: [String!] """Whether this field is displayed in the Plain UI.""" isVisible: Boolean! """ Display order of this field relative to other tenant field schemas (lower numbers appear first). """ order: Int! """ The concept this field maps to, if any. Used for automatic tier assignment. """ mapsTo: TenantFieldMappingConcept createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } type TenantFieldSchemaEdge { cursor: String! node: TenantFieldSchema! } type TenantFieldSchemaConnection { edges: [TenantFieldSchemaEdge!]! pageInfo: PageInfo! } input TenantFieldSchemasFilter { source: String isVisible: Boolean } input TenantFieldSchemaInput { source: String! externalFieldId: ID! label: String! type: TenantFieldType! options: [String!] isVisible: Boolean! order: Int! } input UpsertTenantFieldSchemaInput { tenantFieldSchemas: [TenantFieldSchemaInput!]! } type UpsertTenantFieldSchemaOutput { tenantFieldSchemas: [TenantFieldSchema!]! result: UpsertResult error: MutationError } input TenantFieldIdentifier { tenantId: ID! externalFieldId: ID! } input UpsertTenantFieldInput { tenantFieldIdentifier: TenantFieldIdentifier! type: TenantFieldType! stringValue: String numberValue: Float booleanValue: Boolean arrayValue: [String!] dateValue: String userReferenceValues: [ID!] } type UpsertTenantFieldOutput { tenantField: TenantField result: UpsertResult error: MutationError } input DeleteTenantFieldInput { tenantFieldId: ID! } type DeleteTenantFieldOutput { tenantField: TenantField error: MutationError } input DeleteTenantFieldSchemaInput { tenantFieldSchemaId: ID! } type DeleteTenantFieldSchemaOutput { tenantFieldSchema: TenantFieldSchema error: MutationError } input SetupTenantFieldSchemaMappingInput { tenantFieldSchemaId: ID! mapsTo: TenantFieldMappingConcept! } type SetupTenantFieldSchemaMappingOutput { tenantFieldSchema: TenantFieldSchema error: MutationError } input RemoveTenantFieldSchemaMappingInput { tenantFieldSchemaId: ID! } type RemoveTenantFieldSchemaMappingOutput { tenantFieldSchema: TenantFieldSchema error: MutationError } union TenantFieldValue = TenantFieldStringValue | TenantFieldNumberValue | TenantFieldBooleanValue | TenantFieldStringArrayValue | TenantFieldDateTimeValue | TenantFieldUserReferenceValue type TenantFieldStringValue { stringValue: String! } type TenantFieldNumberValue { numberValue: Float! } type TenantFieldBooleanValue { booleanValue: Boolean! } type TenantFieldStringArrayValue { arrayValue: [String!]! } type TenantFieldDateTimeValue { dateValue: DateTime! } type TenantFieldUserReferenceValue { userReferenceValues: [ID!]! users: [User!]! } type TenantField { id: ID! """ References the `externalFieldId` of the corresponding `TenantFieldSchema`. """ externalFieldId: ID! """ The typed value stored for this field. The concrete union member matches the schema's `type`. """ value: TenantFieldValue! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } input TenantIdentifierInput { tenantId: ID externalId: String } input UpsertTenantInput { identifier: TenantIdentifierInput! name: String! externalId: String! url: OptionalStringInput } type UpsertTenantOutput { tenant: Tenant result: UpsertResult error: MutationError } input DeleteTenantInput { tenantIdentifier: TenantIdentifierInput! } type DeleteTenantOutput { tenant: Tenant error: MutationError } input CompanyIdentifierInput { """Plain's internal identifier for the company.""" companyId: ID """ The domain name of the company (e.g. plain.com). Alternatively, you can provide a full URL (e.g. https://www.plain.com) and we will do our best to extract the domain name. """ companyDomainName: String } input UpdateCustomerCompanyInput { """The identifier of the customer we want to update the company for.""" customerId: ID! """ The identifier of the company we want to update the customer with. Pass null if you want to remove the company from the customer. """ companyIdentifier: CompanyIdentifierInput } type UpdateCustomerCompanyOutput { customer: Customer error: MutationError } input MentionInput { userId: ID! displayName: String! } input SendMSTeamsMessageInput { threadId: ID! markdownContent: String attachmentIds: [ID!] mentions: [MentionInput!] } type SendMSTeamsMessageOutput { msTeamsMessage: MSTeamsMessage error: MutationError } input SendSlackMessageInput { threadId: ID! markdownContent: String! attachmentIds: [ID!] unfurlLinks: Boolean replyBroadcast: Boolean } type SendSlackMessageOutput { error: MutationError } input ShareThreadToUserInSlackInput { threadId: ID! userId: ID! } type ShareThreadToUserInSlackOutput { error: MutationError } input SendDiscordMessageInput { threadId: ID! markdownContent: String attachmentIds: [ID!] } type SendDiscordMessageOutput { discordMessage: DiscordMessage error: MutationError } input ToggleSlackMessageReactionInput { threadId: ID! timelineEntryId: ID! reactionName: String! } type ToggleSlackMessageReactionOutput { error: MutationError } input ForkThreadInput { threadId: ID! timelineEntryId: ID! } type ForkThreadOutput { thread: Thread error: MutationError } input CustomerImpersonationInput { customerIdentifier: CustomerIdentifierInput! } input ImpersonationInput { asCustomer: CustomerImpersonationInput! } input ReplyToThreadEmailChannelSpecificOptionsInput { additionalRecipients: [EmailParticipantInput!] hiddenRecipients: [EmailParticipantInput!] } input ReplyToThreadChannelSpecificOptionsInput { email: ReplyToThreadEmailChannelSpecificOptionsInput! } input ReplyToThreadInput { threadId: ID! textContent: String! markdownContent: String impersonation: ImpersonationInput attachmentIds: [ID!] channelSpecificOptions: ReplyToThreadChannelSpecificOptionsInput } type ReplyToThreadOutput { error: MutationError } """ Query to search for threads. The search term provided is used to match against different parts of the thread: - its title - its messages - the customer's name - the customer's email """ input ThreadsSearchQuery { """ The term to search for. It must be at least 2 characters long. The search is case-insensitive. """ term: String! } type ThreadSearchResult { thread: Thread! } type ThreadSearchResultEdge { cursor: String! node: ThreadSearchResult! } type ThreadSearchResultConnection { edges: [ThreadSearchResultEdge!]! pageInfo: PageInfo! } input UpsertMyEmailSignatureInput { text: String! markdown: String } type UpsertMyEmailSignatureOutput { emailSignature: EmailSignature result: UpsertResult error: MutationError } enum DoneStatusDetail { IGNORED DONE_MANUALLY_SET DONE_AUTOMATICALLY_SET TIMER_EXPIRED @deprecated(reason: "Use DONE_AUTOMATICALLY_SET instead.") } input MarkThreadAsDoneInput { threadId: ID! statusDetail: DoneStatusDetail } type MarkThreadAsDoneOutput { thread: Thread error: MutationError } enum StatusDetailType { CREATED IN_PROGRESS NEW_REPLY THREAD_LINK_UPDATED THREAD_DISCUSSION_RESOLVED WAITING_FOR_CUSTOMER WAITING_FOR_DURATION WAITING_INDEFINITELY IGNORED DONE_MANUALLY_SET DONE_AUTOMATICALLY_SET TIMER_EXPIRED @deprecated(reason: "Use DONE_AUTOMATICALLY_SET instead.") } enum TodoStatusDetail { CREATED IN_PROGRESS NEW_REPLY THREAD_LINK_UPDATED THREAD_DISCUSSION_RESOLVED } input MarkThreadAsTodoInput { threadId: ID! statusDetail: TodoStatusDetail } type MarkThreadAsTodoOutput { thread: Thread error: MutationError } input ChangeThreadPriorityInput { threadId: ID! priority: Int! } type ChangeThreadPriorityOutput { thread: Thread error: MutationError } input UpdateThreadTitleInput { threadId: ID! title: String! } type UpdateThreadTitleOutput { thread: Thread error: MutationError } input UpdateThreadExternalIdInput { threadId: ID! """The external ID to set on the thread, or `null` to clear it.""" externalId: ID } type UpdateThreadExternalIdOutput { thread: Thread error: MutationError } input LockThreadInput { threadId: ID! } type LockThreadOutput { thread: Thread error: MutationError } enum SnoozeStatusDetail { WAITING_FOR_CUSTOMER WAITING_FOR_DURATION WAITING_INDEFINITELY } input SnoozeThreadInput { threadId: ID! durationSeconds: Int statusDetail: SnoozeStatusDetail } type SnoozeThreadOutput { thread: Thread error: MutationError } input AssignThreadInput { threadId: ID! userId: ID machineUserId: ID } type AssignThreadOutput { thread: Thread error: MutationError } input UnassignThreadInput { threadId: ID! } type UnassignThreadOutput { thread: Thread error: MutationError } input AddAdditionalAssigneesInput { threadId: ID! userIds: [ID!] machineUserIds: [ID!] } type AddAdditionalAssigneesOutput { thread: Thread error: MutationError } input RemoveAdditionalAssigneesInput { threadId: ID! userIds: [ID!] machineUserIds: [ID!] } type RemoveAdditionalAssigneesOutput { thread: Thread error: MutationError } """ A short-lived signed JWT that an embed iframe can pass to a customer's backend for verification against the Plain-hosted JWKS at `jwksUrl`. """ type EmbedToken { """ The signed RS256 JWT. Pass this value to your backend and verify it against the JWKS at `jwksUrl`. """ token: String! """ The time at which the token expires. Tokens have a 60-second TTL; mint a fresh token for each request. """ expiresAt: DateTime! """ URL of the Plain-hosted JWKS endpoint. Fetch the public keys from this URL to verify the token signature. """ jwksUrl: String! } input MintEmbedTokenInput { """ Stable identifier for the embed. Passed through as the `plain_embed_id` claim for audit logging. """ embedId: String! """ Thread the command is being invoked from. Access is checked server-side. """ threadId: ID! } type MintEmbedTokenOutput { token: EmbedToken error: MutationError } """The lifecycle status of a thread.""" enum ThreadStatus { """The thread requires attention from the support team.""" TODO """ The thread is temporarily paused until a timer expires or the customer replies. """ SNOOZED """ The support team has finished with the thread for now. It reverts to TODO automatically when new activity arrives. """ DONE } type ThreadStatusDetailCreated { statusChangedAt: DateTime! createdAt: DateTime! } type ThreadStatusDetailNewReply { statusChangedAt: DateTime! newReplyAt: DateTime @deprecated(reason: "newReplyAt is no longer supported, query Thread.lastInboundMessageInfo.timestamp instead.") } type ThreadStatusDetailReplied { repliedAt: DateTime! @deprecated(reason: "ThreadStatusDetailReplied is no longer supported.") statusChangedAt: DateTime! @deprecated(reason: "ThreadStatusDetailReplied is no longer supported.") } type ThreadStatusDetailThreadLinkUpdated { statusChangedAt: DateTime! updatedAt: DateTime! @deprecated(reason: "Use statusChangedAt instead") linearIssueId: ID } type ThreadStatusDetailLinearUpdated { statusChangedAt: DateTime! @deprecated(reason: "ThreadStatusDetailLinearUpdated is no longer supported, query ThreadStatusDetailThreadLinkUpdated instead.") updatedAt: DateTime! @deprecated(reason: "ThreadStatusDetailLinearUpdated is no longer supported, query ThreadStatusDetailThreadLinkUpdated instead.") linearIssueId: ID! @deprecated(reason: "ThreadStatusDetailLinearUpdated is no longer supported, query ThreadStatusDetailThreadLinkUpdated instead.") } type ThreadStatusDetailInProgress { statusChangedAt: DateTime! } type ThreadStatusDetailThreadDiscussionResolved { statusChangedAt: DateTime! threadDiscussionId: ID } type ThreadStatusDetailUnsnoozed { snoozedAt: DateTime! @deprecated(reason: "ThreadStatusDetailUnsnoozed is no longer supported.") statusChangedAt: DateTime! @deprecated(reason: "ThreadStatusDetailUnsnoozed is no longer supported.") } type ThreadStatusDetailSnoozed { snoozedAt: DateTime! @deprecated(reason: "ThreadStatusDetailSnoozed is no longer supported.") snoozedUntil: DateTime! @deprecated(reason: "ThreadStatusDetailSnoozed is no longer supported.") statusChangedAt: DateTime! @deprecated(reason: "ThreadStatusDetailSnoozed is no longer supported.") } type ThreadStatusDetailWaitingForDuration { statusChangedAt: DateTime! waitingUntil: DateTime! } type ThreadStatusDetailWaitingForCustomer { statusChangedAt: DateTime! } type ThreadStatusDetailWaitingIndefinitely { statusChangedAt: DateTime! } type ThreadStatusDetailIgnored { statusChangedAt: DateTime! } type ThreadStatusDetailDoneManuallySet { statusChangedAt: DateTime! } type ThreadStatusDetailDoneAutomaticallySet { statusChangedAt: DateTime! afterSeconds: Int } union ThreadStatusDetail = ThreadStatusDetailCreated | ThreadStatusDetailSnoozed | ThreadStatusDetailUnsnoozed | ThreadStatusDetailNewReply | ThreadStatusDetailReplied | ThreadStatusDetailLinearUpdated | ThreadStatusDetailInProgress | ThreadStatusDetailWaitingForCustomer | ThreadStatusDetailWaitingForDuration | ThreadStatusDetailWaitingIndefinitely | ThreadStatusDetailThreadLinkUpdated | ThreadStatusDetailIgnored | ThreadStatusDetailDoneManuallySet | ThreadStatusDetailDoneAutomaticallySet | ThreadStatusDetailThreadDiscussionResolved enum AgentStatus { IN_PROGRESS HANDED_OFF HANDLED } type AgentStatusDetailInProgress { type: String! } type AgentStatusDetailHandedOff { type: String! reason: String } type AgentStatusDetailHandled { type: String! } union AgentStatusDetail = AgentStatusDetailInProgress | AgentStatusDetailHandedOff | AgentStatusDetailHandled enum MessageSource { CHAT EMAIL API SLACK MS_TEAMS DISCORD INTERNAL } type ThreadMessageInfo { """The datetime when the last message was received.""" timestamp: DateTime! """The source through which the message came through.""" messageSource: MessageSource! } """ The entity a thread is assigned to: a human user, a machine user (bot), or the Plain system itself. """ union ThreadAssignee = User | MachineUser | System """ A link between a connected messaging channel and a company or tenant. When a message arrives in the associated channel, Plain uses this association to route threads to the correct customer context. Implemented by SlackThreadChannelAssociation. """ interface ThreadChannelAssociation { id: ID! """ The ID of the company this channel is associated with. Null if the association targets a tenant instead. """ companyId: ID """ The ID of the tenant this channel is associated with. Null if the association targets a company instead. """ tenantId: ID createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } """A thread channel association backed by a connected Slack channel.""" type SlackThreadChannelAssociation implements ThreadChannelAssociation { id: ID! companyId: ID tenantId: ID createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! """ The ID of the connected Slack channel that is linked to the company or tenant. """ connectedSlackChannelId: ID! } input CreateThreadChannelAssociationInput { companyIdentifier: CompanyIdentifierInput tenantIdentifier: TenantIdentifierInput connectedSlackChannelId: ID } type CreateThreadChannelAssociationOutput { threadChannelAssociation: ThreadChannelAssociation error: MutationError } input DeleteThreadChannelAssociationInput { threadChannelAssociationId: ID! } type DeleteThreadChannelAssociationOutput { error: MutationError } type SlackThreadChannelDetails { slackChannelId: String! slackChannelName: String! slackTeamId: String! slackTeamName: String! } type ChatThreadChannelDetails { customerReadAt: DateTime! } enum MSTeamsMessageType { CHATS CHANNELS } type MSTeamsThreadChannelDetails { msTeamsTeamId: ID! msTeamsTeamName: String! msTeamsChannelId: ID! msTeamsChannelName: String! msTeamsMessageType: MSTeamsMessageType! } type DiscordThreadChannelDetails { discordGuildId: String! discordChannelId: String! discordChannelName: String } type ImportThreadChannelDetails { importSourceUrl: String importIntegrationKey: String! } union ThreadChannelDetails = SlackThreadChannelDetails | MSTeamsThreadChannelDetails | ChatThreadChannelDetails | DiscordThreadChannelDetails | ImportThreadChannelDetails input SlackThreadChannelDetailsInput { slackChannelId: ID! slackTeamId: ID! } input MSTeamsThreadChannelDetailsInput { msTeamsTeamId: ID! msTeamsChannelId: ID! } input ThreadChannelDetailsInput { slack: SlackThreadChannelDetailsInput msTeams: MSTeamsThreadChannelDetailsInput } enum ThreadChannel { EMAIL SLACK CHAT API MS_TEAMS DISCORD IMPORT INTERNAL } """ Discriminates the type of a timeline entry, used when filtering the thread timeline. """ enum TimelineEntryType { HELP_CENTER_AI_CONVERSATION_MESSAGE CHAT CUSTOM EMAIL LINEAR_ISSUE_THREAD_LINK_STATE_TRANSITIONED NOTE THREAD_ASSIGNMENT_TRANSITIONED THREAD_ADDITIONAL_ASSIGNEES_TRANSITIONED THREAD_STATUS_TRANSITIONED THREAD_PRIORITY_CHANGED """ A custom event scoped to a single thread, created via `createThreadEvent`. """ THREAD_EVENT """ A custom event that appears across all threads for a customer, created via `createCustomerEvent`. """ CUSTOMER_EVENT SLACK_MESSAGE SLACK_REPLY MS_TEAMS_MESSAGE """A message from a thread that was merged into this thread.""" MERGED_THREAD_MESSAGE DISCORD_MESSAGE THREAD_LABELS_CHANGED THREAD_DISCUSSION THREAD_DISCUSSION_MESSAGE THREAD_DISCUSSION_RESOLVED SERVICE_LEVEL_AGREEMENT_STATUS_TRANSITIONED THREAD_LINK_CREATED THREAD_LINK_UPDATED THREAD_LINK_DELETED """ A new issue was created in an external tracker (e.g. Linear) and linked to this thread. """ THREAD_LINK_TARGET_CREATED """A linked issue in an external tracker was deleted.""" THREAD_LINK_TARGET_DELETED CUSTOMER_SURVEY_REQUESTED } input ThreadTimelineEntriesFilter { """Only return message timeline entries.""" isMessage: Boolean """ Only return timeline entries of the specified types. If provided, this takes precedence over isMessage. """ entryTypes: [TimelineEntryType!] } """ A thread represents a conversation with a customer, around a specific topic. """ type Thread { """The unique identifier of the thread.""" id: ID! """The human-readable identifier of the thread, ie T-1234.""" ref: String! """The customer involved in this thread.""" customer: Customer! """ The title of this thread, which allows to quickly identify what it is about. """ title: String! """The description of this thread.""" description: String """ A short preview of the most recent activity in the thread, updated automatically as new messages arrive. Suitable for displaying in list views. """ previewText: String """The priority of the thread: 0 = urgent, 1 = high, 2 = normal, 3 = low.""" priority: Int! """ The external ID of this thread. You can use this field to store your own unique identifier for this thread. """ externalId: ID """The status of this thread.""" status: ThreadStatus! """The datetime when the status of this thread was last changed.""" statusChangedAt: DateTime! """The actor who last changed the status of this thread.""" statusChangedBy: Actor! """ Structured details about the current status — for example, when a snoozed thread will wake up, or which linked issue triggered a status change. The concrete type varies with the current status. """ statusDetail: ThreadStatusDetail """ The AI agent handling status of this thread: `IN_PROGRESS` (agent is actively working), `HANDED_OFF` (agent transferred to a human), or `HANDLED` (agent resolved it). Null if no agent is involved. """ agentStatus: AgentStatus """ Structured details about the current agent status, such as the hand-off reason. The concrete type varies with `agentStatus`. """ agentStatusDetail: AgentStatusDetail """The datetime when the agent status of this thread was last changed.""" agentStatusUpdatedAt: DateTime """The actor who last changed the agent status of this thread.""" agentStatusUpdatedBy: Actor """Who or what this thread is assigned to.""" assignedTo: ThreadAssignee """ The datetime when this thread was last assigned to someone or something. """ assignedAt: DateTime """ Secondary assignees who are looped in on the thread but are not the primary person responsible. May be users or machine users. """ additionalAssignees: [ThreadAssignee!]! """The labels attached to this thread.""" labels: [Label!]! """The links attached to this thread.""" links(first: Int, after: String, last: Int, before: String): ThreadLinkConnection! """The thread fields attached to this thread.""" threadFields: [ThreadField!]! """The thread discussions attached to this thread.""" threadDiscussions: [ThreadDiscussion!]! """All of the timeline entries in this thread.""" timelineEntries(filters: ThreadTimelineEntriesFilter, first: Int, after: String, last: Int, before: String): TimelineEntryConnection! """ Metadata about the first message received from the customer on this thread. Null if no inbound message exists yet. """ firstInboundMessageInfo: ThreadMessageInfo """ Metadata about the first message sent to the customer on this thread. Null if no outbound message has been sent yet. """ firstOutboundMessageInfo: ThreadMessageInfo """ Metadata about the most recent message received from the customer. Null if no inbound message exists yet. """ lastInboundMessageInfo: ThreadMessageInfo """ Metadata about the most recent message sent to the customer. Null if no outbound message has been sent yet. """ lastOutboundMessageInfo: ThreadMessageInfo """The datetime when this thread was created.""" createdAt: DateTime! """The actor who created this thread.""" createdBy: Actor! """The datetime when this thread was last updated.""" updatedAt: DateTime! """The actor who last updated this thread.""" updatedBy: Actor! """ The support email addresses involved in this thread. A support email address is either the default support email address or an alternate support email address. A support email address is considered to be involved in a thread when any participant in the thread uses it as their email recipient. """ supportEmailAddresses: [String!]! """The tenant this thread is associated with.""" tenant: Tenant """ The tier this thread is associated with. Tiers mandate the SLAs for this thread. """ tier: Tier """ Summary of SLA objective statuses for this thread (e.g. whether first-response or next-response time targets are met, breached, or approaching breach). Always present but may show no active trackers if no SLA is linked. """ serviceLevelAgreementStatusSummary: ServiceLevelAgreementStatusSummary! """The channel this thread belongs to.""" channel: ThreadChannel! """Details about the channel this thread is on.""" channelDetails: ThreadChannelDetails """The participants in this thread.""" participants(first: Int, after: String, last: Int, before: String): ActorConnection! """The survey responses for this thread.""" surveyResponse: SurveyResponse """The escalation details for this thread.""" escalationDetails: ThreadEscalationDetails """ AI-generated summary and suggested actions for this thread, helping a support agent quickly get up to speed. Null if no catchup has been generated yet. """ catchupDetail: ThreadCatchupDetail """ The datetime when the thread was locked. Null if the thread is currently unlocked. """ lockedAt: DateTime """ The user or machine user who locked the thread. Null if the thread is currently unlocked. """ lockedBy: InternalActor } type ThreadEscalationDetails { """The escalation path this thread is associated with.""" escalationPath: EscalationPath! """ The step this thread will be escalated to. If it is null, the thread is at the end of the escalation path. """ nextEscalationPathStep: EscalationPathStep } type ThreadCatchupDetail { summary: String suggestedActions: [ThreadCatchupSuggestedAction!]! completedActions: [ThreadCatchupUserEvent]! customerEvents: [ThreadCatchupCustomerEvent]! generatedAt: DateTime } union ThreadCatchupSuggestedAction = ThreadCatchupSuggestedInternalAction | ThreadCatchupSuggestedCustomerAction enum ThreadCatchupSuggestedActionStatus { SUGGESTED ACCEPTED REJECTED } type ThreadCatchupSuggestedInternalAction { id: ID! threadLinkType: String! userId: ID content: String! status: ThreadCatchupSuggestedActionStatus! } type ThreadCatchupSuggestedCustomerAction { id: ID! customerId: ID! content: String! status: ThreadCatchupSuggestedActionStatus! } type ThreadCatchupUserEvent { userId: ID! content: String! } type ThreadCatchupCustomerEvent { customerId: ID! content: String! } type SurveyResponse { id: ID! sentiment: SentimentType rating: Int surveyId: ID comment: String respondedAt: DateTime createdAt: DateTime! createdBy: Actor! updatedAt: DateTime! updatedBy: Actor! } type ActorEdge { cursor: String! node: Actor! } type ActorConnection { edges: [ActorEdge!]! pageInfo: PageInfo! } """The lifecycle status of an SLA tracker for a given thread.""" enum ServiceLevelAgreementStatus { """The SLA is active and the deadline has not yet been reached.""" PENDING """ The SLA deadline is approaching; a breach is imminent based on the configured `beforeBreachMinutes` action. """ IMMINENT_BREACH """ The SLA deadline has passed and no compliant response has been sent yet. """ BREACHING """ The SLA deadline passed without a compliant response and the thread was subsequently resolved. """ BREACHED """A compliant response was sent within the SLA deadline.""" ACHIEVED """ The SLA was cancelled for this thread, e.g. because the thread's tier or priority changed after creation. """ CANCELLED } type ServiceLevelAgreementStatusDetailPending { """The time when this SLA will breach.""" breachingAt: DateTime! } type ServiceLevelAgreementStatusDetailImminentBreach { """The time when this SLA will breach.""" breachingAt: DateTime! } type ServiceLevelAgreementStatusDetailBreaching { """The time when this SLA breached.""" breachedAt: DateTime! } type ServiceLevelAgreementStatusDetailAchieved { """The time when this SLA was achieved.""" achievedAt: DateTime! } type ServiceLevelAgreementStatusDetailBreached { """The time when this SLA breached.""" breachedAt: DateTime! """The time when we completed this breached SLA.""" completedAt: DateTime! } type ServiceLevelAgreementStatusDetailCancelled { """The time when this SLA was cancelled.""" cancelledAt: DateTime! } union ServiceLevelAgreementStatusDetail = ServiceLevelAgreementStatusDetailPending | ServiceLevelAgreementStatusDetailImminentBreach | ServiceLevelAgreementStatusDetailBreaching | ServiceLevelAgreementStatusDetailAchieved | ServiceLevelAgreementStatusDetailBreached | ServiceLevelAgreementStatusDetailCancelled """ A snapshot of the current SLA tracking status for a thread, broken down by SLA type. """ type ServiceLevelAgreementStatusSummary { """ Current tracking status for the first-response-time SLA, if one applies to this thread's tier. """ firstResponseTime: ServiceLevelAgreementStatusDetail """ Current tracking status for the next-response-time SLA, if one applies to this thread's tier. """ nextResponseTime: ServiceLevelAgreementStatusDetail } """Only one of the fields can be set.""" input CustomerIdentifierInput { externalId: ID emailAddress: String customerId: ID } """Only one of the fields can be set.""" input CreateThreadAssignedToInput { userId: ID machineUserId: ID } input CreateThreadInput { """ The identifier of the customer being either the existing customer ID, the customer's email address or an external ID. """ customerIdentifier: CustomerIdentifierInput! """The title of the thread.""" title: String """The components used to create the first timeline entry in the thread.""" components: [ComponentInput!] @deprecated(reason: "Use sendChat and sendCustomerChat mutations instead. Both allow you to backdate messages.") """An array of attachments for the first timeline entry in the thread.""" attachmentIds: [ID!] @deprecated(reason: "Use sendChat and sendCustomerChat mutations instead. Both allow you to backdate messages.") """An array of label types to attach to the thread upon creation.""" labelTypeIds: [ID!] """An array of thread fields to attach to the thread upon creation.""" threadFields: [CreateThreadFieldOnThreadInput!] """User or machine user this thread should be assigned to.""" assignedTo: CreateThreadAssignedToInput """ The external ID of this thread. You can use this field to store your own unique identifier for this thread. """ externalId: ID """ The optional description for this thread. This is used to display a preview of the thread in the UI. If not provided, we will automatically infer it from the components you provided. """ description: String """ The priority of the thread. Valid values are 0, 1, 2, 3, from most to least urgent, defaults to 2 (normal). """ priority: Int """A thread may be assigned to a specific tenant.""" tenantIdentifier: TenantIdentifierInput """ The channel to create the thread for. Currently supported: API, EMAIL, SLACK, MS_TEAMS, CHAT, or INTERNAL. Defaults to API. """ channel: ThreadChannel """ Channel details for the thread, required if channel is SLACK or MS_TEAMS """ channelDetails: ThreadChannelDetailsInput } type CreateThreadOutput { thread: Thread error: MutationError } enum ImportThreadStatusDetail { NEW_REPLY IN_PROGRESS DONE_MANUALLY_SET IGNORED WAITING_FOR_CUSTOMER } input ImportThreadStatusDetailInput { """The status detail type.""" type: ImportThreadStatusDetail! } """Only one of the fields can be set.""" input ImportThreadAssignedToInput { userId: ID machineUserId: ID } input ImportThreadInput { """ The identifier of the customer being either the existing customer ID, the customer's email address or an external ID. """ customerIdentifier: CustomerIdentifierInput! """The title of the thread.""" title: String! """ An external ID for this thread. Duplicate imports with the same externalId are skipped. """ externalId: ID! """An optional external URL for this thread.""" externalUrl: String """An optional description for this thread.""" description: String """ The priority of the thread. Valid values are 0, 1, 2, 3, from most to least urgent. """ priority: Int! """The status of the thread.""" status: ThreadStatus! """ The status detail for the thread. Must correspond to the provided status. """ statusDetail: ImportThreadStatusDetailInput! """An array of label type IDs to attach to the thread.""" labelTypeIds: [ID!] """User this thread should be assigned to.""" assignedTo: ImportThreadAssignedToInput """The ID of the tenant for this thread.""" tenantId: ID """The original creation timestamp of this thread in the source system.""" createdAt: String! } type ImportThreadOutput { thread: Thread result: UpsertResult error: MutationError } enum ImportThreadMessageType { INBOUND OUTBOUND NOTE } """Only one of customerId or userId must be provided.""" input ImportThreadMessageAuthorInput { """The Plain customer who authored this message.""" customerId: ID """The Plain user who authored this message.""" userId: ID } input ImportThreadMessageInput { """The author of the message.""" author: ImportThreadMessageAuthorInput! """The text content of the message.""" text: String! """The original creation timestamp of this message in the source system.""" createdAt: String! type: ImportThreadMessageType! """ An external ID for this message. Duplicate imports with the same externalId are skipped. """ externalId: ID! """An array of attachments for the message.""" attachmentIds: [ID!] } input ImportThreadMessagesInput { """The ID of the thread to import messages into.""" threadId: ID! """The messages to import.""" threadMessages: [ImportThreadMessageInput!]! } union ImportedThreadMessage = TimelineEntry | Note type ImportThreadMessageResult { """ The imported thread message. Null if the import of this message failed. """ threadMessage: ImportedThreadMessage result: UpsertResult """The error if the import of this message failed. Null on success.""" error: MutationError } type ImportThreadMessagesOutput { """ Per-message results in the same order as the input. Each result contains either the imported message or an error. """ results: [ImportThreadMessageResult!]! error: MutationError } input MarkCustomerAsSpamInput { customerId: ID! } input UnmarkCustomerAsSpamInput { customerId: ID! } type MarkCustomerAsSpamOutput { customer: Customer error: MutationError } type UnmarkCustomerAsSpamOutput { customer: Customer error: MutationError } type SubscriptionEventType { """ The event type identifier used in webhook target subscriptions (e.g. 'thread.thread_created'). """ eventType: String! description: String! } type WebhookVersionEdge { cursor: String! node: WebhookVersion! } type WebhookVersionConnection { edges: [WebhookVersionEdge!]! pageInfo: PageInfo! } type WebhookTargetEdge { cursor: String! node: WebhookTarget! } type WebhookTargetConnection { edges: [WebhookTargetEdge!]! pageInfo: PageInfo! } type WebhookVersion { """ The version identifier string used when creating or updating a webhook target (e.g. '2024-01-01'). """ version: String! """ When true, this version is deprecated and webhook targets pinned to it should be migrated to a newer version. """ isDeprecated: Boolean! """ When true, this is the most recent available version. New webhook targets should be pinned to this version. """ isLatest: Boolean! } type WebhookTarget { id: ID! url: String! description: String! """The list of event types this target is subscribed to.""" eventSubscriptions: [WebhookTargetEventSubscription!]! """ The webhook schema version this target is pinned to (e.g. '2024-01-01'). All events delivered to this target conform to that version's schema. """ version: String! """ When false, Plain will not attempt to deliver any events to this target until it is re-enabled. """ isEnabled: Boolean! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } type WebhookTargetEventSubscription { eventType: String! } input WebhookTargetEventSubscriptionInput { eventType: String! } input CreateWebhookTargetInput { url: String! eventSubscriptions: [WebhookTargetEventSubscriptionInput!]! isEnabled: Boolean! description: String! version: String } input UpdateWebhookTargetInput { webhookTargetId: ID! url: StringInput eventSubscriptions: [WebhookTargetEventSubscriptionInput!] isEnabled: BooleanInput description: StringInput version: StringInput } input DeleteWebhookTargetInput { webhookTargetId: ID! } type CreateWebhookTargetOutput { webhookTarget: WebhookTarget error: MutationError } type UpdateWebhookTargetOutput { webhookTarget: WebhookTarget error: MutationError } type DeleteWebhookTargetOutput { error: MutationError } enum WebhookDeliveryAttemptResultStatus { """Plain received a 2xx HTTP response from the endpoint.""" SUCCESSFUL_RESPONSE """The endpoint returned a non-2xx HTTP status code (4xx or 5xx).""" FAILED_RESPONSE """ A network-level error occurred before a response was received (e.g. connection timeout or DNS failure). """ ERROR """ Plain rejected the delivery before it was attempted, for example because the target was disabled. """ REJECTED """ The event payload failed schema validation against the target's pinned webhook version. """ SCHEMA_VALIDATION_FAILED } type WebhookDeliveryAttemptSuccessfulResult { status: WebhookDeliveryAttemptResultStatus! httpStatusCode: Int! } type WebhookDeliveryAttemptFailedResult { status: WebhookDeliveryAttemptResultStatus! httpStatusCode: Int! } type WebhookDeliveryAttemptErrorResult { status: WebhookDeliveryAttemptResultStatus! errorCode: String! errorMessage: String! } type WebhookDeliveryAttemptRejectedResult { status: WebhookDeliveryAttemptResultStatus! rejectedMessage: String! } type WebhookDeliveryAttemptSchemaValidationFailedResult { status: WebhookDeliveryAttemptResultStatus! errorMessage: String! } union WebhookDeliveryAttemptResult = WebhookDeliveryAttemptSuccessfulResult | WebhookDeliveryAttemptFailedResult | WebhookDeliveryAttemptErrorResult | WebhookDeliveryAttemptRejectedResult | WebhookDeliveryAttemptSchemaValidationFailedResult type WebhookDeliveryAttempt { id: ID! """The ID of the webhook target this delivery was made to.""" webhookTargetId: ID! """ The ID of the public event that was delivered. Pass this to `publicEventRequestBody` to retrieve the exact payload that was sent. """ publicEventId: ID! """ The event type identifier of the delivered event (e.g. 'thread.thread_created'). """ publicEventEventType: String! """When Plain attempted to deliver the event.""" attemptedAt: DateTime! """How long the delivery attempt took, in milliseconds.""" durationInMilliseconds: Int! """ The outcome of the delivery attempt — one of a successful response, failed HTTP response, network error, rejection, or schema validation failure. """ result: WebhookDeliveryAttemptResult! } type WebhookDeliveryAttemptEdge { cursor: String! node: WebhookDeliveryAttempt! } type WebhookDeliveryAttemptConnection { edges: [WebhookDeliveryAttemptEdge!]! pageInfo: PageInfo! } input WebhookDeliveryAttemptFilter { eventTypes: [String!] resultStatus: WebhookDeliveryAttemptResultStatus } input CustomerCardConfigOrderInput { """The ID of the customer card config to be reordered.""" customerCardConfigId: ID! """The order the customer card config should have.""" order: Int! } input ReorderCustomerCardConfigsInput { """An array of ordering updates.""" customerCardConfigOrders: [CustomerCardConfigOrderInput!]! } type ReorderCustomerCardConfigsOutput { """The reordered customer card configs.""" customerCardConfigs: [CustomerCardConfig!] error: MutationError } """An API header that will be sent to the configured API URL.""" input CustomerCardConfigApiHeaderInput { """ The name of the header, trimmed and treated case insensitively for deduplication purposes (min length: 1, max length: 100). Not all header names are allowed. """ name: String! """ The value of the header, treated case sensitively for deduplication purposes (min length: 1, max length: 500). """ value: String! } """ Input type to create a new customer card config. By default new customer cards will have an ordering of 100000 (to place them at the bottom). """ input CreateCustomerCardConfigInput { """The title of the card (max length: 500 characters).""" title: String! """ The key of the card (must be unique in a workspace, max length: 500 characters, must match regex: `[a-zA-Z0-9_-]+`). """ key: String! """ The default time the card should be cached for if no TTL is provided in the card response. (minimum: 15 seconds, maximum: 1 year or 31,536,000 seconds). """ defaultTimeToLiveSeconds: Int! """ The URL from which this card should be loaded (must start with `https://` and be a valid URL, max length: 600 characters). """ apiUrl: String! """An array of headers name-value pairs (maximum length of array: 20).""" apiHeaders: [CustomerCardConfigApiHeaderInput!]! } type CreateCustomerCardConfigOutput { """The created customer card config.""" customerCardConfig: CustomerCardConfig error: MutationError } """ For constraints and details on the fields see the `CustomerCardConfig` type. """ input UpdateCustomerCardConfigInput { """The customer card config to update.""" customerCardConfigId: ID! """If provided, will update the order.""" order: IntInput """If provided, will update the title.""" title: StringInput """If provided, will update the key. Keys must be unique in a workspace.""" key: StringInput """If provided, will update the default time to live seconds.""" defaultTimeToLiveSeconds: IntInput """ If provided, will update the API URL. Requires the `customerCardConfigApiDetails:edit` permission. """ apiUrl: StringInput """ If provided, will replace the existing API headers. Requires the `customerCardConfigApiDetails:edit` permission. """ apiHeaders: [CustomerCardConfigApiHeaderInput!] """If provided, will update the enabled flag.""" isEnabled: BooleanInput } type UpdateCustomerCardConfigOutput { """The updated customer card config.""" customerCardConfig: CustomerCardConfig error: MutationError } input DeleteCustomerCardConfigInput { """The customer card config ID to delete.""" customerCardConfigId: ID! } type DeleteCustomerCardConfigOutput { error: MutationError } input ReloadCustomerCardInstanceInput { customerId: ID! customerCardConfigId: ID! threadId: ID } type ReloadCustomerCardInstanceOutput { """ The reloaded customer card instance. Currently this will always be a `CustomerCardInstanceLoading` type. """ customerCardInstance: CustomerCardInstance error: MutationError } """An input to specify the scope for a setting.""" input SettingScopeInput { """ An optional ID input. Depends on the type of scope if this is required. """ id: ID """Determines the type of the scope.""" scopeType: SettingScopeType! } """ An input "union" where exactly one field may be be provided as an input. Current API only supports booleans but as the API expands more optional fields will be added. """ input SettingValueInput { """If the setting value is a boolean then this field should be set.""" boolean: Boolean """If the setting value is a string then this field should be set.""" string: String """If the setting value is a number then this field should be set""" number: Int """If the setting value is a string array then this field should be set.""" stringArray: [String] } """An input provided to the `updateSetting` mutation.""" input UpdateSettingInput { """A code for the setting.""" code: String! """A valid scope for the setting code.""" scope: SettingScopeInput! """The setting value.""" value: SettingValueInput! } """ An output type provided by the `updateSetting` mutation. Returns the updated setting or an error. """ type UpdateSettingOutput { """The updated setting.""" setting: Setting error: MutationError } """An input provided to the `deleteSetting` mutation.""" input DeleteSettingInput { """A code for the setting.""" code: String! """A valid scope for the setting code.""" scope: SettingScopeInput! } """ An output type provided by the `deleteSetting` mutation. Returns the deleted setting (or null if it did not exist) or an error. """ type DeleteSettingOutput { """ The setting that was removed. Null when no stored value existed at the given scope. """ previousSetting: Setting error: MutationError } """An input provided to the `bulkUpdateSlackChannelSettings` mutation.""" input BulkUpdateSlackChannelSettingsInput { """ List of per-channel setting writes to apply. Writes are applied as a single batched DynamoDB transaction (chunked at 100 items per call) and emit a single batched event publish, so large lists are accepted without per-item fan-out cost. """ updates: [BulkUpdateSlackChannelSettingItem!]! } """ A single update applied by the `bulkUpdateSlackChannelSettings` mutation. """ input BulkUpdateSlackChannelSettingItem { """ The Plain slack channel id that the setting applies to. Used as the scope id of the `WORKSPACE_SLACK_CONNECTED_CHANNEL` scope. """ slackChannelId: ID! """The setting code.""" code: String! """The setting value.""" value: SettingValueInput! } """ An output type provided by the `bulkUpdateSlackChannelSettings` mutation. Returns per-update results so the caller can distinguish successful writes from failed ones. """ type BulkUpdateSlackChannelSettingsOutput { """Per-update results, in the same order as the input list.""" results: [BulkUpdateSlackChannelSettingResult!]! """ Top-level error. Set to a `bulk_partial_failure` error whenever any per-item write failed, so callers that only check the top-level error don't silently miss per-item failures. Null only when every item succeeded. """ error: MutationError } """ The outcome of a single update inside `bulkUpdateSlackChannelSettings`. """ type BulkUpdateSlackChannelSettingResult { """The Plain slack channel id this result corresponds to.""" slackChannelId: ID! """The setting code this result corresponds to.""" code: String! """The setting after the update. Null when the update failed.""" setting: Setting """Per-item error, if the update failed. Null when the update succeeded.""" error: MutationError } input CreateMySlackIntegrationInput { authCode: String! redirectUrl: String! } type CreateMySlackIntegrationOutput { integration: UserSlackIntegration error: MutationError } input CreateUserAuthSlackIntegrationInput { authCode: String! redirectUrl: String! } type CreateUserAuthSlackIntegrationOutput { integration: UserAuthSlackIntegration error: MutationError } input CreateWorkspaceSlackIntegrationInput { authCode: String! redirectUrl: String! } type CreateWorkspaceSlackIntegrationOutput { integration: WorkspaceSlackIntegration error: MutationError } input RefreshWorkspaceSlackChannelIntegrationInput { integrationId: ID! authCode: String! redirectUrl: String! } type RefreshWorkspaceSlackChannelIntegrationOutput { integration: WorkspaceSlackChannelIntegration error: MutationError } input DeleteWorkspaceSlackIntegrationInput { integrationId: ID! } type DeleteWorkspaceSlackIntegrationOutput { integration: WorkspaceSlackIntegration error: MutationError } input CreateWorkspaceSlackChannelIntegrationInput { authCode: String! redirectUrl: String! } type CreateWorkspaceSlackChannelIntegrationOutput { integration: WorkspaceSlackChannelIntegration error: MutationError } input DeleteWorkspaceSlackChannelIntegrationInput { integrationId: ID! } type DeleteWorkspaceSlackChannelIntegrationOutput { integration: WorkspaceSlackChannelIntegration error: MutationError } input CreateWorkspaceSlackSidekickIntegrationInput { authCode: String! redirectUrl: String! } type CreateWorkspaceSlackSidekickIntegrationOutput { integration: WorkspaceSlackSidekickIntegration error: MutationError } input RefreshWorkspaceSlackSidekickIntegrationInput { authCode: String! redirectUrl: String! } type RefreshWorkspaceSlackSidekickIntegrationOutput { integration: WorkspaceSlackSidekickIntegration error: MutationError } type DeleteWorkspaceSlackSidekickIntegrationOutput { integration: WorkspaceSlackSidekickIntegration error: MutationError } input UpdateSidekickSlackConfigInput { operatingInstructions: String } type UpdateSidekickSlackConfigOutput { integration: WorkspaceSlackSidekickIntegration error: MutationError } type DeleteMySlackIntegrationOutput { error: MutationError } input DeleteUserAuthSlackIntegrationInput { slackTeamId: String! } type DeleteUserAuthSlackIntegrationOutput { error: MutationError } type SlackUserConnection { edges: [SlackUserEdge!]! pageInfo: PageInfo! } type SlackUserEdge { cursor: String! node: SlackUser! } type SlackUser { id: ID! """The user's ID as provided by Slack.""" slackUserId: ID! slackAvatarUrl72px: String """The user's Slack handle (username without the @ prefix).""" slackHandle: String! fullName: String! """ Whether this user is currently a member of the Slack channel that was queried. """ isInChannel: Boolean! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } type SlackChannelMembership { """The Slack channel ID the current user is a member of.""" slackChannelId: ID! } input BulkJoinSlackChannelsInput { integrationId: ID! } type BulkJoinSlackChannelsOutput { error: MutationError } input UpdateConnectedSlackChannelInput { connectedSlackChannelId: ID! channelType: ConnectedSlackChannelType isEnabled: BooleanInput } type UpdateConnectedSlackChannelOutput { connectedSlackChannel: ConnectedSlackChannel error: MutationError } """An input provided to the `bulkUpdateConnectedSlackChannels` mutation.""" input BulkUpdateConnectedSlackChannelsInput { """ List of per-channel updates to apply. Each item is applied independently; partial failures are reported per-item. """ updates: [BulkUpdateConnectedSlackChannelItem!]! } """ A single update applied by the `bulkUpdateConnectedSlackChannels` mutation. Mirrors `UpdateConnectedSlackChannelInput`. """ input BulkUpdateConnectedSlackChannelItem { connectedSlackChannelId: ID! channelType: ConnectedSlackChannelType isEnabled: BooleanInput } """ An output type provided by the `bulkUpdateConnectedSlackChannels` mutation. Returns per-update results so the caller can distinguish successful writes from failed ones. """ type BulkUpdateConnectedSlackChannelsOutput { """Per-update results, in the same order as the input list.""" results: [BulkUpdateConnectedSlackChannelResult!]! """ Top-level error. Set to a `bulk_partial_failure` error whenever any per-item update failed, so callers that only check the top-level error don't silently miss per-item failures. Null only when every item succeeded. """ error: MutationError } """ The outcome of a single update inside `bulkUpdateConnectedSlackChannels`. """ type BulkUpdateConnectedSlackChannelResult { """The Plain slack channel id this result corresponds to.""" connectedSlackChannelId: ID! """The updated connected slack channel. Null when the update failed.""" connectedSlackChannel: ConnectedSlackChannel """Per-item error, if the update failed. Null when the update succeeded.""" error: MutationError } input ConnectedSlackChannelsFilter { slackTeamIds: [String!] slackChannelIds: [String!] channelTypes: [ConnectedSlackChannelType!] isEnabled: BooleanInput name: String } type ConnectedSlackChannelConnection { pageInfo: PageInfo! edges: [ConnectedSlackChannelEdge!]! totalCount: Int! } type ConnectedSlackChannelEdge { cursor: String! node: ConnectedSlackChannel! } enum ConnectedSlackChannelType { """A channel that Plain tracks for customer support requests.""" CUSTOMER """A channel that Plain tracks for internal team discussions.""" DISCUSSION } type ConnectedSlackChannel { id: ID! """The Slack workspace ID this channel belongs to.""" slackTeamId: String! """The Slack channel ID as provided by Slack.""" slackChannelId: String! name: String! """ Whether this channel is used for customer support requests or internal team discussions. """ channelType: ConnectedSlackChannelType! """ Whether Plain is actively monitoring this channel. Disabled channels are connected but not processed. """ isEnabled: Boolean! """Whether this is a private Slack channel.""" isPrivate: Boolean! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! """ Thread-channel association rules that control which threads are routed to this channel, scoped by company or tenant. """ threadChannelAssociations: [SlackThreadChannelAssociation!]! } """ The default channel mode an auto-join rule applies to newly joined channels. """ enum SlackAutoJoinRuleChannelMode { """Connect the channel as a customer channel.""" CUSTOMER """Connect the channel as an internal discussion channel.""" DISCUSSION """Connect the channel in a disabled state.""" DISABLED } """How threads are created from messages in a connected slack channel.""" enum SlackIngestionMode { """Each message starts a new thread after a period of inactivity.""" TIME_BASED """An AI model decides when a message starts a new thread.""" AI_BASED """Each message always creates its own thread.""" SINGLE_MESSAGE """A thread is created only when a user reacts with a specific emoji.""" MANUAL } """ Manual (emoji-reaction) ingestion configuration. Each field is null when the team default for that option applies. """ type SlackAutoJoinRuleManualIngestion { """ The reaction emoji that triggers ingestion. Null means the team default emoji applies. """ emoji: String """Whether customers (not just Plain users) can trigger ingestion.""" isCustomerEnabled: Boolean! } """ The default thread-creation (ingestion) configuration an auto-join rule applies to channels it connects. `manual` is set only when `type` is `MANUAL`; it is null for other modes. """ type SlackAutoJoinRuleIngestionMode { type: SlackIngestionMode! manual: SlackAutoJoinRuleManualIngestion } """ Manual (emoji-reaction) ingestion configuration provided to setSlackAutoJoinRules. """ input SlackAutoJoinRuleManualIngestionInput { emoji: String isCustomerEnabled: Boolean! } """ The default thread-creation (ingestion) configuration for an auto-join rule. Provide `manual` only when `type` is `MANUAL` — it is ignored for other modes. Omit the whole object to inherit the team default ingestion mode. """ input SlackAutoJoinRuleIngestionModeInput { type: SlackIngestionMode! manual: SlackAutoJoinRuleManualIngestionInput } """ A rule defining which Slack channels the Plain bot automatically joins, plus optional default channel options applied when a channel is connected via this rule. """ type SlackAutoJoinRule { """ Null for legacy rules read from the prefix/suffix settings that have not yet been saved through `setSlackAutoJoinRules`. """ id: ID """The workspace Slack channel integration this rule belongs to.""" integrationId: ID! """ Plain joins any new Slack channel whose name starts with this string. Empty string means no prefix filter. """ channelNamePrefix: String! """ Plain joins any new Slack channel whose name ends with this string. Empty string means no suffix filter. """ channelNameSuffix: String! """ Null means the channel mode is detected automatically (current behaviour). """ defaultChannelMode: SlackAutoJoinRuleChannelMode """Null means the team default ingestion mode applies.""" defaultIngestionMode: SlackAutoJoinRuleIngestionMode } input SlackAutoJoinRuleInput { """ At least one of channelNamePrefix and channelNameSuffix must be non-empty. """ channelNamePrefix: String! channelNameSuffix: String! defaultChannelMode: SlackAutoJoinRuleChannelMode """Omit to inherit the team default ingestion mode.""" defaultIngestionMode: SlackAutoJoinRuleIngestionModeInput } """An input provided to the `setSlackAutoJoinRules` mutation.""" input SetSlackAutoJoinRulesInput { integrationId: ID! """ The complete set of auto-join rules for the integration. Replaces any existing rules. """ rules: [SlackAutoJoinRuleInput!]! } """An output type provided by the `setSlackAutoJoinRules` mutation.""" type SetSlackAutoJoinRulesOutput { """The saved rules. Empty when the mutation failed.""" rules: [SlackAutoJoinRule!]! error: MutationError } type ConnectedDiscordChannel { id: ID! """The ID of the Discord guild (server) this channel belongs to.""" discordGuildId: String! """The Discord-assigned ID for this channel.""" discordChannelId: String! name: String! """ Whether this channel is enabled for receiving and sending messages via Plain. Disabled channels are ignored. """ isEnabled: Boolean! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } type ConnectedDiscordChannelConnection { pageInfo: PageInfo! edges: [ConnectedDiscordChannelEdge!]! } type ConnectedDiscordChannelEdge { cursor: String! node: ConnectedDiscordChannel! } input CreateWorkspaceDiscordChannelIntegrationInput { authCode: String! redirectUrl: String! } type CreateWorkspaceDiscordChannelIntegrationOutput { integration: WorkspaceDiscordChannelIntegration error: MutationError } input CreateWorkspaceDiscordIntegrationInput { name: String! webhookUrl: String! } type CreateWorkspaceDiscordIntegrationOutput { integration: WorkspaceDiscordIntegration error: MutationError } input DeleteWorkspaceDiscordIntegrationInput { integrationId: ID! } type DeleteWorkspaceDiscordIntegrationOutput { integration: WorkspaceDiscordIntegration error: MutationError } input DeleteWorkspaceDiscordChannelIntegrationInput { integrationId: ID! } type DeleteWorkspaceDiscordChannelIntegrationOutput { error: MutationError } input DeleteUserAuthDiscordChannelIntegrationInput { integrationId: ID! } type DeleteUserAuthDiscordChannelIntegrationOutput { error: MutationError } input RefreshConnectedDiscordChannelsInput { discordGuildId: String! } type RefreshConnectedDiscordChannelsOutput { error: MutationError } input UpdateConnectedDiscordChannelInput { connectedDiscordChannelId: ID! isEnabled: BooleanInput } type UpdateConnectedDiscordChannelOutput { connectedDiscordChannel: ConnectedDiscordChannel error: MutationError } input CreateUserAuthDiscordChannelIntegrationInput { discordGuildId: String! authCode: String! redirectUrl: String! } type CreateUserAuthDiscordChannelIntegrationOutput { integration: UserAuthDiscordChannelIntegration error: MutationError } type UserAuthDiscordChannelIntegration { id: ID! """ The Discord guild (server) this personal auth integration is scoped to. """ discordGuildId: String! """The user's Discord user ID.""" discordUserId: String! """ The user's Discord username (the unique handle, e.g. `username#0000` or the new username format). """ discordUsername: String! """ The user's Discord display name if they have set one (separate from their username). """ discordGlobalName: String """The email address associated with the user's Discord account.""" discordUserEmail: String! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } type UserAuthDiscordChannelIntegrationEdge { cursor: String! node: UserAuthDiscordChannelIntegration! } type UserAuthDiscordChannelIntegrationConnection { edges: [UserAuthDiscordChannelIntegrationEdge!]! pageInfo: PageInfo! } input CreateMyLinearIntegrationInput { authCode: String! redirectUrl: String! } type CreateMyLinearIntegrationOutput { integration: UserLinearIntegration error: MutationError } type DeleteMyLinearIntegrationOutput { error: MutationError } input CreateLinearAppIntegrationInput { authCode: String! redirectUrl: String! } type CreateLinearAppIntegrationOutput { integration: WorkspaceLinearIntegration error: MutationError } type DeleteLinearAppIntegrationOutput { error: MutationError } input CreateGithubUserAuthIntegrationInput { nangoSessionToken: String! nangoConnectionId: String! } type CreateGithubUserAuthIntegrationOutput { integration: GithubUserAuthIntegration error: MutationError } type DeleteGithubUserAuthIntegrationOutput { deletedIntegrationId: ID error: MutationError } input CreateWorkspaceCursorIntegrationInput { token: String! } type CreateWorkspaceCursorIntegrationOutput { integration: WorkspaceCursorIntegration error: MutationError } type DeleteWorkspaceCursorIntegrationOutput { id: ID error: MutationError } type ConnectedMSTeamsChannelConnection { pageInfo: PageInfo! edges: [ConnectedMSTeamsChannelEdge!]! totalCount: Int! } type ConnectedMSTeamsChannelEdge { cursor: String! node: ConnectedMSTeamsChannel! } type ConnectedMSTeamsChannel { id: ID! workspaceId: ID! """ The Azure Active Directory tenant ID of the Microsoft Teams organization this channel belongs to. """ msTeamsTenantId: ID! """The Microsoft Teams team (group) ID that contains this channel.""" msTeamsTeamId: ID! """The Microsoft Teams channel ID within the team.""" msTeamsChannelId: ID! name: String! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! """ The display name of the Microsoft Teams team (group) that contains this channel. """ teamName: String! } input CreateMyMSTeamsIntegrationInput { authCode: ID! redirectUrl: String! } type CreateMyMSTeamsIntegrationOutput { integration: UserMSTeamsIntegration error: MutationError } type DeleteMyMSTeamsIntegrationOutput { integration: UserMSTeamsIntegration error: MutationError } input CreateWorkspaceMSTeamsIntegrationInput { msTeamsTenantId: ID! } type CreateWorkspaceMSTeamsIntegrationOutput { integration: WorkspaceMSTeamsIntegration error: MutationError } input DeleteWorkspaceMSTeamsIntegrationInput { integrationId: ID! } type DeleteWorkspaceMSTeamsIntegrationOutput { integration: WorkspaceMSTeamsIntegration error: MutationError } type WorkspaceMSTeamsIntegration { id: ID! """ The Azure Active Directory tenant ID for the connected Microsoft Teams organization. """ msTeamsTenantId: ID! """ When true, the workspace admin must re-authorize the integration (e.g. because required permissions have changed). """ isReinstallRequired: Boolean! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } enum ChangeType { ADDED UPDATED REMOVED } type TimelineEntryChange { changeType: ChangeType! timelineEntry: TimelineEntry! } type CustomerChange { changeType: ChangeType! customer: Customer! } type ThreadChange { changeType: ChangeType! thread: Thread! } type DiscussionChange { changeType: ChangeType! discussion: ThreadDiscussion! """ The specific message that was created or updated. Populated only for message-level events; null for discussion-level changes (created, resolved, deleted, status changed). """ discussionMessage: ThreadDiscussionMessage } type UserChange { changeType: ChangeType! user: User! } type CustomerCardInstanceChange { changeType: ChangeType! customerCardInstance: CustomerCardInstance! } type ThreadFieldSchemaChange { changeType: ChangeType! threadFieldSchema: ThreadFieldSchema! } type InternalNotificationChange { """ Whether the notification was newly created (ADDED) or an existing notification was updated (UPDATED). """ changeType: ChangeType! internalNotification: InternalNotification! } """ Emitted to the current user when one of their Sidekick sessions becomes unread (i.e. an agent response settled into IDLE). Carries the affected discussion id; clients typically refetch their sessions list on receipt. """ type SidekickSessionsChange { discussionId: ID! } type SubscriptionAcknowledgement { subscriptionId: ID! } union CustomerCardInstanceChangesResult = CustomerCardInstanceChange | SubscriptionAcknowledgement type AgentSessionDelta { agentSessionId: ID! """ An incremental text chunk from the agent's current response. Append successive chunks to reconstruct the full message. """ text: String! } input ThreadChangesFilter { statuses: [ThreadStatus!] statusChangedAt: DatetimeFilter not: ThreadChangesFilter } type Subscription { """ Subscribes to all timeline entry changes (added, updated, or removed) across every thread for a given customer. Requires the `timeline:read` permission. """ timelineChanges(customerId: ID!): TimelineEntryChange! """ Subscribes to all timeline entry changes (added, updated, or removed) for a specific thread. Requires the `timeline:read` permission. """ threadTimelineChanges(threadId: ID!): TimelineEntryChange! """ Subscribe to real-time create, update, and delete events for all customers in the workspace. Each event includes a `changeType` and the affected `Customer`. """ customerChanges: CustomerChange! """ Real-time subscription that fires whenever a thread is created or updated. Accepts optional filters to limit events by thread status or the time the status last changed. Each event includes the full current thread and a `changeType` indicating whether the thread was created or updated. """ threadChanges(filters: ThreadChangesFilter): ThreadChange! """ Subscribe to real-time changes for a specific discussion. Fires when the discussion is created, updated, resolved, deleted, or when a message is added or updated. The changeType field indicates whether the change is an addition, update, or removal. When a message event fires, discussionMessage is populated with the affected message; otherwise it is null. Requires thread:read permission. """ discussionChanges(discussionId: ID!): DiscussionChange! """ Subscribes to customer card instance changes for a given customer. Emits a `CustomerCardInstanceChange` whenever a card transitions from loading to loaded or errored, or when a card is reloaded. The first message is a `SubscriptionAcknowledgement`. Requires the `customer:read` permission; the `customerCardConfig.apiUrl` and `customerCardConfig.apiHeaders` fields within the payload additionally require `customerCardConfigApiDetails:read`. """ customerCardInstanceChanges(customerId: ID!): CustomerCardInstanceChangesResult! """ Workspace-scoped subscription that fires whenever a member's status changes. Delivers a `UserChange` payload with the updated `User` and a `changeType` of `UPDATED`. Requires the `user:read` permission. """ userChanges: UserChange! """ Workspace-scoped subscription that fires whenever a thread field schema is created, updated, or deleted. Use `changeType` on the result to distinguish the operation. Requires the `threadFieldSchema:read` permission. """ threadFieldSchemaChanges: ThreadFieldSchemaChange! """ Real-time subscription that fires whenever an internal notification is created or updated for the currently authenticated user. Each event carries a `changeType` of `ADDED` (new notification) or `UPDATED` (e.g. read/archived state changed) together with the full notification object. Requires the `user:read` permission. """ internalNotificationChanges: InternalNotificationChange! """ Streams incremental text tokens from a running Sidekick agent session. Subscribe with the agentSessionId returned when starting a session; each event carries a text chunk that should be appended to the in-progress assistant message. The stream ends when the session status transitions to IDLE or an error occurs. """ agentSessionDelta(agentSessionId: ID!): AgentSessionDelta! """ Fires for the current user whenever one of their Sidekick sessions receives a new agent response and becomes unread (i.e. the session settles into IDLE). Use the returned discussionId to refetch or highlight the relevant session. The session becomes read again when it is opened or explicitly marked read. """ sidekickSessionsChanged: SidekickSessionsChange! } input UpsertCustomerGroupInput { identifier: CustomerGroupIdentifier! name: String! key: String! color: String! externalId: String } type UpsertCustomerGroupOutput { customerGroup: CustomerGroup result: UpsertResult error: MutationError } input CreateCustomerGroupInput { name: String! key: String! color: String! externalId: String } type CreateCustomerGroupOutput { customerGroup: CustomerGroup error: MutationError } input UpdateCustomerGroupInput { customerGroupId: ID! name: StringInput key: StringInput color: StringInput externalId: OptionalStringInput } type UpdateCustomerGroupOutput { customerGroup: CustomerGroup error: MutationError } input DeleteCustomerGroupInput { customerGroupId: ID! } type DeleteCustomerGroupOutput { error: MutationError } """ Identifies a customer group by exactly one of: its Plain-assigned ID, its unique key, or the external ID from your system. Provide exactly one field. """ input CustomerGroupIdentifier { customerGroupId: ID customerGroupKey: String externalId: String } input AddCustomerToCustomerGroupsInput { customerId: ID! customerGroupIdentifiers: [CustomerGroupIdentifier!]! } type AddCustomerToCustomerGroupsOutput { customerGroupMemberships: [CustomerGroupMembership!] error: MutationError } input RemoveCustomerFromCustomerGroupsInput { customerId: ID! customerGroupIdentifiers: [CustomerGroupIdentifier!]! } type RemoveCustomerFromCustomerGroupsOutput { error: MutationError } """Query to search for companies.""" input CompaniesSearchQuery { """ The term to search for. It must be at least 2 characters long. The search is case-insensitive on these two fields: - the company name (partial match) - the company domain name (partial match) """ term: String! } type CompanySearchResult { company: Company! } type CompanySearchResultEdge { cursor: String! node: CompanySearchResult! } type CompanySearchResultConnection { edges: [CompanySearchResultEdge!]! pageInfo: PageInfo! } """Query to search for tenants.""" input TenantsSearchQuery { """ The term to search for. It must be at least 2 characters long. The search is case-insensitive on these two fields: - the tenant name (partial match) - the tenant external id (exact match) """ term: String! } type TenantSearchResult { tenant: Tenant! } type TenantSearchResultEdge { cursor: String! node: TenantSearchResult! } type TenantSearchResultConnection { edges: [TenantSearchResultEdge!]! pageInfo: PageInfo! } input StartServiceAuthorizationInput { """ One of: zendesk, salesforce, freshdesk, helpscout-mailbox, hubspot, jira, shortcut, rootly, incidentio, github-app-oauth, attio. """ serviceIntegrationKey: String! } input ServiceAuthorizationsFilter { """ One of: zendesk, salesforce, freshdesk, helpscout-mailbox, hubspot, jira, shortcut, rootly, incidentio, github-app-oauth, attio. """ serviceIntegrationKey: String } type ServiceAuthorizationConnectionDetails { """ One of: zendesk, salesforce, freshdesk, helpscout-mailbox, hubspot, jira, shortcut, rootly, incidentio, github-app-oauth, attio. """ serviceIntegrationKey: String! """ The ID of the pending service authorization. Pass this to completeServiceAuthorization after the OAuth callback. """ serviceAuthorizationId: ID! """ HMAC-SHA256 digest that must be included in the OAuth authorization URL to verify the callback belongs to this workspace. """ hmacDigest: String! } type StartServiceAuthorizationOutput { connectionDetails: ServiceAuthorizationConnectionDetails error: MutationError } input CompleteJiraAuthorizationInput { refreshToken: String! siteId: String! } input CompleteServiceAuthorizationInput { serviceAuthorizationId: ID! """JSON-encoded payload of the service configuration.""" payload: String jira: CompleteJiraAuthorizationInput } type CompleteServiceAuthorizationOutput { serviceAuthorization: ServiceAuthorization error: MutationError } input DeleteServiceAuthorizationInput { serviceAuthorizationId: ID! } type DeleteServiceAuthorizationOutput { error: MutationError } input DeleteMyServiceAuthorizationInput { serviceAuthorizationId: ID! } type DeleteMyServiceAuthorizationOutput { error: MutationError } interface ServiceIntegration { name: String! key: String! } type JiraSite { id: ID! name: String! url: String! avatarUrl: String } type JiraSiteIntegration implements ServiceIntegration { name: String! key: String! site: JiraSite! } type DefaultServiceIntegration implements ServiceIntegration { name: String! key: String! } """ The status of the service authorization. The status transitions are: PENDING_AUTH → COMPLETED_AUTH → CONNECTED ↔ REINSTALL_REQUIRED Once connected, the status may revert to REINSTALL_REQUIRED if the integration is revoked in the third-party service; re-running the authorization flow will restore it to CONNECTED. """ enum ServiceAuthorizationStatus { """ Service authorization was requested, but the user has not yet completed the authorization. """ PENDING_AUTH """ User has completed the service authorization, but the service is not yet ready for use. This happens when the service requires additional configuration (e.g. creating webhooks in the service). This is a transient state that typically lasts for a few seconds. Plain will automatically attempt to configure the service, and transition to CONNECTED or REINSTALL_REQUIRED. """ COMPLETED_AUTH """Service authorization is connected and ready for use.""" CONNECTED """ Service authorization was revoked, this typically happen when the Plain integration is removed from the service. Plain keeps the service authorization to allow for reconnection without losing the service's configuration. """ REINSTALL_REQUIRED } type ServiceAuthorization { id: ID! """ The third-party service this authorization connects to, including its name and integration key. """ serviceIntegration: ServiceIntegration! status: ServiceAuthorizationStatus! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! """ Timestamp when the service authorization moved to CONNECTED status for the first time. """ connectedAt: DateTime! connectedBy: InternalActor! """ Whether this service authorization uses the import runner system for syncing data into Plain. When true, data is synced via Plain's import runner pipeline. When false, data is synced via the legacy Nango-managed sync pipeline. """ isImportRunnerIntegration: Boolean! } type ServiceAuthorizationEdge { cursor: String! node: ServiceAuthorization! } type ServiceAuthorizationConnection { edges: [ServiceAuthorizationEdge!]! pageInfo: PageInfo! } """ A GitHub repository the workspace's Sidekick GitHub integration has access to. """ type SidekickGithubRepo { """ GitHub's numeric repository id. This value is stable across repository renames and should be used as the canonical identifier when configuring selected repos. """ repoId: ID! owner: String! name: String! } """A repository selected for the workspace's Sidekick GitHub integration.""" type SidekickGithubSelectedRepo { repoId: ID! owner: String! name: String! """Free-text guidance for the agent that is specific to this repository.""" operatingInstructions: String } """ The Sidekick GitHub configuration for a workspace: selected repositories and operating instructions. """ type SidekickGithubServiceConfig { """ The repositories Sidekick is configured to read from, each with optional per-repo operating instructions. """ selectedRepos: [SidekickGithubSelectedRepo!]! """Free-text guidance for the agent that applies across the workspace.""" operatingInstructions: String } """ A repository to select for the workspace's Sidekick GitHub integration, with optional per-repo operating instructions. """ input SidekickGithubRepoInput { """GitHub's repository id.""" repoId: ID! owner: String! name: String! """Free-text guidance for the agent that is specific to this repository.""" operatingInstructions: String } input UpdateSidekickGithubConfigInput { serviceAuthorizationId: ID! selectedRepos: [SidekickGithubRepoInput!]! """Free-text guidance for the agent that applies across the workspace.""" operatingInstructions: String } type UpdateSidekickGithubConfigOutput { serviceAuthorization: ServiceAuthorization error: MutationError } """ The Sidekick configuration for a connected service whose only user-editable setting is its operating instructions. Covers Datadog, Sentry, Grafana, Linear, Notion, incident.io, Attio, HubSpot, Jira, Granola and LaunchDarkly. The access scope of each service is enforced by the credentials the workspace connected, not by this configuration. GitHub has a richer shape — see SidekickGithubServiceConfig. """ type SidekickServiceConfig { """The service authorization this configuration belongs to.""" serviceAuthorizationId: ID! """ Free-text guidance for the agent that applies to this service across the workspace. """ operatingInstructions: String } input UpdateSidekickServiceConfigInput { serviceAuthorizationId: ID! """ Free-text guidance for the agent that applies to this service across the workspace. Pass null or an empty string to clear. """ operatingInstructions: String } type UpdateSidekickServiceConfigOutput { serviceAuthorization: ServiceAuthorization error: MutationError } """ The Sidekick PostHog configuration for a workspace: operating instructions plus the default project the agent queries. """ type SidekickPosthogServiceConfig { """ Free-text guidance for the agent that applies to PostHog use across the workspace. """ operatingInstructions: String """ The default PostHog project (team) id the agent queries when the user doesn't specify one. Null when no default has been set; the agent is then expected to call project.list and pick one or ask the user. """ projectId: ID } input UpdateSidekickPosthogConfigInput { serviceAuthorizationId: ID! """ Free-text guidance for the agent that applies to PostHog use across the workspace. Pass null or an empty string to clear. """ operatingInstructions: String """ The default PostHog project (team) id the agent queries when the user doesn't specify one. Pass null or an empty string to clear. """ projectId: ID } type UpdateSidekickPosthogConfigOutput { serviceAuthorization: ServiceAuthorization error: MutationError } """ Workspace-level settings for Sidekick. Wrapper type so additional settings can be added without breaking existing clients. """ type SidekickSettings { """ Free-text instructions appended to Sidekick's system prompt for every session in this workspace, allowing teams to tailor its behavior (tone, escalation rules, domain knowledge, etc.). Null when no custom prompt has been configured. """ customPrompt: String } input UpdateSidekickSettingsInput { """ Custom prompt to append to the Sidekick system prompt. Pass null or an empty string to clear. """ customPrompt: String } type UpdateSidekickSettingsOutput { sidekickSettings: SidekickSettings error: MutationError } """ When Sidekick is allowed to use a tool: - NOT_REQUIRED: Sidekick runs the tool directly. - APPROVAL_REQUIRED: Sidekick must request human approval before running it. - DISABLED: the tool is listed to Sidekick but every call is rejected. """ enum AgentSandboxToolMode { NOT_REQUIRED APPROVAL_REQUIRED DISABLED } """ The resolved approval policy for one Sidekick tool: its effective mode plus the factory default it derives from. """ type AgentSandboxToolPolicy { """Registry service name, e.g. "plain" or "slack".""" service: String! """Op id within the service, e.g. "thread.note".""" op: String! """Human-readable tool name shown as the row label.""" displayName: String! """One-line description shown as the row subtitle.""" description: String! """ Effective mode after merging the workspace override over the factory default. """ mode: AgentSandboxToolMode! """The mode this tool uses when the workspace has set no override.""" factoryDefault: AgentSandboxToolMode! """ True when the effective mode comes from a workspace override rather than the default. """ isOverride: Boolean! } input UpdateAgentSandboxToolPolicyInput { service: String! op: String! mode: AgentSandboxToolMode! } type UpdateAgentSandboxToolPolicyOutput { policy: AgentSandboxToolPolicy error: MutationError } """ A list or view of tenants/companies available in a connected external service, used to scope which records are synced during an import. """ type ImporterTenantList { id: ID! name: String! } type ImporterTenantListEdge { node: ImporterTenantList! cursor: String! } type ImporterTenantListConnection { edges: [ImporterTenantListEdge!]! pageInfo: PageInfo! } input CreateImportSyncInput { serviceIntegrationKey: String! filters: ImportSyncFiltersInput! } input ImportSyncFiltersInput { tenantListId: ID tenantListName: String } type ImportJobDefinitionList { id: ID! name: String } type ImportJobDefinitionMetadata { lists: [ImportJobDefinitionList!]! } type ImportJobDefinition { id: ID! """ The sync mode for this definition (e.g. SYNC for continuous synchronization). """ mode: String! """ The entity types this definition syncs (e.g. TENANTS, CUSTOMERS, TENANT_FIELD_SCHEMAS). """ entityTypes: [String!]! """ Optional metadata about the import configuration, such as the tenant list being synced. """ metadata: ImportJobDefinitionMetadata """ When this import job definition was enabled. Null if it has never been enabled. """ enabledAt: DateTime createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } type CreateImportSyncOutput { importJobDefinition: ImportJobDefinition error: MutationError } input UpdateImportJobDefinitionInput { serviceIntegrationKey: String! isEnabled: Boolean } type UpdateImportJobDefinitionOutput { importJobDefinition: ImportJobDefinition error: MutationError } input BusinessHoursWeekDayInput { """ The time you open for business on this day as an UTC ISO time. For example: 09:00Z . """ startTime: String! """ The time you close for business on this day as an UTC ISO time. For example: 17:00Z . """ endTime: String! } """ Represents the times in which you are open for business during a week. Only provide the days you are open for business. """ input BusinessHoursWeekDaysInput { monday: BusinessHoursWeekDayInput tuesday: BusinessHoursWeekDayInput wednesday: BusinessHoursWeekDayInput thursday: BusinessHoursWeekDayInput friday: BusinessHoursWeekDayInput saturday: BusinessHoursWeekDayInput sunday: BusinessHoursWeekDayInput } input UpsertBusinessHoursInput { weekDays: BusinessHoursWeekDaysInput } type UpsertBusinessHoursOutput { businessHours: BusinessHours result: UpsertResult error: MutationError } type DeleteBusinessHoursOutput { error: MutationError } type BusinessHoursWeekDays { monday: BusinessHoursWeekDay tuesday: BusinessHoursWeekDay wednesday: BusinessHoursWeekDay thursday: BusinessHoursWeekDay friday: BusinessHoursWeekDay saturday: BusinessHoursWeekDay sunday: BusinessHoursWeekDay } """ The workspace's business hours schedule expressed as per-weekday open/close windows. A null value for a given day means the workspace is not open on that day. Deprecated in favour of BusinessHoursSlot which supports timezone-aware, multi-slot schedules. """ type BusinessHours { weekDays: BusinessHoursWeekDays! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } type BusinessHoursWeekDay { startTime: Time! endTime: Time! } enum WeekDay { MONDAY TUESDAY WEDNESDAY THURSDAY FRIDAY SATURDAY SUNDAY } type Timezone { name: String! } type BusinessHoursSlot { """ The IANA timezone in which opensAt and closesAt are interpreted (e.g. 'Europe/London'). """ timezone: Timezone! """The day of the week this slot applies to.""" weekday: WeekDay! """The time this slot opens, in HH:MM format (e.g. '09:00').""" opensAt: String! """The time this slot closes, in HH:MM format (e.g. '17:00').""" closesAt: String! } input BusinessHoursSlotInput { timezone: String! weekday: WeekDay! opensAt: String! closesAt: String! } input SyncBusinessHoursSlotsInput { slots: [BusinessHoursSlotInput!]! } type SyncBusinessHoursSlotsOutput { slots: [BusinessHoursSlot!]! error: MutationError } """ Configuration for automatic status switching based on a user's working hours. """ type UserWorkingHours { id: ID! userId: ID! """The timezone in which the working hours slots are defined.""" timezone: Timezone! """Whether automatic status switching is enabled.""" isEnabled: Boolean! """ The UTC time at which the system will next automatically change this user's status. Null when automatic switching is disabled or no future slot is scheduled. """ nextStatusTransitionAt: DateTime """ The status the user will transition to at the next scheduled time. Null when no transition is pending. """ nextStatusTransitionTo: UserStatus """The time slots that define when the user is available.""" slots: [UserWorkingHoursSlot!]! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } """ A time slot representing when a user is available during a specific day. """ type UserWorkingHoursSlot { id: ID! """The day of the week this slot applies to.""" weekday: WeekDay! """ The time the slot starts at this time (HH:mm format in the user's timezone). """ startsAt: String! """ The time the slot ends at this time (HH:mm format in the user's timezone). """ endsAt: String! } input UserWorkingHoursSlotInput { """The time the slot is for this day of the week.""" weekday: WeekDay! """The time the slot starts at this time (HH:mm format).""" startsAt: String! """The time the slot ends at this time (HH:mm format).""" endsAt: String! } input SyncUserWorkingHoursInput { """ The user to configure working hours for. Required for admins setting other users' hours. """ userId: ID """The timezone for the working hours slots.""" timezone: String! """Whether automatic status switching should be enabled.""" isEnabled: Boolean! """The time slots that define when the user is available.""" slots: [UserWorkingHoursSlotInput!]! } type SyncUserWorkingHoursOutput { userWorkingHours: UserWorkingHours error: MutationError } input UpdateThreadTenantInput { threadId: ID! tenantIdentifier: TenantIdentifierInput } type UpdateThreadTenantOutput { thread: Thread error: MutationError } input UpdateThreadTierInput { threadId: ID! tierIdentifier: TierIdentifierInput } type UpdateThreadTierOutput { thread: Thread error: MutationError } input UpdateThreadEscalationPathInput { threadId: ID! escalationPathId: ID } type UpdateThreadEscalationPathOutput { thread: Thread error: MutationError } input UpdateThreadAgentStatusInput { threadId: ID! agentStatus: AgentStatus } type UpdateThreadAgentStatusOutput { thread: Thread error: MutationError } input UpdateThreadSuggestedActionStatusInput { threadId: ID! suggestedActionId: ID! status: ThreadCatchupSuggestedActionStatus! } type UpdateThreadSuggestedActionStatusOutput { thread: Thread error: MutationError } input AddCustomerToTenantsInput { customerIdentifier: CustomerIdentifierInput! tenantIdentifiers: [TenantIdentifierInput!]! } input RemoveCustomerFromTenantsInput { customerIdentifier: CustomerIdentifierInput! tenantIdentifiers: [TenantIdentifierInput!]! } input SetCustomerTenantsInput { customerIdentifier: CustomerIdentifierInput! tenantIdentifiers: [TenantIdentifierInput!]! } type AddCustomerToTenantsOutput { customer: Customer error: MutationError } type RemoveCustomerFromTenantsOutput { customer: Customer error: MutationError } type SetCustomerTenantsOutput { customer: Customer error: MutationError } input CompaniesFilter { companyIds: [ID!] """ True to only return companies that have been deleted. False to only return companies that have not been deleted. Omit to return all companies. """ isDeleted: Boolean updatedAt: DatetimeFilter } input TenantsFilter { tenantIds: [ID!] """ True to only return tenants that have been deleted. False to only return tenants that have not been deleted. Omit to return all tenants. """ isDeleted: Boolean updatedAt: DatetimeFilter } enum BillingSubscriptionStatus { """ The subscription is current and the workspace has full access to its plan features. """ ACTIVE """ The subscription has expired, been cancelled, or is otherwise not in good standing. """ INACTIVE } enum BillingPlanKey { """A grandfathered plan no longer available for new subscriptions.""" LEGACY """Free evaluation / trial plan.""" EVALUATE """Entry-level paid plan.""" LAUNCH """Mid-tier paid plan.""" GROW """Upper mid-tier paid plan.""" SCALE FOUNDATION HORIZON FRONTIER } enum HyperlineCheckoutPlanKey { FOUNDATION HORIZON FRONTIER } enum BillingSeatType { VIEWER @deprecated MEMBER @deprecated ENG_ROTA @deprecated } enum BillingInterval { MONTH @deprecated(reason: "Use BillingIntervalUnit.MONTH instead") YEAR @deprecated(reason: "Use BillingIntervalUnit.YEAR instead") } enum BillingIntervalUnit { MONTH YEAR } input CreateHyperlineCheckoutSessionInput { plan: HyperlineCheckoutPlanKey! intervalUnit: BillingIntervalUnit! } type CreateHyperlineCheckoutSessionOutput { checkoutUrl: String error: MutationError } enum CurrencyCode { USD } type Price { amount: Int! currency: CurrencyCode! } type PriceTier { maxSeats: Int! perSeatAmount: Int! flatAmount: Int! } interface RecurringPrice { billingIntervalUnit: BillingIntervalUnit! billingIntervalCount: Int! currency: CurrencyCode! } type PerSeatRecurringPrice implements RecurringPrice { billingIntervalUnit: BillingIntervalUnit! billingIntervalCount: Int! currency: CurrencyCode! perSeatAmount: Int! } type TieredRecurringPrice implements RecurringPrice { billingIntervalUnit: BillingIntervalUnit! billingIntervalCount: Int! currency: CurrencyCode! tiers: [PriceTier!]! } type BillingPlan { key: BillingPlanKey! name: String! description: String! """List of human-readable feature descriptions included in this plan.""" features: [String!]! """ Optional marketing label shown to highlight this plan (e.g. 'Most popular'). """ highlightedLabel: String """ Whether this plan can be purchased directly via the self-serve checkout flow. """ isSelfCheckoutEligible: Boolean! monthlyPrice: Price @deprecated(reason: "Use prices instead") yearlyPrice: Price @deprecated(reason: "Use prices instead") """ Available recurring prices for this plan, covering all supported billing intervals. """ prices: [RecurringPrice!]! } type BillingPlanConnection { edges: [BillingPlanEdge!]! pageInfo: PageInfo! } type BillingPlanEdge { cursor: String! node: BillingPlan! } type CreateHyperlineBillingPortalSessionOutput { billingPortalSessionUrl: String error: MutationError } type CreateHyperlineComponentsAuthTokenOutput { token: String error: MutationError } type CancelHyperlineSubscriptionOutput { error: MutationError } enum FeatureKey { BUSINESS_HOURS SLACK_DISCUSSIONS SERVICE_LEVEL_AGREEMENTS DATA_IMPORTERS ZENDESK_IMPORTER INTERCOM_IMPORTER SALESFORCE_IMPORTER HELPSCOUT_SYNC HUBSPOT_SYNC INCIDENTIO_INTEGRATION ATTIO_IMPORTER FRESHDESK_IMPORTER ROOTLY_INTEGRATION MS_TEAMS_INTEGRATION LIVE_CHAT ESCALATION_PATHS DISCORD_INTEGRATION WORKFLOW_RULES CONNECTED_CUSTOMER_SLACK_CHANNELS CONNECTED_SUPPORT_EMAIL_ADDRESSES INSIGHTS_LOOKBACK_DAYS BILLING_ROTA_SEATS MORE_ACTIVE_ENG_ROTA_SEATS AI_SUGGESTED_RESPONSES TEAM_REPORTING CUSTOMER_SURVEYS HELP_CENTER AI_AGENT AI_ASSISTANT CUSTOM_ROLES CURSOR_AGENTS SSO } interface BillingFeatureEntitlement { feature: FeatureKey! isEntitled: Boolean! } type ToggleFeatureEntitlement implements BillingFeatureEntitlement { feature: FeatureKey! isEntitled: Boolean! } type MeteredFeatureEntitlement implements BillingFeatureEntitlement { feature: FeatureKey! isEntitled: Boolean! current: Int! limit: Int! } type BillingSubscription { status: BillingSubscriptionStatus! planKey: BillingPlanKey! """Internal plan code string from the billing provider.""" planCode: String planName: String! interval: BillingInterval """ The date the subscription is scheduled to cancel at the end of the current period. Null if not set to cancel. """ cancelsAt: DateTime """The date the free trial ends. Null if not on a trial.""" trialEndsAt: DateTime """ List of feature entitlements for the current plan, indicating which features are available and any usage limits. """ entitlements: [BillingFeatureEntitlement!]! """The date the subscription was terminated. Null if still active.""" endedAt: DateTime """ An in-progress Hyperline checkout session, if one exists for this workspace. """ checkoutSession: HyperlineCheckoutSession """ Current credit balances across all credit products (monthly allowance and top-up). Empty array if no credit products are attached. """ creditBalances: [BillingCreditBalance!]! } union BillingCreditBalance = BillingMonthlyAllowanceCreditBalance | BillingTopupCreditBalance enum BillingCreditBalanceType { monthlyAllowance topUp } type BillingMonthlyAllowanceCreditBalance { type: BillingCreditBalanceType! """Total credits granted for the current billing period.""" allowance: Int! """Credits consumed so far in the current billing period.""" used: Int! """ Credits remaining in the current billing period. Resets to allowance at periodEndsAt. """ remaining: Int! """ The end of the current grant period, after which unused allowance credits expire and a new allowance is issued. """ periodEndsAt: DateTime! """ If set, a warning should be shown when remaining credits fall below this threshold. """ lowBalanceThreshold: Int } type BillingTopupCreditBalance { type: BillingCreditBalanceType! """ Credits remaining from purchased top-up bundles. Top-up credits do not expire and are used after the monthly allowance is exhausted. """ remaining: Int! } input SidekickCreditUsageByDayInput { """ Start of the range, inclusive. ISO 8601 format (e.g. 2026-06-01T00:00:00Z). """ from: String! """ End of the range, exclusive. ISO 8601 format (e.g. 2026-07-01T00:00:00Z). """ to: String! } """Sidekick credit usage broken down by UTC day.""" type SidekickCreditUsageByDay { """ One entry per day in the range that had usage, ordered by day ascending. """ days: [SidekickCreditUsageDay!]! } """Sidekick credit usage for a single UTC day.""" type SidekickCreditUsageDay { """Start of the UTC day this usage falls in.""" day: DateTime! """Credits charged across the sessions that resolved on this day.""" credits: Int! """Number of Sidekick sessions that resolved on this day.""" sessionCount: Int! } type HyperlineCheckoutSession { id: ID! status: HyperlineCheckoutSessionStatus! url: String! } enum HyperlineCheckoutSessionStatus { OPENED COMPLETED CANCELLED ERRORED } input CalculateRoleChangeCostInput { roleKey: RoleKey! quantity: IntInput userId: ID usingBillingRotaSeat: BooleanInput } type RoleChangeCost { """Deprecated. Use fullPrice instead.""" totalPrice: Price! """ The total price delta for the entire subscription billing period (i.e. non-prorated). Could be negative (e.g. swapping member for viewer). """ fullPrice: Price! """ The total price delta for the remainder of the current billing period (i.e. prorated). Could be negative (e.g. swapping member for viewer). """ adjustedPrice: Price! """ Total amount that will be invoiced immediately for the role change. If this is negative, we would credit the amount to your account. """ dueNowPrice: Price! """The number of seats.""" quantity: Int! intervalUnit: BillingIntervalUnit! intervalCount: Int! addingSeatType: BillingSeatType! removingSeatType: BillingSeatType } type CalculateRoleChangeCostOutput { roleChangeCost: RoleChangeCost error: MutationError } input AddUserToActiveBillingRotaInput { userId: ID! } type AddUserToActiveBillingRotaOutput { billingRota: BillingRota error: MutationError } input RemoveUserFromActiveBillingRotaInput { userId: ID! } type RemoveUserFromActiveBillingRotaOutput { billingRota: BillingRota error: MutationError } type BillingRota { """IDs of users who currently hold an active eng-rota billing seat.""" onRotaUserIds: [ID!]! """ IDs of users who hold a billing rota seat but are currently off-rota (not consuming an active seat). """ offRotaUserIds: [ID!]! } input UpdateActiveBillingRotaInput { userIdsToAdd: [ID!] userIdsToRemove: [ID!] } type UpdateActiveBillingRotaOutput { billingRota: BillingRota error: MutationError } """Stable identifiers for the built-in workspace roles.""" enum RoleKey { OWNER ADMIN SUPPORT VIEWER """ Represents no role — the user exists in the workspace but has no permissions. """ NONE } input MachineUsersFilter { type: MachineUserType } enum MachineUserType { """A machine user that interacts via API""" API_USER """A machine user that represents an AI Agent""" AI_AGENT } input ChangeBillingPlanInput { planKey: BillingPlanKey! intervalUnit: BillingIntervalUnit } type ChangeBillingPlanOutput { error: MutationError } input PreviewBillingPlanChangeInput { planKey: BillingPlanKey! intervalUnit: BillingIntervalUnit } type PreviewBillingPlanChangeOutput { preview: BillingPlanChangePreview error: MutationError } type BillingPlanChangePreview { immediateCost: Price! earliestEffectiveAt: DateTime! } input PurchaseCreditsInput { """ The number of credits to purchase. Must match the unit count of a configured top-up bundle. """ count: Int! } type PurchaseCreditsOutput { creditBalance: BillingTopupCreditBalance error: MutationError } type WorkspaceHmac { """ The HMAC secret string used to sign outbound HTTP requests from Plain. Treat this value like a password — store it securely and rotate it with `regenerateWorkspaceHmac` if it is ever compromised. """ hmacSecret: String createdAt: DateTime! """The user who first generated the HMAC secret for this workspace.""" createdBy: InternalActor! updatedAt: DateTime! """The user who most recently regenerated the HMAC secret.""" updatedBy: InternalActor! } type RegenerateWorkspaceHmacOutput { workspaceHmac: WorkspaceHmac error: MutationError } """Controls where the file is stored and how it can be accessed.""" enum WorkspaceFileVisibility { """ File is stored in a private bucket; access requires a short-lived pre-signed download URL obtained via createWorkspaceFileDownloadUrl. """ PRIVATE """ File is stored in a public bucket and is accessible via a permanent CDN URL with no authentication required. """ PUBLIC } type WorkspaceFileDownloadUrl { downloadUrl: String! """ The time when the download URL will expire. Only set when visibility of the workspace file is PRIVATE. """ expiresAt: DateTime } type WorkspaceFile { id: ID! fileName: String! """The size of the file expressed in bytes, kilobytes, and megabytes.""" fileSize: FileSize! """ The file extension detected from the uploaded content (e.g. 'png', 'pdf'). Null until the file has been uploaded and its type determined. """ fileExtension: String """ The MIME type detected from the uploaded content (e.g. 'image/png'). Set to 'application/octet-stream' until the file has been uploaded. """ fileMimeType: String! """ Whether the file is stored in a publicly accessible bucket (PUBLIC) or a private bucket requiring a signed URL (PRIVATE). """ visibility: WorkspaceFileVisibility! """This URL will only be available after the file has been uploaded.""" downloadUrl: WorkspaceFileDownloadUrl createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } type WorkspaceFileUploadUrl { workspaceFile: WorkspaceFile! """ The S3 endpoint URL to POST the file to. Must be used together with uploadFormData fields. """ uploadFormUrl: String! """ Key-value pairs that must be included as form fields in the multipart POST to uploadFormUrl, alongside the file binary. """ uploadFormData: [UploadFormData!]! """ When this upload URL expires. The upload must be completed before this time. """ expiresAt: DateTime! } input CreateWorkspaceFileUploadUrlInput { fileName: String! fileSizeBytes: Int! visibility: WorkspaceFileVisibility! } type CreateWorkspaceFileUploadUrlOutput { workspaceFileUploadUrl: WorkspaceFileUploadUrl error: MutationError } input CreateWorkspaceFileDownloadUrlInput { workspaceFileId: ID! } type CreateWorkspaceFileDownloadUrlOutput { workspaceFileDownloadUrl: WorkspaceFileDownloadUrl error: MutationError } input DeleteWorkspaceFileInput { workspaceFileId: ID! } type DeleteWorkspaceFileOutput { error: MutationError } input ResolveCustomerForMSTeamsChannelInput { msTeamsTeamId: ID! msTeamsChannelId: ID! } input ResolveCustomerForSlackChannelInput { slackTeamId: String! slackChannelId: String! } type ResolveCustomerForSlackChannelOutput { error: MutationError customer: Customer } type ResolveCustomerForMSTeamsChannelOutput { error: MutationError customer: Customer } input HelpCentersFilter { isCustomerFacingAiEnabled: Boolean } type HelpCenterConnection { edges: [HelpCenterEdge!]! pageInfo: PageInfo! } type HelpCenterEdge { cursor: String! node: HelpCenter! } type HelpCenterArticleGroupConnection { edges: [HelpCenterArticleGroupEdge!]! pageInfo: PageInfo! } type HelpCenterArticleGroupEdge { cursor: String! node: HelpCenterArticleGroup! } enum HelpCenterArticleStatus { """The article is live and visible to customers on the help center.""" PUBLISHED """ The article is a work in progress and only visible inside the Plain app. """ DRAFT } type HelpCenterDomainNameVerificationTxtRecord { name: String! value: String! } type HelpCenterDomainSettings { """ The Plain-hosted subdomain for this help center (e.g. acme.plain.help). """ domainName: String! """ A custom domain configured to serve this help center (e.g. help.acme.com). Null if not configured. """ customDomainName: String """ The DNS TXT record that must be added to verify ownership of the custom domain. Null if no custom domain is set. """ customDomainNameVerificationTxtRecord: HelpCenterDomainNameVerificationTxtRecord """ When the custom domain was successfully verified. Null if not yet verified. """ customDomainNameVerifiedAt: DateTime } type HelpCenterAccessSettings { """ IDs of tiers whose members are allowed to access this PRIVATE help center. """ tierIds: [String!]! """ IDs of tenants whose members are allowed to access this PRIVATE help center. """ tenantIds: [String!]! """ IDs of companies whose customers are allowed to access this PRIVATE help center. """ companyIds: [String!]! """ IDs of individual customers explicitly allowed to access this PRIVATE help center. """ customerIds: [String!]! } enum HelpCenterType { """Accessible to anyone without authentication.""" PUBLIC """ Accessible only to authenticated customers matching the configured access rules. """ PRIVATE """ Visible only to your team inside the Plain app; not accessible to customers. """ INTERNAL } enum HelpCenterAuthMechanismType { """Authentication via WorkOS AuthKit (hosted login UI managed by WorkOS).""" WORKOS_AUTHKIT """Authentication via a custom WorkOS Connect OAuth application.""" WORKOS_CONNECT } type HelpCenterAuthMechanismWorkosAuthkit { type: HelpCenterAuthMechanismType! } type HelpCenterAuthMechanismWorkosConnect { type: HelpCenterAuthMechanismType! """ WorkOS Connect OAuth application client ID used to authenticate customers. """ appClientId: String! """ Masked version of the WorkOS Connect application secret (last few characters only). """ appSecretMasked: String! """The WorkOS API host URL for this Connect integration.""" apiHost: String! } union HelpCenterAuthMechanism = HelpCenterAuthMechanismWorkosAuthkit | HelpCenterAuthMechanismWorkosConnect type HelpCenter { id: ID! """ Whether this help center is publicly accessible, restricted to specific customers/tenants/tiers, or internal-only. """ type: HelpCenterType! """The name shown to customers on the help center site.""" publicName: String! """ The name used to identify this help center within the Plain app (not shown to customers). """ internalName: String! description: String """Subdomain and custom domain configuration for this help center.""" domainSettings: HelpCenterDomainSettings! """ Settings for the customer support portal embedded in this help center (contact form, thread visibility, etc.). """ portalSettings: HelpCenterPortalSettings! """Custom JavaScript injected into the of every help center page.""" headCustomJs: String """ Custom JavaScript injected at the end of the of every help center page. """ bodyCustomJs: String """Whether the live chat widget is enabled on this help center.""" isChatEnabled: Boolean! """ Controls which copy-to-clipboard format options (plain text, markdown, ChatGPT, etc.) are shown on article pages. """ articleCopyOptions: HelpCenterArticleCopyOptions! """ Whether the AI-powered answer widget is enabled for customers browsing this help center. """ isCustomerFacingAiEnabled: Boolean! favicon: HelpCenterThemedImage! """ Avatar image used to represent the support agent in the AI chat widget on the help center. """ agentAvatarImage: HelpCenterThemedImage! logo: HelpCenterThemedImage! color: String """ Image used as the Open Graph / social media preview when links to this help center are shared. """ socialPreviewImage: WorkspaceFile """ Allowlist of tiers, tenants, companies, and customers that can access this help center when type is PRIVATE. """ access: HelpCenterAccessSettings """ The authentication mechanism used to verify customer identity on a PRIVATE help center. """ authMechanism: HelpCenterAuthMechanism! """ When this help center was first published (made publicly accessible). Null if never published. """ publishedAt: DateTime publishedBy: InternalActor """Whether this help center has been soft-deleted.""" isDeleted: Boolean! deletedAt: DateTime deletedBy: InternalActor createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! """All article groups in the help center.""" articleGroups(first: Int, after: String, last: Int, before: String): HelpCenterArticleGroupConnection! """All articles in the help center.""" articles(first: Int, after: String, last: Int, before: String): HelpCenterArticleConnection! } type HelpCenterIndex { """The ID of the help center this index belongs to.""" helpCenterId: ID! """ Opaque version token. Must be passed back to updateHelpCenterIndex to prevent overwriting concurrent changes. """ hash: String! """ Ordered, flat list of items that represents the navigation tree. Items reference their parent via parentId to reconstruct the hierarchy. """ navIndex: [HelpCenterIndexItem!]! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! } enum HelpCenterIndexItemType { """A link to a specific help center article.""" ARTICLE """A folder grouping articles in the navigation.""" ARTICLE_GROUP """A plain text label used to visually separate sections in the sidebar.""" HEADING } type HelpCenterIndexItem { """ The kind of entry: an article, an article group folder, or a plain heading label. """ type: HelpCenterIndexItemType! id: ID! title: String! slug: String! """ ID of the parent ARTICLE_GROUP item, or null if this item is at the top level. """ parentId: ID """Publication status of the article. Null for ARTICLE_GROUP items.""" status: HelpCenterArticleStatus """ Short summary. Null for ARTICLE_GROUP items and for articles that have no description set. """ description: String """Opaque icon key for ARTICLE items; null for groups or when unset.""" icon: String } type HelpCenterArticleGroup { id: ID! name: String! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! """The help center this group belongs to.""" helpCenter: HelpCenter! """Parent group in the hierarchy. Null for top-level groups.""" parentArticleGroup: HelpCenterArticleGroup """Direct child groups under this group.""" childArticleGroups(first: Int, after: String, last: Int, before: String): HelpCenterArticleGroupConnection! """Direct articles in this group.""" articles(first: Int, after: String, last: Int, before: String): HelpCenterArticleConnection! """ URL-safe identifier for this article group, unique within the help center. """ slug: String! } type HelpCenterArticle { id: ID! title: String! """ Short summary of the article shown in search results and navigation previews. """ description: String """ Opaque icon key that identifies the decorative icon displayed alongside the article title. """ icon: String """ Full article body as HTML. This is rendered directly in the help center. """ contentHtml: String! """ URL-safe identifier for the article, unique within the help center. Normalized to lowercase. """ slug: String! """ Whether the article is publicly visible (PUBLISHED) or only visible inside the Plain app (DRAFT). """ status: HelpCenterArticleStatus! """When the article's status was last changed.""" statusChangedAt: DateTime! statusChangedBy: InternalActor! createdAt: DateTime! createdBy: InternalActor! updatedAt: DateTime! updatedBy: InternalActor! """The help center this article belongs to.""" helpCenter: HelpCenter! """The group this article belongs to, if any.""" articleGroup: HelpCenterArticleGroup } type HelpCenterArticleConnection { edges: [HelpCenterArticleEdge!]! pageInfo: PageInfo! } type HelpCenterArticleEdge { cursor: String! node: HelpCenterArticle! } type HelpCenterThemedImage { light: WorkspaceFile dark: WorkspaceFile } input HelpCenterThemedImageInput { light: WorkspaceFileInput! dark: WorkspaceFileInput! } input CreateHelpCenterInput { publicName: String! internalName: String! type: HelpCenterType! description: String! headCustomJs: String bodyCustomJs: String isChatEnabled: Boolean articleCopyOptions: HelpCenterArticleCopyOptionsInput isCustomerFacingAiEnabled: Boolean favicon: HelpCenterThemedImageInput logo: HelpCenterThemedImageInput color: String socialPreviewImage: WorkspaceFileInput subdomain: String! authMechanism: HelpCenterAuthMechanismInput } type HelpCenterArticleCopyOptionSettings { isPlaintextEnabled: Boolean! isMarkdownEnabled: Boolean! isChatgptEnabled: Boolean! isClaudeEnabled: Boolean! isCursorEnabled: Boolean! } input HelpCenterArticleCopyOptionSettingsInput { isPlaintextEnabled: Boolean! isMarkdownEnabled: Boolean! isChatgptEnabled: Boolean! isClaudeEnabled: Boolean! isCursorEnabled: Boolean! } input UpdateHelpCenterArticleCopyOptionSettingsInput { isPlaintextEnabled: Boolean isMarkdownEnabled: Boolean isChatgptEnabled: Boolean isClaudeEnabled: Boolean isCursorEnabled: Boolean } type HelpCenterArticleCopyOptions { isEnabled: Boolean! options: HelpCenterArticleCopyOptionSettings! } input HelpCenterArticleCopyOptionsInput { isEnabled: Boolean! options: HelpCenterArticleCopyOptionSettingsInput! } input UpdateHelpCenterArticleCopyOptionsInput { isEnabled: Boolean options: UpdateHelpCenterArticleCopyOptionSettingsInput } input HelpCenterPortalSettingsInput { isEnabled: Boolean threadVisibility: HelpCenterPortalSettingsThreadVisibilityInput formFields: [HelpCenterPortalSettingsFormFieldInput!] isAdditionalRecipientsEnabled: Boolean } input HelpCenterAccessSettingsInput { tierIds: [String!] tenantIds: [String!] companyIds: [String!] customerIds: [String!] } type HelpCenterPortalSettingsOverrideCustomerCompany { companyIds: [String!]! customerIds: [String!]! } input HelpCenterPortalSettingsOverrideCustomerCompanyInput { companyIds: [String!] customerIds: [String!] } type HelpCenterPortalSettingsOverrideCustomerTenants { tenantIds: [String!]! customerIds: [String!]! } input HelpCenterPortalSettingsOverrideCustomerTenantsInput { tenantIds: [String!] customerIds: [String!] } type HelpCenterPortalSettingsThreadVisibility { customerCompany: Boolean! overrideCustomerCompany: HelpCenterPortalSettingsOverrideCustomerCompany customerTenants: Boolean! overrideCustomerTenants: HelpCenterPortalSettingsOverrideCustomerTenants } input HelpCenterPortalSettingsThreadVisibilityInput { customerCompany: Boolean overrideCustomerCompany: HelpCenterPortalSettingsOverrideCustomerCompanyInput customerTenants: Boolean overrideCustomerTenants: HelpCenterPortalSettingsOverrideCustomerTenantsInput } enum HelpCenterPortalSettingsFormFieldType { TEXT_INPUT TEXT_AREA DROPDOWN } input HelpCenterPortalSettingsFormFieldInput { id: ID type: HelpCenterPortalSettingsFormFieldType! label: String! placeholder: String isRequired: Boolean! threadDetails: HelpCenterPortalSettingsThreadDetailsInput dropdownOptions: [HelpCenterPortalSettingsDropdownOptionInput!] } input ThreadAssigneeInput { userId: ID machineUserId: ID } input HelpCenterPortalSettingsThreadDetailsInput { labelTypeIds: [ID!] priority: Int assignees: [ThreadAssigneeInput!] threadFields: [HelpCenterPortalSettingsThreadFieldsInput!] } input HelpCenterPortalSettingsThreadFieldsInput { threadFieldSchemaId: ID! selectedStringValue: String selectedBooleanValue: Boolean } input HelpCenterPortalSettingsDropdownOptionInput { dropdownOptionId: ID label: String! threadDetails: HelpCenterPortalSettingsThreadDetailsInput } type HelpCenterPortalSettingsThreadFields { threadFieldSchema: ThreadFieldSchema! selectedStringValue: String selectedBooleanValue: Boolean } type HelpCenterPortalSettingsThreadDetails { labelTypes: [LabelType!] priority: Int assignees: [ThreadAssignee!] threadFields: [HelpCenterPortalSettingsThreadFields!] } type HelpCenterPortalSettingsTextFormField { id: ID! type: HelpCenterPortalSettingsFormFieldType! label: String! placeholder: String isRequired: Boolean! threadDetails: HelpCenterPortalSettingsThreadDetails } type HelpCenterPortalSettingsDropdownOption { dropdownOptionId: ID label: String! threadDetails: HelpCenterPortalSettingsThreadDetails } type HelpCenterPortalSettingsDropdownFormField { id: ID! type: HelpCenterPortalSettingsFormFieldType! label: String! placeholder: String isRequired: Boolean! dropdownOptions: [HelpCenterPortalSettingsDropdownOption!]! } union HelpCenterPortalSettingsFormField = HelpCenterPortalSettingsTextFormField | HelpCenterPortalSettingsDropdownFormField type HelpCenterPortalSettings { isEnabled: Boolean! threadVisibility: HelpCenterPortalSettingsThreadVisibility! formFields: [HelpCenterPortalSettingsFormField!]! isAdditionalRecipientsEnabled: Boolean! } input UpdateHelpCenterInput { helpCenterId: ID! type: HelpCenterType publicName: String internalName: String description: String headCustomJs: StringInput bodyCustomJs: StringInput isChatEnabled: Boolean articleCopyOptions: UpdateHelpCenterArticleCopyOptionsInput isCustomerFacingAiEnabled: Boolean favicon: HelpCenterThemedImageInput logo: HelpCenterThemedImageInput color: StringInput socialPreviewImage: WorkspaceFileInput agentAvatarImage: HelpCenterThemedImageInput subdomain: String portalSettings: HelpCenterPortalSettingsInput access: HelpCenterAccessSettingsInput authMechanism: HelpCenterAuthMechanismInput } input HelpCenterAuthMechanismInput { workosAuthkitAuthMechanism: HelpCenterWorkosAuthkitAuthMechanismInput workosConnectAuthMechanism: WorkosConnectAuthMechanismInput } input HelpCenterWorkosAuthkitAuthMechanismInput { ignore: Boolean } input WorkosConnectAuthMechanismInput { appClientId: String! appSecret: String! apiHost: String! } input UpsertHelpCenterArticleInput { helpCenterId: ID! helpCenterArticleId: ID helpCenterArticleGroupId: ID title: String! description: String icon: String contentHtml: String! slug: String status: HelpCenterArticleStatus } input CreateHelpCenterArticleGroupInput { helpCenterId: ID! name: String! slug: String """Parent group ID. If not provided, creates a top-level group.""" parentHelpCenterArticleGroupId: ID } input UpdateHelpCenterArticleGroupInput { helpCenterArticleGroupId: ID! name: String } type CreateHelpCenterOutput { helpCenter: HelpCenter error: MutationError } type UpdateHelpCenterOutput { helpCenter: HelpCenter error: MutationError } input DeleteHelpCenterInput { helpCenterId: ID! } type DeleteHelpCenterOutput { error: MutationError } type UpsertHelpCenterArticleOutput { helpCenterArticle: HelpCenterArticle error: MutationError } input DeleteHelpCenterArticleInput { helpCenterArticleId: ID! } type DeleteHelpCenterArticleOutput { error: MutationError } type CreateHelpCenterArticleGroupOutput { helpCenterArticleGroup: HelpCenterArticleGroup error: MutationError } type UpdateHelpCenterArticleGroupOutput { helpCenterArticleGroup: HelpCenterArticleGroup error: MutationError } input DeleteHelpCenterArticleGroupInput { helpCenterArticleGroupId: ID! } type DeleteHelpCenterArticleGroupOutput { error: MutationError } input CreateIssueTrackerIssueInput { issueTrackerType: String! title: String! description: String fields: [IssueTrackerFieldInput!]! } input IssueTrackerFieldInput { key: String! value: String! } type CreateIssueTrackerIssueOutput { error: MutationError threadLinkCandidate: ThreadLinkCandidate } type CustomerSurveyEdge { cursor: String! node: CustomerSurvey! } type CustomerSurveyConnection { edges: [CustomerSurveyEdge!]! pageInfo: PageInfo! } enum ImporterEntityType { THREADS CUSTOMERS TENANTS TENANT_FIELD_SCHEMAS USERS } enum ImporterStatus { PENDING RUNNING SUCCEEDED FAILED CANCELLED } type ImportRun { id: ID! importJobId: ID! """ The type of entity this run is responsible for importing (e.g. THREADS, CUSTOMERS, TENANTS). """ entityType: ImporterEntityType! status: ImporterStatus! """Number of records fetched from the external service during this run.""" downloadedRecords: Int """ Number of records successfully created or updated in Plain during this run. """ savedRecords: Int startedAt: DateTime completedAt: DateTime } type ImportJob { id: ID! """Overall status of this import job run.""" status: ImporterStatus! startedAt: DateTime completedAt: DateTime """ Per-entity-type runs that make up this import job, each tracking progress independently. """ importRuns: [ImportRun!]! } type ImportJobEdge { cursor: String! node: ImportJob! } type ImportJobConnection { edges: [ImportJobEdge!]! pageInfo: PageInfo! } input ImportJobsFilter { serviceIntegrationKeys: [String!] importJobDefinitionIds: [ID!] } type ImportResult { added: Int! updated: Int! skipped: Int! } input ImportTenantFieldSchemasFromServiceInput { serviceIntegrationKey: String! } type ImportTenantFieldSchemasFromServiceOutput { tenantFieldSchemas: [TenantFieldSchema!]! error: MutationError } input ImportTenantFieldSchemaInput { externalFieldId: String! type: TenantFieldType! label: String! options: [String!] isDeleted: Boolean! } input ImportTenantFieldSchemasInput { tenantFieldSchemas: [ImportTenantFieldSchemaInput!]! } type ImportTenantFieldSchemasOutput { result: ImportResult error: MutationError } input ImportTenantFieldValueInput { fieldId: String! type: TenantFieldType! value: String! } input ImportTenantInput { externalId: String! name: String! url: String tenantFields: [ImportTenantFieldValueInput!] } input ImportTenantsInput { tenants: [ImportTenantInput!]! } type ImportTenantsOutput { result: ImportResult error: MutationError } input ImportCustomerTenantInput { externalId: String! name: String! url: String } input ImportCustomerInput { externalId: String! fullName: String! shortName: String email: String createdAt: String tenants: [ImportCustomerTenantInput!]! } input ImportCustomersInput { customers: [ImportCustomerInput!]! } type ImportCustomersOutput { result: ImportResult error: MutationError } enum KnowledgeGapStatus { """The gap is active and has not yet been addressed.""" OPEN """ The gap has been resolved, typically by adding documentation that covers the topic. """ RESOLVED """The gap has been dismissed and will not be acted on.""" IGNORED } enum KnowledgeGapSignalType { ARI } type KnowledgeGap { id: ID! """ AI-generated title summarising the question or topic customers are not finding answers to. """ title: String! """ AI-generated description providing more detail about the knowledge gap. """ description: String! status: KnowledgeGapStatus! """ Timestamp of the most recent status change. Updated automatically whenever `changeKnowledgeGapStatus` transitions the gap to a new status. """ statusChangedAt: DateTime! """ Timestamp of the most recently linked signal. Monotonically increasing — it only advances forward as new signals are attached to this gap. """ lastSeenAt: DateTime! createdAt: DateTime! createdBy: Actor! updatedAt: DateTime! updatedBy: Actor! """Paginated list of signals linked to this knowledge gap.""" signals(first: Int, after: String, last: Int, before: String): KnowledgeGapSignalConnection! } union KnowledgeGapSignal = AriKnowledgeGapSignal type AriKnowledgeGapSignal { id: ID! type: KnowledgeGapSignalType! """ID of the support thread whose AI reply (ARI) generated this signal.""" threadId: ID! createdAt: DateTime! createdBy: Actor! updatedAt: DateTime! updatedBy: Actor! } type KnowledgeGapEdge { cursor: String! node: KnowledgeGap! } type KnowledgeGapConnection { edges: [KnowledgeGapEdge!]! pageInfo: PageInfo! totalCount: Int! } type KnowledgeGapSignalEdge { cursor: String! node: KnowledgeGapSignal! } type KnowledgeGapSignalConnection { edges: [KnowledgeGapSignalEdge!]! pageInfo: PageInfo! totalCount: Int! } input KnowledgeGapsFilter { statuses: [KnowledgeGapStatus!] } input KnowledgeGapsSort { field: KnowledgeGapsSortField! direction: SortDirection! } enum KnowledgeGapsSortField { """Sort by number of linked signals (uses correlated subquery COUNT).""" SIGNAL_COUNT """Sort by when the most recent signal was linked.""" LAST_SEEN_AT } input ChangeKnowledgeGapStatusInput { knowledgeGapId: ID! status: KnowledgeGapStatus! } type ChangeKnowledgeGapStatusOutput { knowledgeGap: KnowledgeGap error: MutationError }