# Set up an agent
Source: https://www.plain.com/docs/agents
Create a machine user for your agent, give it an API key, and start receiving events from Plain.
To build an agent in Plain, at a high level you have to:
Go to **Settings → Machine users & API Keys** and click **Add machine user**.
Set the type to "Custom agent" for any agent you build. Leave the default type for integrations that only call the API.
* Name is only visible to your team, for example "Billing agent"
* Public name is what customers see when the agent replies, for example "Acme Support"
* The avatar is shown to you internally and, if your agent interacts with customers, publicly as well.
Create one machine user per agent.
Click **Add API key** and grant the permissions the agent needs. Each page in this section lists the permissions for its calls. A mutation without the right permission returns an error that names the missing one.
Plain shows the key once, so you should copy it when it is created.
A machine user can hold more than one key, so you can rotate without downtime.
[Read more on GraphQL authentication](/docs/graphql/authentication)
Go to **Settings → Webhooks** and click **Add webhook target**. Point it at an HTTPS endpoint you control and subscribe to the events you need. You can update these later as you build your agent.
[Read more on working with webhooks](/docs/webhooks).
## Building your agent
With the above basic steps, you can now build your agent. Agents can do anything in Plain.
You should pick where you want to start based on the use case for your agent.
Make your agent available to your team directly within Plain to chat and assist them.
Let your agent handle threads autonomously end to end.
# Interacting in discussions
Source: https://www.plain.com/docs/agents/discussions
Let users talk to your agent in Ask Sidekick, receive their messages as webhooks, and reply through the API.
A discussion is where users talk to your agent inside Plain. Any user can click **Ask Sidekick** from any page, pick your agent, and send a message.
Plain sends you a webhook, and your agent replies through the API. Discussions are completely internal and not visible to your customers.
A discussion can be attached to a thread or be standalone. When a user starts a discussion from a thread, the webhook payload will include `discussion.threadId` to tell you which thread the discussion is linked to.
Discussions can also be started from a [workflow](/docs/product/agents/sidekick/in-workflows). In the workflow builder you can pick your agent as well.
## High-level flow
1. A user starts a discussion with your agent.
2. Plain sends [`discussion.message_created`](/docs/webhooks/discussion-message-created) to your webhook target.
3. Your agent sets the discussion status to `IN_PROGRESS`, does its work, posts a reply, and sets `IDLE`.
4. Your agent can then log its tool calls as well as ask for a user's approval. See [tool calls](/docs/agents/tool-calls) for more info.
## Permissions
For working with discussions, your machine user's API key must have the following permissions:
* `threadDiscussion:read` and `threadDiscussion:edit` to read discussions, report status, request approvals, resolve or reopen discussions
* `threadDiscussionMessage:create` and `threadDiscussionMessage:edit` to post messages to the discussion
* `thread:read` and `customer:read` if the agent needs to read the linked thread
## Webhooks
Subscribe your [webhook target](/docs/webhooks) to these events:
| Event | When | Needed |
| --------------------------------------------------------------------------------------------------- | -------------------------------------- | ---------------------------------------------- |
| [`discussion.message_created`](/docs/webhooks/discussion-message-created) | A message was posted in any discussion | Always |
| [`discussion.tool_call_approval_resolved`](/docs/webhooks/discussion-tool-call-approval-resolved) | A user approved or denied a tool call | If you ask for approvals |
| [`discussion.turn_stop_requested`](/docs/webhooks/discussion-turn-stop-requested) | A user asked your agent to stop | If you can stop mid-turn |
| [`discussion.discussion_created`](/docs/webhooks/discussion-created) | A discussion was started | Optional. The first message is enough to start |
| [`discussion.tool_call_approval_requested`](/docs/webhooks/discussion-tool-call-approval-requested) | Echo of an approval you requested | Optional |
## Decide whether to answer
`discussion.message_created` fires for every discussion in the workspace, including your own replies. Answer only when all of these are true:
| Condition | Why |
| ------------------------------------------ | -------------------------------------------------------------------- |
| `discussion.type` is `AGENT_SESSION` | Other types are Slack and email discussions between people. |
| `discussion.agent.id` is your machine user | Otherwise it is Sidekick or another agent. |
| `message.type` is `OUTBOUND` | A user's message is `OUTBOUND`. Your replies come back as `INBOUND`. |
| `discussion.status` is not `RESOLVED` | Otherwise the discussion is over. |
Load your machine user once at startup and deduplicate on `message.id`:
```ts theme={null}
import { PlainClient } from "@team-plain/graphql";
const plain = new PlainClient({ apiKey: process.env.PLAIN_API_KEY! });
const me = await plain.query.myMachineUser();
function shouldAnswer(payload: DiscussionMessageCreatedPublicEventPayload) {
return (
payload.discussion.type === "AGENT_SESSION" &&
payload.discussion.agent?.id === me.id &&
payload.message.type === "OUTBOUND" &&
payload.discussion.status !== "RESOLVED"
);
}
```
In the webhook payload, `message.markdown` is what the user wrote. `message.workspaceFiles` lists any files they attached. You must respond with a status code of `200` before your agent starts work so that Plain doesn't retry the webhook delivery.
## Replying
You can use the `sendDiscussionMessage` mutation to reply to the discussion using markdown. This also marks the discussion as having unread messages within the Plain app.
## Updating the discussion status
Set the discussion's agent status to `IN_PROGRESS` when a turn starts and `IDLE` when it ends. If the turn fails, post the error as a message first, then set `IDLE`.
Asking for a tool call approval will automatically update the discussion status to `TOOL_CALL_APPROVAL_PENDING`.
When a discussion is pending a tool call approval, you cannot change its status manually.
## Stop when asked
A user can stop your agent mid-turn. Plain sends [`discussion.turn_stop_requested`](/docs/webhooks/discussion-turn-stop-requested) with the `discussion` to stop. Cancel the model call, post what you have if useful, and set the discussion status to `IDLE`. The discussion stays open for the next message.
## Resolve the discussion
You can use `changeThreadDiscussionStatus` to resolve a discussion when the user needs nothing further, or set the status to `OPEN` to reopen it.
## Replying to customers on behalf of the user
A user in a discussion can ask your agent to message the customer. The agent can do this by calling the [`replyToThread`](/docs/graphql/messaging/reply-to-thread) mutation.
By default the reply is from the machine user. To send it as the user who asked, add them to the API key's **impersonation allow list** and pass `impersonation.asUser`. See [reply as a user](/docs/graphql/messaging/reply-to-thread#reply-as-a-user).
The allow list belongs to the API key, not the machine user. Open the API key from the machine user's page and, under **Impersonation**, add who it may reply as:
* individual team members
* everyone holding a built-in role: Owner, Admin, or Support
* everyone holding a custom role
# Example agents
Source: https://www.plain.com/docs/agents/examples
Working agents built on the Plain API that you can run locally and copy from.
Two working agents, one repository each.
Both answer users in [discussions](/docs/agents/discussions), receive webhooks on a local endpoint, and gate the one action a customer would see behind an [approval](/docs/agents/tool-calls#ask-for-approval). They differ in how much of the agent loop they hand to a framework.
| Agent | Built with | What it shows |
| ---------------------------------------------------------------------------------------------- | ------------------------------------------- | --------------------------------------------------------------- |
| [`example-eve-assistant-agent`](https://github.com/team-plain/example-eve-assistant-agent) | [Vercel eve](https://github.com/vercel/eve) | Durable sessions, one per discussion, with a ready-made harness |
| [`example-aisdk-assistant-agent`](https://github.com/team-plain/example-aisdk-assistant-agent) | [Vercel AI SDK](https://ai-sdk.dev) | Owning the model loop yourself, with no framework in between |
## The tools
Both agents give the model the same 5 tools, each a call on Plain's API:
* **`list_thread_queue`** and **`search_threads`**: find a thread when the discussion wasn't opened on one
* **`read_customer_thread`**: paginates `timelineEntries` and concatenates `llmText`, as in [read the thread](/docs/agents/threads#read-the-thread)
* **`search_knowledge`**: calls `searchKnowledgeSources`, as in [using knowledge](/docs/agents/searching-knowledge)
* **`reply_to_customer`**: reports the tool call, asks for approval, waits for the decision, then calls `replyToThread`
## Running one
Each repository stands alone, with its own lockfile, its own CI, and a README that walks through the setup: a machine user with the Custom agent type, the permissions to grant, a public URL for the webhook target, and the events to subscribe to.
From there, open a thread in Plain, click **Ask Sidekick**, pick your agent, and ask it to help.
# Working with notes
Source: https://www.plain.com/docs/agents/notes
Get a webhook when a user mentions your agent in a note on a thread, and reply with a note of your own.
Notes are internal comments on a thread. A user can mention your agent in a note to hand it a question about that thread, and your agent can answer with a note.
Notes are never shown to the customer.
To work with notes, your machine user's API key needs `note:create` permissions as well as `thread:read` and `customer:read` to read the thread.
## Get notified when you're mentioned
Subscribe your [webhook target](/docs/webhooks) to [`thread.note_mention_created`](/docs/webhooks/thread-note-mention-created).
It fires when a note mentions a machine user, including when a user edits a note to add the mention. The payload has the `thread`, the `note`, and `mentions`, the list of machine users mentioned.
Check that your agent is among them:
```ts theme={null}
if (event.payload.eventType === "thread.note_mention_created") {
const mentioned = event.payload.mentions.some(
(machineUser) => machineUser.id === process.env.AGENT_MACHINE_USER_ID,
);
if (!mentioned) return;
await runAgent(event.payload.thread, event.payload.note);
}
```
`note.markdown` holds what the user wrote. Mentions appear in it as tokens: `<@mu_…>` for a machine user and `<@u_…>` for a user. Only machine users are listed in `mentions`; strip or resolve the tokens before you pass the text to a model.
To see every note on a thread, not only the ones that mention you, subscribe to [`thread.note_created`](/docs/webhooks/thread-note-created) as well. It fires for your own notes too, so skip notes where `note.createdBy.actorType` is `machineUser` and `machineUserId` is yours.
## Reply with a note
The `createNote` mutation adds a note to the thread under the machine user's name. Send `text` as the plain version and `markdown` for formatting. To mention the user who asked, put their `<@u_…>` token in `markdown`; Plain renders it as a mention and notifies them.
```ts theme={null}
const author = event.payload.note.createdBy;
const mention = author.actorType === "user" ? `<@${author.userId}> ` : "";
const result = await plain.mutation.createNote({
input: {
customerId: event.payload.thread.customer.id,
threadId: event.payload.thread.id,
text: "The invoice failed because the card was declined on 3 September.",
markdown: `${mention}The invoice failed because the card was **declined** on 3 September.`,
},
});
if (result.error) throw new Error(result.error.message);
```
Notes have a maximum length of 10k characters. See [notes](/docs/graphql/notes) for updating and deleting.
Notes also work without a mention. An agent that triages threads can leave what it found as a note, and an agent that [hands off](/docs/agents/threads#hand-off) can use notes to say why it's handing off.
# Using knowledge
Source: https://www.plain.com/docs/agents/searching-knowledge
Search Help Center articles and indexed documents from your agent and put the results in its prompt.
Within Plain, you can add [knowledge sources](/docs/product/agents/knowledge-sources) such as your documentation or marketing website.
Your agent can then search through your knowledge sources by using the `searchKnowledgeSources` query.
There are 2 kinds of knowledge sources in Plain:
* Help Center articles published in [Plain's Help Center](/docs/product/help-center)
* Indexed documents you have added as knowledge sources, such as your docs site or your marketing website.
The API needs the `knowledgeSource:read` permission to be able to do this.
## Example query
```ts theme={null}
const results = await plain.query.searchKnowledgeSources({
searchQuery: "how do I reset my password?",
pageSize: 5,
});
for (const result of results) {
console.log(result.content);
if (result.__typename === "HelpCenterArticleSearchResult") {
const article = await result.helpCenterArticle;
console.log(article.title);
} else if (result.__typename === "IndexedDocumentSearchResult") {
const doc = await result.indexedDocument;
console.log(doc.url);
}
}
```
The return type is a union. Narrow it with `__typename`. See [union types](/docs/graphql/sdk#union-types).
The page size (`pageSize`) defaults to 10 and accepts 1–50. `searchQuery` is 1–1000 characters.
Keep the article title or document URL if you want the agent to cite its sources.
## Options
Include "`INDEXED_DOCUMENT`" if you want to include external knowledge sources added to Plain.
Include "`HELP_CENTER_ARTICLE`" if you want to include Plain-hosted Help Center articles.
A list of label type IDs. When provided, knowledge sources will be filtered to only include knowledge sources with these labels.
`labelTypeIds` without `types` searches indexed documents only. Labels don't apply to Help Center articles.
Whether to include Help Centers that are not public.
# Suggesting replies
Source: https://www.plain.com/docs/agents/suggested-replies
Draft a reply on a thread for a user to review, edit, and send, instead of replying to the customer directly.
You can suggest a reply for a user to review on a thread. The user sees it in the composer, edits it if needed, and sends it.
The customer sees nothing until then. Use suggested replies when a human needs to check a message before it goes out, or while you build trust in a new agent before letting it [reply on its own](/docs/agents/threads#reply).
The API key needs `generatedReply:create`, plus `thread:read` if the agent reads the thread first.
See [suggested replies](/docs/graphql/messaging/suggested-replies) for the full API reference.
## Multiple suggestions
A thread can have multiple suggested messages. Your suggestions will take precedence over suggestions from Plain.
# Working on threads
Source: https://www.plain.com/docs/agents/threads
Listen for thread events, check whether your agent is assigned, and read, reply to, and hand off threads through the API.
An agent working on threads subscribes to the events it cares about, decides whether to act, and uses the thread APIs to reply, update the thread, or hand it to a user.
The customer sees the agent's public name and avatar.
## Permissions
Depending on what your agent does, you will need different permissions. Basic permissions most agents need are:
* `thread:read` and `customer:read`
* `thread:reply` to reply to threads
* `thread:edit` to update the thread's status
* `thread:assign` and `thread:unassign` for handoffs
* `note:create` if it leaves [notes](/docs/agents/notes)
## Choose your events
Subscribe your [webhook target](/docs/webhooks) to the events that should wake the agent:
| Event | When |
| ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| [`thread.thread_assignment_transitioned`](/docs/webhooks/thread-assignment-transitioned) | The thread was assigned or unassigned |
| [`thread.email_received`](/docs/webhooks/thread-email-received) | An email was sent by the customer |
| [`thread.chat_received`](/docs/webhooks/thread-chat-received) | A chat message was sent by the customer |
| [`thread.slack_message_received`](/docs/webhooks/thread-slack-message-received) | A Slack message was sent by the customer |
| [`thread.thread_created`](/docs/webhooks/thread-created) | A new thread, before anyone is assigned |
| [`thread.thread_status_transitioned`](/docs/webhooks/thread-status-transitioned) | The thread moved between todo, snoozed, and done |
| [`thread.thread_labels_changed`](/docs/webhooks/thread-labels-changed) | Labels were changed |
| [`thread.thread_priority_changed`](/docs/webhooks/thread-priority-changed) | Priority was changed |
| [`thread.note_mention_created`](/docs/webhooks/thread-note-mention-created) | A user mentioned the agent in a note. See [notes](/docs/agents/notes) |
Two things to handle in every listener:
* Your own actions come back as events. Skip messages whose author is your machine user.
* The first email on a thread fires both `thread.thread_created` and `thread.email_received`. Check `isStartOfThread` on the email if you want to handle it once.
## Assignment
Most agents act only on threads assigned to them. That keeps the decision of which threads the agent handles in Plain, where the team can see and change it. Assign in the UI, or with a [workflow](/docs/product/workflows) based on the thread's channel, labels, tier, or support hours.
You can also assign threads programmatically:
```ts theme={null}
await plain.mutation.assignThread({
input: {
threadId: thread.id,
machineUserId: process.env.AGENT_MACHINE_USER_ID,
},
});
```
Assignment arrives as `thread.thread_assignment_transitioned`, with `previousThread` alongside `thread`. Every thread event also carries `thread.assignee`, so check it on message events too:
```ts theme={null}
function isAssignedToMe(thread: { assignee?: { id: string } | null }): boolean {
return thread.assignee?.id === process.env.AGENT_MACHINE_USER_ID; // Find your machine user id on its settings page
}
switch (event.payload.eventType) {
case "thread.thread_assignment_transitioned":
case "thread.email_received":
if (!isAssignedToMe(event.payload.thread)) return;
await runAgent(event.payload.thread);
break;
}
```
To hand off a thread, unassign the machine user or assign a different user.
For an agent that never replies, such as a classifier or a note-writer, you can skip assignment and filter in the handler instead. For example, act on `thread.thread_created` only when `thread.tier?.name` is `Premium`.
## Reading the thread
```ts theme={null}
const thread = await plain.query.thread({
threadId: "th_01H8H46YPB2S4MAJM382FG9423",
});
```
As with all of our GraphQL queries, you can selectively expand your query to include details you need such as `customer`, `assignee`, and `labels`. See the [GraphQL SDK](/docs/graphql/sdk).
Every timeline entry has `llmText`, Plain's rendering of that entry for a language model. Concatenate them for a prompt-ready thread:
```ts theme={null}
async function getThreadAsLlmText(threadId: string): Promise {
const thread = await plain.query.thread({ threadId });
const parts: string[] = [];
let page = await thread.timelineEntries({ first: 50 });
while (true) {
for (const entry of page.nodes) {
if (entry.llmText) parts.push(entry.llmText);
}
const next = await page.fetchNext();
if (!next) break;
page = next;
}
return parts.join("\n\n");
}
```
The `llmText` field is `null` for entries with no user or customer messages and can be skipped. You can also read `thread.customer`, [thread fields](/docs/graphql/threads/thread-fields), and the message directly from the webhook payload.
## Replying
The `replyToThread` mutation automatically replies on the right channel (Chat, Email, Slack, MS Teams, etc.) based on the messages in the thread.
```ts theme={null}
const result = await plain.mutation.replyToThread({
input: {
threadId: thread.id,
textContent: "Thanks for reaching out, let me look into this.",
markdownContent: "Thanks for reaching out, let me look into this.",
},
});
if (result.error) {
console.error(result.error.message);
}
```
`markdownContent` is rendered in Plain, chat, and modern email. See [reply to thread](/docs/graphql/messaging/reply-to-thread). `textContent` is the fallback for channels that don't support markdown and multi-part emails.
If you'd rather suggest a message for a user, you can use the [suggest a reply mutation](/docs/agents/suggested-replies) instead.
## Updating the agent status
A thread's agent status tells the team what the agent is doing with a thread, and tells Plain which threads to count in response time metrics.
| Status | When |
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `IN_PROGRESS` | The agent is working the thread. |
| `HANDLED` | The agent resolved it. Also [mark it as done](/docs/graphql/threads/status-changes#mark-thread-as-done). |
| `HANDED_OFF` | A user is needed. Also [unassign](/docs/graphql/threads/assignment#unassigning-threads), and optionally [mark as todo](/docs/graphql/threads/status-changes#mark-thread-as-todo). |
```ts theme={null}
await plain.mutation.updateThreadAgentStatus({
input: {
threadId: thread.id,
agentStatus: "IN_PROGRESS",
},
});
```
If a user replies on a thread marked `HANDLED` or `IN_PROGRESS`, Plain sets `HANDED_OFF` itself. First Response, Next Response, and Investigating metrics only count `HANDED_OFF` threads.
## Hand off
When the agent can't help, hand the thread to a person in this order:
1. Leave a [note](/docs/agents/notes) saying what it tried and why it stopped.
2. Set agent status to `HANDED_OFF`.
3. Unassign the thread, or assign it to a user with `assignThread` and a `userId`.
4. Mark the thread as todo so it shows up in the queue.
```ts theme={null}
await plain.mutation.unassignThread({
input: { threadId: thread.id },
});
await plain.mutation.markThreadAsTodo({
input: { threadId: thread.id },
});
```
## Other mutations
| Action | Mutation | Permission |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ |
| Done or todo | [`markThreadAsDone`](/docs/graphql/threads/status-changes), [`markThreadAsTodo`](/docs/graphql/threads/status-changes) | `thread:edit` |
| Labels | [`addLabels`](/docs/graphql/labels/add), `removeLabels` (takes label IDs, not label type IDs) | `label:create`, `label:delete` |
| Thread field | [`upsertThreadField`](/docs/graphql/threads/thread-fields) | `threadField:create`, `threadField:update` |
| New outbound email | [`sendNewEmail`](/docs/graphql/messaging/send-email) | `email:create` |
| Reply to a specific email | [`replyToEmail`](/docs/graphql/messaging/reply-email) | `email:create` |
| Customer event | [`createCustomerEvent`](/docs/graphql/events/create-customer-event) | `customerEvent:create` |
| Thread event | [`createThreadEvent`](/docs/graphql/events/create-thread-event) | `threadEvent:create` |
Try any of them in the [API explorer](https://app.plain.com/developer/api-explorer/).
# Tool calls
Source: https://www.plain.com/docs/agents/tool-calls
Show users which tools your agent ran, and in discussions, wait for approval before running one.
Plain shows your agent's tool calls differently depending on where it is working.
In a discussion, tool calls are interactive: you report each call, the user watches it run, and you can pause a call until the user approves it.
On a thread, you record tool calls as [thread events](/docs/graphql/events/create-thread-event) on the timeline. Those are a log for the team, not something a user can act on.
## In discussions
Your machine user's API key needs to have the `threadDiscussion:read` and `threadDiscussion:edit` permissions.
### Report a tool call
Call `upsertDiscussionToolCall` with a `toolCallId` you define.
The initial tool call should be in a status of `PENDING`. When your tool call completes, update the status to `SUCCESS` or `ERROR` depending on the outcome.
Plain shows the call in the discussion with its duration.
* `toolCallId`: yours, unique within the discussion, 1–256 characters of `[A-Za-z0-9_-]`
* `text`: required on every write, max 2000 characters. This is what the user reads.
* `error`: required on `ERROR`, max 4000 characters
* `SUCCESS` and `ERROR` are final. A later write returns `result: NOOP`.
### Asking for approval
For a call that needs a user's decision, you can call the `requestDiscussionToolCallApproval` mutation.
Plain shows a card with `text` as the heading, `justification` underneath, and **Approve** and **Deny** buttons.
The discussion's `agentStatus` will automatically be set to `TOOL_CALL_APPROVAL_PENDING`.
When requesting approval, the tool call must be in a status of `PENDING`.
Asking again for the same id returns the same approval. Several approvals can be open on one discussion at once.
Plain sends [`discussion.tool_call_approval_requested`](/docs/webhooks/discussion-tool-call-approval-requested) as an echo, then [`discussion.tool_call_approval_resolved`](/docs/webhooks/discussion-tool-call-approval-resolved) when the user makes a decision:
* **`APPROVED`**: means your tool call was approved and you can run the tool.
* **`DENIED`**: means the user rejected your tool call. Your tool will have already been updated to a status of `FAILED` and will include the user's `reviewerNote` if provided. This is a human-provided message as to why the tool call was rejected.
* If your agent stops waiting, report `ERROR` so the call doesn't stay open.
## On threads
On a thread there is no approval flow. You can record what the agent did by using [thread events](/docs/graphql/events/create-thread-event) so you can see it on a thread's timeline.
The API key needs `threadEvent:create`.
```ts theme={null}
const result = await plain.mutation.createThreadEvent({
input: {
threadId: thread.id,
title: "Searched knowledge",
components: [
{
componentText: {
text: "Query: `refund policy`. 3 results, top match: **Refunds and cancellations**.",
},
},
],
isCollapsed: true,
},
});
if (result.error) throw new Error(result.error.message);
```
`title` is the line users see on the timeline. `components` hold the detail, built from [UI components](/docs/ui-components). Set `isCollapsed` to `true` so a run with many calls doesn't crowd out the conversation. Pass an `externalId` if you want Plain to reject a duplicate tool call thread event.
If a tool on a thread needs a user's decision, ask for it somewhere a user can answer: post a [note](/docs/agents/notes) that mentions them, or [hand the thread off](/docs/agents/threads#hand-off).
# API and webhook updates
Source: https://www.plain.com/docs/changelog
Updates to Plain's API, and new webhook schema versions.
**SLA policies**
* Added `serviceLevelAgreementPolicies` and `serviceLevelAgreementPolicy` to list the SLA policies in a workspace or fetch one by ID. Both need the `serviceLevelAgreement:read` permission.
* Added `createServiceLevelAgreementPolicy`, `updateServiceLevelAgreementPolicy`, and `deleteServiceLevelAgreementPolicy`. A policy holds at most one target per `ServiceLevelAgreementType`, and each target sets `minutes`, `warnBefore`, and optional `businessHoursScheduleIds`.
* Added `updateThreadServiceLevelAgreementPolicy` to apply a policy to a thread. Pass `serviceLevelAgreementPolicyId: null` to remove it. Needs the `thread:edit` permission.
* `Thread` exposes `serviceLevelAgreementPolicy`. `ThreadsFilter` and saved view filters accept `serviceLevelAgreementPolicyIds`.
**SLAs**
* Added `FirstResolutionTimeServiceLevelAgreement` and `TotalResolutionTimeServiceLevelAgreement`. First resolution time is met when the thread is first marked Done. Total resolution time keeps counting after the thread is reopened.
* `createServiceLevelAgreement` accepts exactly one of `firstResponseTimeMinutes`, `nextResponseTimeMinutes`, `firstResolutionTimeMinutes`, or `totalResolutionTimeMinutes`. `pauseOnStatusDetailTypes` sets which thread status details pause a resolution-time clock. It defaults to waiting for the customer. Pass an empty list to keep the clock running.
* `updateServiceLevelAgreement` accepts the matching minute inputs and `pauseOnStatusDetailTypes`.
* `ServiceLevelAgreementType` includes `FIRST_RESOLUTION_TIME` and `TOTAL_RESOLUTION_TIME`.
* `ServiceLevelAgreementStatusSummary` exposes `firstResolutionTime` and `totalResolutionTime`.
* `ServiceLevelAgreementStatusDetailPending` and `ServiceLevelAgreementStatusDetailImminentBreach` expose `pausedAt` and `remainingMinutesAtPause` when a resolution-time clock is paused.
**Slack**
* `createThreadFromSlackMessage` accepts `title`, `description`, `labelTypeIds`, `tenantIdentifier`, `priority`, `externalId`, and `threadFields`. They apply only when a new thread is created. An already-ingested message ignores them.
* `changeThreadPriority` accepts `onlyIfDefault`. When true, the mutation changes priority only if it is still Normal and has not been set since thread creation. Otherwise it returns the unchanged thread.
**Workflows**
* WAIT steps accept `businessHoursScheduleIds`. When set, `duration` counts only while one of the selected schedules is open.
**Webhook targets**
* `createWebhookTarget` and `updateWebhookTarget` accept `headers`. `WebhookTarget.headers` returns the configured names. Values are never returned.
**Broadcasts**
* `broadcasts` and related connections expose `totalCount`.
**Billing**
* `sidekickCreditUsageByDay` and `sessionCount` include work done by the workspace's agents, not only Sidekick sessions. Requires `billing:read`.
* `BillingTopupCreditBalance` exposes `lifetimeGranted` and `lifetimeUsed` for top-up credits.
**Email**
* Added `isPlainManagedDomain` on `WorkspaceEmailDomainSettings`, so you can tell whether Plain manages the domain.
* Added `isFromPlainManagedDomain` on `Thread`, which is true when any of the thread's support email addresses is on a Plain-managed domain.
[SLA policies](/docs/product/platform/slas) · [SLA policies API](/docs/graphql/sla-policies) · [Tier SLAs](/docs/product/platform/slas/tier-slas) · [Service level agreements](/docs/graphql/tiers/service-level-agreements) · [Create a thread from a Slack message](/docs/product/channels/create-from-slack)
**Business hours schedules**
* Added `businessHoursSchedule` and `businessHoursSchedules`, which read the [named schedules](/docs/product/platform/business-hours) in your workspace. Both need the `businessHours:read` permission.
* Added `createBusinessHoursSchedule`, `updateBusinessHoursSchedule`, and `deleteBusinessHoursSchedule`. Overlapping and adjacent slots are merged when you write them.
* [SLA](/docs/product/platform/slas) types expose `businessHoursSchedules`, so you can read which schedules an SLA is tracked against. An empty list means it is tracked 24/7.
* `businessHoursScheduleIds` on `ServiceLevelAgreementInput` and `UpdateServiceLevelAgreementInput` now takes IDs you can create, so you can point an SLA at a specific schedule.
**Workflows**
* Added `lastTriggeredAt` on `Workflow`, and `LAST_TRIGGERED_AT` on `WorkflowsSortField`.
* `Workflow.order` is the 1-based dispatch priority among published event workflows with a start step. `moveWorkflow` updates it.
**Broadcasts**
* Added `imageUrl` on `BroadcastReactionCount`, so you can render a custom emoji.
**Discussions**
* `DiscussionsFilter.isWorkflowTask` separates the Sidekick tasks generated from `ask_sidekick` workflow steps from the discussions that people join. Pass `false` for a human-facing list.
**Changed and removed**
Update your integration if it uses any of these fields or operations:
* Replace `businessHoursSlots` with `businessHoursSchedules`.
* Replace `syncBusinessHoursSlots` with `createBusinessHoursSchedule` or `updateBusinessHoursSchedule`. It errors when the workspace has more than one schedule.
* Replace `useBusinessHoursOnly` with `businessHoursSchedules` when you read an SLA, and with `businessHoursScheduleIds` when you write one.
**Slack**
* Added `createThreadFromSlackMessage`, so you can create a thread from a top-level message in a connected customer Slack channel. It is the trigger for [API-only ingestion](/docs/product/channels/slack-ingestion-modes#api-only), and it works in every other ingestion mode too.
* Added `API_ONLY` to `SlackIngestionMode`.
**AI feedback**
* Added `aiFeedback`, so you can list the feedback your team has left on Ari replies and other Plain AI features. Filter by `features`, `sentiments`, and `createdAt`. Requires `aiFeatureFeedback:read`.
**Other**
* `MachineUser.isAssignableToThreads` reports whether custom-agent pricing allows assigning that machine user to a thread.
[Create a thread from a Slack message](/docs/product/channels/create-from-slack) · [Ingestion modes](/docs/product/channels/slack-ingestion-modes) · [AI feedback](/docs/graphql/ai-feedback)
**Broadcasts**
* Added `broadcastAudience`, `broadcastAudiences`, `createBroadcastAudience`, `updateBroadcastAudience`, and `deleteBroadcastAudience`, so you can save and reuse [broadcast audiences](/docs/product/broadcasts/audiences).
* Added `broadcastSendTargetRecipients`, so you can see who a send target's `scope`, `filters`, `recipients`, and `excludeRecipients` resolve to before you save or send.
* Added `sendTestBroadcast`, so you can post a draft to up to 10 named Slack channels without changing the broadcast's status.
[Broadcasts](/docs/product/broadcasts#using-the-api) · [Broadcast audiences](/docs/product/broadcasts/audiences)
**Reply as a user**
* `replyToThread` accepts `impersonation.asUser`, so an API key can reply as a team member. `impersonation` takes exactly one of `asCustomer` or `asUser`.
* `createApiKey` and `updateApiKey` accept `impersonationAllowList`, and `ApiKey.impersonationAllowList` returns it. It names the users, roles and custom roles a key may reply as.
* `TimelineEntry.createdBy` returns whose credentials wrote an entry, which is the machine user for a reply sent as a team member, while `actor` stays the team member. It is null on entries created before this was recorded.
**Other**
* `Workspace.isSSOEnabled` and `Workspace.isDirectoryEnabled` report an active SSO connection and a configured directory, and `User.isManagedByDirectory` marks the users the directory manages.
* `WorkflowsFilter.createdByUserIds` filters workflows by creator, and `sortBy` accepts `NAME`, `CREATED_AT` and `PUBLISHED_AT`.
**Webhooks**
* Added [`discussion.turn_stop_requested`](/docs/webhooks/discussion-turn-stop-requested), sent when someone requests that an agent stop its current turn. The payload contains `discussion`, `requestedBy`, and `requestedAt`.
[Reply to threads](/docs/graphql/messaging/reply-to-thread#reply-as-a-user) · [Machine users](/docs/agents/machine-users#reply-as-a-team-member) · [Webhook versions](/docs/webhooks/versions) · [JSON schema](https://core-api.uk.plain.com/webhooks/schema/2026-09-14.json)
**Broadcasts**
* Broadcasts expose sends, deliveries, reactions, reply threads, `SLACK_BLOCK_KIT` content, and link unfurling.
* `scheduleBroadcast.scheduledAt` is optional, so you can return a scheduled broadcast to draft.
**Other**
* `Task.sourceLinks` lists linked knowledge gaps. `knowledgeGaps` accepts `filters`, including status.
* `moveWorkflow` places a workflow after another, or first when `afterWorkflowId` is null. Read where it sits from lexicographic `position`, and sort `workflows` with `sortBy`.
* `EmailEntry.inlineAttachments` and `ImportThreadMessageInput.emailMessageId` are available.
* `Thread.broadcast` and `ThreadsFilter.broadcastIds` relate reply threads to a broadcast.
* `changeThreadDiscussionStatus` resolves and reopens a [discussion](/docs/graphql/discussions).
**Changed and removed**
Update your integration if it uses any of these fields or operations:
* Replace `BroadcastSendTarget.channels` and `BroadcastSendTargetInput.channels` with `scope`, `filters`, `recipients`, and `excludeRecipients`.
* Replace `ThreadDiscussionAgentStatus.NEEDS_INPUT` with `TOOL_CALL_APPROVAL_PENDING`.
* Replace `isSuccess` on `ThreadDiscussionToolCallEntryPayload` with `status` (`DiscussionToolCallStatus`). `service` and `op` are optional.
* Remove `DiscussionsFilter.hasAgentSession`. Filter with `agentTypes` and `agentMachineUserIds`.
* Replace `order` on workflow inputs with `moveWorkflow`. Setting `order` no longer affects ordering, and `Workflow.order` is deprecated in favor of `position`.
* Replace `markThreadDiscussionAsResolved` with `changeThreadDiscussionStatus`, which moves a discussion between `OPEN` and `RESOLVED`.
[Broadcasts](/docs/product/broadcasts#using-the-api) · [Discussions](/docs/graphql/discussions) · [Tasks](/docs/graphql/tasks)
* Added `task.task_created`, `task.task_updated`, `task.task_status_transitioned`, and `task.task_deleted`. A status change fires both `task.task_updated` and `task.task_status_transitioned`.
* A task raised by Plain's AI agent for a knowledge gap has a `sourceLinks` entry with `sourceType: "knowledge_gap"`.
[Webhook versions](/docs/webhooks/versions) · [JSON schema](https://core-api.uk.plain.com/webhooks/schema/2026-09-11.json)
* Added `discussion.tool_call_approval_requested` and `discussion.tool_call_approval_resolved` for agent tool-call approval changes.
* Added `agentStatus` to discussions in `discussion.message_created`.
[Webhook versions](/docs/webhooks/versions) · [JSON schema](https://core-api.uk.plain.com/webhooks/schema/2026-09-06.json)
**GraphQL schema**
* Added `createBroadcast`, `updateBroadcast`, `deleteBroadcast`, and `scheduleBroadcast` mutations, plus `broadcast`, `broadcasts`, and `searchBroadcasts` queries, so you can manage [broadcasts](/docs/product/broadcasts) programmatically.
**Guides**
* The Broadcasts guide now documents [using the API](/docs/product/broadcasts#using-the-api), including the permissions your [API key](/docs/graphql/authentication) needs to create, update, and send broadcasts.
[Broadcasts](/docs/product/broadcasts#using-the-api) · [Authentication](/docs/graphql/authentication)
Added `workspaceFiles` to messages in `discussion.message_created`.
[Webhook versions](/docs/webhooks/versions) · [JSON schema](https://core-api.uk.plain.com/webhooks/schema/2026-09-02.json)
Added `discussion.discussion_created`, sent when a [discussion](/docs/graphql/discussions) is started on a thread. Existing event payloads did not change.
[Webhook versions](/docs/webhooks/versions) · [JSON schema](https://core-api.uk.plain.com/webhooks/schema/2026-08-31.json)
Added `thread.thread_locked`, sent when a thread is [locked](/docs/product/platform/threads/locking-threads). Existing event payloads did not change.
[Webhook versions](/docs/webhooks/versions) · [JSON schema](https://core-api.uk.plain.com/webhooks/schema/2026-08-25.json)
Discussion `status` is now `OPEN` or `RESOLVED`. Agent activity moved to `agentStatus` (`UNKNOWN`, `IDLE`, and related values). If you filter or switch on the old discussion status values (`IDLE`, `IN_PROGRESS`, `APPROVAL_REQUESTED`), update those clients.
Added the `discussion.message_created` webhook event, sent when a message is posted in a discussion. Existing event payloads did not change.
[Discussions](/docs/graphql/discussions) · [Webhook versions](/docs/webhooks/versions) · [JSON schema](https://core-api.uk.plain.com/webhooks/schema/2026-08-19.json)
Added `isCustomAgent` on `MachineUser`. `true` when the machine user is an agent the workspace built itself, rather than an ordinary API integration.
[Machine users](/docs/agents/machine-users)
* Added `thread.note_mention_created` when a note mentions a machine user (for example an [agent](/docs/agents)). Payload includes `thread`, `note`, and `mentions` (an array of `machineUser`).
* Added `attio` and `email_domain` to `tenant.source` on `thread.tenant_updated`.
[Webhook versions](/docs/webhooks/versions) · [JSON schema](https://core-api.uk.plain.com/webhooks/schema/2026-07-14.json)
Added `createTenant`. It fails if a tenant already exists with the given `externalId`. Use [`upsertTenant`](/docs/graphql/tenants/upsert) when you want create-or-update. Requires `tenant:create`.
Added `attio` and `email_domain` to `tenant.source` on `thread.tenant_updated`.
[Webhook versions](/docs/webhooks/versions) · [JSON schema](https://core-api.uk.plain.com/webhooks/schema/2026-07-07.json)
* Added `threadsByExternalId` to list every thread with a given `externalId`, across customers. `threadByExternalId` still returns a single thread for one customer.
* Added `knowledgeSourceCitationsByThread` for the knowledge sources cited by AI replies on a thread.
[Fetch threads](/docs/graphql/threads/get) · [Searching knowledge](/docs/agents/searching-knowledge)
Added `updateThreadExternalId`. Pass `externalId: null` to clear it. Requires `thread:edit`.
[Updating threads](/docs/graphql/threads/update)
* Added `WAITING_INDEFINITELY` to `statusDetail.type` on `thread` (snoozed with no duration). The same value is available on the thread type in the API.
* `componentUser.user` may be a `machineUser` as well as a `user` when a [UI component](/docs/ui-components) references a machine user.
[Webhook versions](/docs/webhooks/versions) · [JSON schema](https://core-api.uk.plain.com/webhooks/schema/2026-06-15.json)
Added `importThread` and `importThreadMessages` for backfilling historical threads without firing SLAs or autoresponders. Each `externalId` is idempotent (`NOOP` on replay). Requires `thread:import`.
[Importing threads](/docs/graphql/threads/import)
Added `dateTime` and `user` [UI component](/docs/ui-components) types to custom timeline entries.
[Webhook versions](/docs/webhooks/versions) · [JSON schema](https://core-api.uk.plain.com/webhooks/schema/2026-05-06.json)
Added a paginated `discussions` query with filters for user and discussion type.
[Fetch discussions](/docs/graphql/discussions/get)
* Added `thread.thread_tenant_updated`, sent when a thread's tenant changes.
* Added `reactionChange` on `thread.slack_message_updated`.
[Webhook versions](/docs/webhooks/versions) · [JSON schema](https://core-api.uk.plain.com/webhooks/schema/2026-04-21.json)
Added `webhookDeliveryAttempts` so you can inspect delivery attempts, including response status and body, without leaving the API.
[Delivery attempts](/docs/graphql/webhook-targets/delivery-attempts)
Added `createDiscussion`, `sendDiscussionMessage`, and the single `discussion` query, so you can open a side-conversation on a thread and post into it programmatically.
[Create a discussion](/docs/graphql/discussions/create) · [Send a message](/docs/graphql/discussions/send-message)
Added `importCustomers`, `importTenants`, and `importTenantFieldSchemas` for bulk backfills from another system. Each is idempotent on `externalId` and returns added, updated, and skipped counts.
Added the `workflowButton` [UI component](/docs/ui-components) type to custom timeline entries.
[Webhook versions](/docs/webhooks/versions) · [JSON schema](https://core-api.uk.plain.com/webhooks/schema/2026-03-13.json)
Added `changeType` on `thread.slack_message_updated`.
[Webhook versions](/docs/webhooks/versions) · [JSON schema](https://core-api.uk.plain.com/webhooks/schema/2026-02-27.json)
* Added `dateValue` on `threadField`.
* Added `DATE` and `CURRENCY` to `threadField.type`.
[Webhook versions](/docs/webhooks/versions) · [JSON schema](https://core-api.uk.plain.com/webhooks/schema/2026-02-13.json)
Added `updateNote` for editing an existing note on a thread.
Added `numberValue` on `threadField` and `NUMBER` to `threadField.type`.
[Notes](/docs/graphql/notes) · [Webhook versions](/docs/webhooks/versions) · [JSON schema](https://core-api.uk.plain.com/webhooks/schema/2026-02-11.json)
Added `lockThread` to freeze a thread so non-admin users cannot reply or make changes.
Added `updateThreadAgentStatus` to set a thread's AI agent status to `IN_PROGRESS`, `HANDED_OFF`, or `HANDLED`.
[Building agents](/docs/agents)
Tasks are now available over the API: `createTask`, `updateTask`, `deleteTask`, plus `task`, `taskByRef`, and a filterable `tasks` query.
[Tasks](/docs/graphql/tasks)
Added `labelTypeByExternalId`, so you can look up a label type by the ID it has in your own system.
[Label types](/docs/graphql/labels/label-types)
Added `deleteThread` for permanently deleting a thread.
[Deleting threads](/docs/graphql/threads/delete)
Added `searchKnowledgeSources` to search the knowledge your agents draw on.
[Searching knowledge](/docs/agents/searching-knowledge)
Added `upsertTenantField`, `deleteTenantField`, and `deleteTenantFieldSchema`, completing tenant field management over the API.
[Tenant fields](/docs/graphql/tenants/tenant-fields)
Added the `tenantFieldSchemas` query and `upsertTenantFieldSchema` mutation, including field visibility.
[Tenant fields](/docs/graphql/tenants/tenant-fields)
Added `deleteTenant`. Deleting a tenant unlinks it from all customers and removes its fields.
[Deleting tenants](/docs/graphql/tenants/delete)
Added the `knowledgeSources` query and `deleteKnowledgeSource` mutation.
[Searching knowledge](/docs/agents/searching-knowledge)
* Added `additionalAssignees` on `thread`.
* Added `externalId` and `isExcludedFromAi` on `labelType`.
[Webhook versions](/docs/webhooks/versions) · [JSON schema](https://core-api.uk.plain.com/webhooks/schema/2025-08-06.json)
# Custom channels
Source: https://www.plain.com/docs/custom-channels
Bring messages from any channel Plain does not support yet into Plain as threads.
Plains API first infrastructure means that you never need to wait for an official integration to bring all of your customer channels and communications into your Support platform.
Until we launch an official integration for a channel that you need. You can start bringing your customer messages in using custom middleware that accepts webhooks and makes POST requests.
## Creating customers
For every message to reach Plain, your middleware needs to create a customer when none exists, and match the existing one when it does.
Plains customer object requires an email address.
* Find out what identifiers are passed from your custom channel message received events
* Map relevant identifiers to Plain
* [Upserting Customers](/docs/graphql/customers/upsert)
* Make sure that the middleware tries to match existing customers first, and creates them as a fallback instead of by default.
## Sending messages
1. Messages created in Plain trigger a webhook
2. Your middleware accepts that webhook
3. Parse the required information out of it (message body, customerId, etc.)
4. Then make a request to send that message into your new channel of choice
## Receiving messages
Accepting messages from your custom channel and writing them into Plain works the exact same way, but in reverse.
* You'll need to accept messages from your custom channel, which is also done via webhooks (for example, with Whatsapp)
* Then your middleware will need to parse that message and use the Plain GraphQL API to create a new thread or create a new message in an existing thread.
* [Creating a new Thread](/docs/graphql/threads/create#graphql)
* [Reply to existing Thread](/docs/graphql/messaging/reply-to-thread#graphql)
## Matching Plain message authors to your custom channel
* In order to match up the author / person info from Plain → your new channel, we recommend querying Plain on startup of your middleware for all of your Plain workspace users and save out their `userId` and `name` and along with else you might need.
* GraphQL `query users`
* This is necessary because once you receive our webhook when a message in Plain has been created, [threads.chat\_sent](/docs/webhooks/thread-chat-sent), there will be a field like `payload.chat.createdBy.userId` containing the userId of whoever authored the message in Plain. You will then be able to look up that userId in the list of users you grabbed earlier to get their name / email / whatever in order to use them in your next step, creating the message payload you'll send to your custom channel's API to create the message there.
## Matching messages from your custom channel to Plain threads
If your customers start conversations from your custom channel, you'll need to make sure that you're passing the custom channels customer identifier as a [Customer externalID](/docs/graphql/customers/upsert) when upserting the customer. Which will need to be done before creating the thread. This is the customer's identifier in the channel, like their WhatsApp userId.
For example, if you identify customers in a channel like Whatsapp by their phone number. You would upsert the Customer with their phone number as their ExternalID, then create the thread in Plain passing in that external ID.
## Considerations
* Custom channels will not look like 1st party channels and do not come with all the native features of first party channels like Email or Slack.
* If you don't have a way of pairing a customer with their real email address on creation, you need to add a temporary email address. Such as: [channelidentifier@temp-yourcompany.com](mailto:channelidentifier@temp-yourcompany.com). If this is the case, use sendChat for replies instead of replyToThread.
* Custom channel threads appear under the "API" channel in the "channel" selector across the app, and in filters (Custom views, Insights.etc).
* You can't set custom icons. So if you built a Whatsapp integration you wouldn't be able to set a logo, or have a Whatsapp entry listed in any channel filtering and dropdowns.
* Use a label to always tag your custom channel's threads when they're created by your Middleware. That way you can filter / sort / report on your custom channel threads via that Label.
## Examples:
### Discourse
With Plain you can connect your Discourse community and automatically keep track of threads via Plain.
This works by deploying a small node service which uses Plain and Discourse's APIs. You can either self-host this little node service or we can deploy this for you.
[**Check out the example on Github →**](https://github.com/team-plain/example-discourse-integration)
This integration can be customized depending on the structure of your Discourse community and ideal support workflow. For example you could customize the priority, labels and assignee of threads from Discourse based on their category, author and content.
### Hubspot
This is now handled by the first-party [HubSpot integration](/docs/product/integrations/hubspot). It's provided here only as an architecture example
This integration automatically creates Hubspot tickets when threads are created in Plain.
1. Receives `thread.thread_created` webhook from Plain
2. Fetches thread details to get tenant ID
3. Fetches tenant details to get HubSpot company ID (`externalId`)
4. Creates HubSpot ticket associated with the company
5. Automatically refreshes HubSpot token if expired
[**Check out the example on Github →**](https://github.com/team-plain/hubspot-push-example)
# Customer cards
Source: https://www.plain.com/docs/customer-cards
Live context straight from your own systems when helping customers.
Customer cards show information from your own systems next to a customer or thread in Plain, so your team has the context it needs without leaving the queue.
Customer cards are configured on Plain (see [how to create one](/docs/customer-cards/create-a-customer-card)) and requested by Plain from your APIs.
## High-level flow
For a more detailed description of the protocol, check out the [full
spec](/docs/customer-cards/protocol).
1. A thread is viewed in Plain.
2. Plain fires a POST request to your API with:
* The thread customer's `email`, `id` and, if set, `externalId`
* The thread's `id` and, if set, `externalId`.
* If the thread has a tenant, the tenant `id` and, if set, `externalId`.
* The configured customer card `key`s
3. Your API responds with the JSON for each card
4. Cards are shown to the user in Plain.
Based on your customer card settings, Plain sends a request to your API like the below example:
Your API should then reply with a list of cards matching the requested keys where each card contains the components you want to display:
## UI components
To define what each customer card should look like, you use the Plain UI components. All the components are documented in the [Plain UI Components](/docs/ui-components/) section.
You can find example customer cards and an example API you can check out [team-plain/example-customer-cards](https://github.com/team-plain/example-customer-cards). You can try these in your own workspace.
## Example cards
To demonstrate what you can build with customer cards we've built some examples you can view and which are open source.
[**Customer cards Examples →**](https://github.com/team-plain/example-customer-cards)
## Playground
The UI components playground lets you build and preview the component JSON needed to create a customer card. Use this to prototype a customer card before starting to build your integration.
[**Playground →**](https://app.plain.com/developer/ui-components-playground/)
# Create a customer card
Source: https://www.plain.com/docs/customer-cards/create-a-customer-card
Define the details of the customer card.
To create a customer card go to **Settings** → **Customer cards** and enter the following details:
* **Title**: this will be displayed as the title of the card so even if the card fails to load users know which card is
errored.
* **Key**: the link between this config and your API. A key can only contain alphanumeric, hyphen, and underscore characters (regex: `[a-zA-Z0-9_-]+`)
* **Default time to live (seconds)**: by default how long Plain should cache customer cards. The minimum is 15 seconds, maximum is 1 year in seconds (31536000 seconds).
* **URL**: the URL of your API endpoint that will be built to return customer cards. It must start with `https://`.
* **Headers (optional)**: the headers Plain should pass along when making the request. While this is optional it is
**highly recommended** to add authorization headers or other tokens that authenticate the request as your API may be
returning customer data.
There are a few example customer cards you can configure and see how they look in your application. All example cards live in the open-source repository:
[team-plain/example-customer-cards](https://github.com/team-plain/example-customer-cards)
Here is one you can try right now:
* Title: e.g. "Usage"
* Key: `usage`
* Default time to live: `120`
* URL: [https://example-customer-cards.plain.com/](https://example-customer-cards.plain.com/)
# Examples
Source: https://www.plain.com/docs/customer-cards/examples
# Playground
Source: https://www.plain.com/docs/customer-cards/playground
# Protocol
Source: https://www.plain.com/docs/customer-cards/protocol
Learn how we request customer cards from your API and how to respond to these requests.
Plain requests customer cards from an HTTP endpoint you host. This page specifies the request Plain sends, the response it expects, and how caching and errors are handled.
This page is intended for a technical audience that will be implementing a customer card API.
Check out the [customer cards](/docs/customer-cards) page for an overview of customer cards.
Customer cards are not proactively loaded. Plain pulls them on demand, when a user opens the customer or thread.
This means that if your APIs are slow then users of the Support App will see a loading spinner over the card.
The protocol is as follows:
1. When a user in Plain opens up a customer's page the cards are loaded.
2. Plain's backend figures out which cards can be returned from the cache and which cards need to be loaded. On the first
load of the customer this would be all cards.
3. It calculates how many requests it needs to make (see [request deduplication](#request-deduplication) for
details).
4. Your APIs are then called with the customer's details, so you can look up the customer's data in your systems
(see [request](#request) section for details).
5. Your APIs then return customer cards that consist of [Plain UI components](/docs/ui-components)
(see [response](#response) section for details).
6. The cards are cached based on either an explicit TTL value in the response or the TTL in the card settings (see [caching](#caching)).
7. Cards are shown to the user in Plain.
8. Users can manually reload the card at any time in which case only that one card will be requested from your API.
A **few limits** to be aware of:
* Your API must **respond within 15 seconds**, or it will time out. See [retry strategy](#retry-strategy) for details on how timed-out requests are retried.
* You can configure a **maximum of 25 customer cards per workspace**.
* **Card keys must be unique within a workspace**. A key can only contain **alphanumeric**, **hyphen**, and **underscore** characters (regex: `[a-zA-Z0-9_-]+`).
## Request
Plain makes the following request to your backend:
* **Method**: `POST`
* **URL:** the URL you configured on customer cards settings page.
* **Headers:**
* All the headers you provided on customer cards settings page. This should include authentication headers.
* `Content-Type`: `application/json`
* `Accept`: `application/json`
* `Plain-Workspace-Id`: the ID of the workspace the customer is in. This is useful for logging or request routing.
* `User-Agent`: `Plain/1.0 (plain.com; help@plain.com)`
* `Plain-Request-Signature`: `XXX` (see [request signing](/docs/request-signing) for details)
* **Body:**
* `cardKeys`: an array of card keys being requested
* `customer`: an object with the customer's core details
* `id`: the id of the customer in Plain
* `email`: the email of the customer
* [`externalId`](/docs/graphql/customers/upsert) (optional): string if the customer has an `externalId`, otherwise it is `null`.
* `thread` (optional): an object with the thread's details, if this customer card is being requested in the context of a thread
* `id`: the id of the thread in Plain
* `externalId` (optional): string if the thread has an `externalId`, otherwise it is `null`.
Example request body:
### Request deduplication
If you configure multiple customer cards that have the same API details then Plain batches them and makes only one request.
The request deduplication logic for customer card configs is:
* The following config properties are ignored: Title, Card key, Default TTL
* **API URL:** Leading and trailing whitespaces are trimmed and then compared. **This is case sensitive**.
* For example, these URLs would be considered **different**:
* `https://api.example.com/cards`
* `https://api.example.com/cards/`
* `https://api.example.com/Cards`
* **API Headers:** Order of headers does not matter
* **Header name:** Leading and trailing whitespaces are trimmed and then compared. **This is case insensitive**.
* For example, these header names be considered **the same**:
* `Authorization`
* `AUTHORIZATION`
* ` authorization `
* **Header value:** No processing done, compared as is (be careful with any extra whitespace characters)
* For example, these header values would be considered **as different**:
* `Bearer my-token`
* `bearer my-token`
* ` bearer my-token `
## Response
For each key requested a corresponding card **MUST** be returned in the response, otherwise an integration error will be returned for that card.
Any extra cards in the response will be ignored.
Your API must respond with a **`200` status code** or the response body won't be processed and will be treated as an error.
The response body must be a JSON object with:
* `cards`: an array of cards. Every `cardKey` requested should have a corresponding `key` returned. Any extra returned
cards will be ignored.
* `key`: the requested key
* `timeToLiveSeconds` (optional, nullable): can either be omitted or `null`. If provided it will override the default time to live value. This allows you to control caching on a case-by-case basis.
* `components` (nullable): `null` to indicate that the card has no data or an array of [Plain UI Components](/docs/ui-components/).
Example response body for a card cached for 1 hour:
Example response body for a card that has no data and should not be displayed and TTL omitted:
## Caching
We cache the responses we get from your APIs. This cache is controlled via two properties:
1. A time to live value (in seconds) in the customer card's settings. This can be changed under **Settings** → **Customer cards**. Any changes here will only apply to newly loaded customer cards.
2. An explicit time to live value (in seconds) in your API response with the key `timeToLiveSeconds`. This overrides the value from settings and allows your API to dynamically set the TTL using custom logic.
Any card that is past its expiry time is deleted within a few minutes, and no later than 48 hours after expiry.
## Retry strategy
Errors are classified into two categories:
1. **Retriable errors**: these are transient issues where retrying once is appropriate
2. **Integration errors**: these are programming or configuration errors. These errors won't be retried and cached for 5 minutes.
## Security
Plain supports [request signing](/docs/request-signing) and [mTLS](/docs/mtls) to verify that the request was made by Plain and not a third party.
### Retriable errors
The following errors are **retried once** after a **1-second delay**:
* HTTP `5xx` response status code
* HTTP `429` Too Many Requests response status code
* The request times out after 15 seconds.
* Plain fails to perform the request for some reason
Retriable errors are not cached, therefore if the cards are requested again via the Support App they will be re-requested.
### Integration errors
The following errors are **not retried**:
* All HTTP 4xx response status codes except for HTTP `429` Too Many Requests response status code
* A card key is missing in the response. For example, if `subscription-details` is requested but the `cards` array in the response doesn't have an element with the key `subscription-details`.
* The response body does not match the expected schema documented in [response](#response).
Integration errors are cached for 5 minutes and indicate a programming or configuration error.
Users can manually refresh a card in the UI, in which case the card will be requested again.
# acceptSuggestedLabelTypes
Source: https://www.plain.com/docs/graphql-reference/mutations/acceptSuggestedLabelTypes
Accepts one or more pending suggested label types, creating a real label type for each and marking the suggestions as ACCEPTED. All ids must currently be PENDING. Fails the whole batch with error code `label_type_name_already_exists` if a suggestion's name matches an existing label type or another suggestion in the same batch. Requires the `labelType:create` and `labelType:read` permissions.
# acceptWorkspaceInvite
Source: https://www.plain.com/docs/graphql-reference/mutations/acceptWorkspaceInvite
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.
# addAdditionalAssignees
Source: https://www.plain.com/docs/graphql-reference/mutations/addAdditionalAssignees
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.
# addCustomerToCustomerGroups
Source: https://www.plain.com/docs/graphql-reference/mutations/addCustomerToCustomerGroups
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.
# addCustomerToTenants
Source: https://www.plain.com/docs/graphql-reference/mutations/addCustomerToTenants
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.
# addGeneratedReply
Source: https://www.plain.com/docs/graphql-reference/mutations/addGeneratedReply
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 an inbound message on the thread, from the customer or from a machine user. A message the workspace sent, including one your own machine user sent, is not a valid target. The `markdown` field is capped at 5,000 characters. Requires the `generatedReply:create` permission.
# addLabels
Source: https://www.plain.com/docs/graphql-reference/mutations/addLabels
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.
# addLabelsToUser
Source: https://www.plain.com/docs/graphql-reference/mutations/addLabelsToUser
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.
# addMembersToTier
Source: https://www.plain.com/docs/graphql-reference/mutations/addMembersToTier
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.
# addUserToActiveBillingRota
Source: https://www.plain.com/docs/graphql-reference/mutations/addUserToActiveBillingRota
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.
# addWorkspaceAlternateSupportEmailAddress
Source: https://www.plain.com/docs/graphql-reference/mutations/addWorkspaceAlternateSupportEmailAddress
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.
# archiveLabelType
Source: https://www.plain.com/docs/graphql-reference/mutations/archiveLabelType
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.
# assignRolesToUser
Source: https://www.plain.com/docs/graphql-reference/mutations/assignRolesToUser
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.
# assignThread
Source: https://www.plain.com/docs/graphql-reference/mutations/assignThread
Assign a thread to a specific user or machine user, replacing any existing primary assignee. Requires the `thread:assign` and `thread:read` permissions.
# bulkJoinSlackChannels
Source: https://www.plain.com/docs/graphql-reference/mutations/bulkJoinSlackChannels
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.
# bulkUpdateConnectedSlackChannels
Source: https://www.plain.com/docs/graphql-reference/mutations/bulkUpdateConnectedSlackChannels
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.
# bulkUpdateSlackChannelSettings
Source: https://www.plain.com/docs/graphql-reference/mutations/bulkUpdateSlackChannelSettings
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.
# bulkUpsertThreadFields
Source: https://www.plain.com/docs/graphql-reference/mutations/bulkUpsertThreadFields
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.
# bulkUpsertWorkflowSteps
Source: https://www.plain.com/docs/graphql-reference/mutations/bulkUpsertWorkflowSteps
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.
# calculateRoleChangeCost
Source: https://www.plain.com/docs/graphql-reference/mutations/calculateRoleChangeCost
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.
# changeBillingPlan
Source: https://www.plain.com/docs/graphql-reference/mutations/changeBillingPlan
Switches the workspace to a different billing plan. Use `previewBillingPlanChange` first to show the user the cost impact before applying the change.
# changeThreadCustomer
Source: https://www.plain.com/docs/graphql-reference/mutations/changeThreadCustomer
Reassign a thread to a different customer. The original customer retains all their other threads. Requires the `thread:edit` permission.
# changeThreadDiscussionStatus
Source: https://www.plain.com/docs/graphql-reference/mutations/changeThreadDiscussionStatus
Move a discussion between OPEN and RESOLVED. Resolving records the resolution timestamp and
settles agentStatus; reopening clears the timestamp and leaves agentStatus alone, because
reopening does not mean an agent has picked the work up again. Doing nothing when the discussion
is already in that status. Callable by a person, or by the agent a custom-agent discussion is
bound to, so an agent can mirror its own thread status back into Plain.
# changeThreadPriority
Source: https://www.plain.com/docs/graphql-reference/mutations/changeThreadPriority
Set the priority of a thread. Priority is an integer from 0 (urgent) to 3 (low). Requires the `thread:edit` permission.
# changeUserStatus
Source: https://www.plain.com/docs/graphql-reference/mutations/changeUserStatus
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.
# completeServiceAuthorization
Source: https://www.plain.com/docs/graphql-reference/mutations/completeServiceAuthorization
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.
# completeSidekickMcpServerConnection
Source: https://www.plain.com/docs/graphql-reference/mutations/completeSidekickMcpServerConnection
Complete a custom MCP server connection after the admin finishes the in-browser OAuth consent: stores the Nango connection, discovers the server's tools (tools/list), persists the snapshot, and marks the server connected.
# createAiFeedback
Source: https://www.plain.com/docs/graphql-reference/mutations/createAiFeedback
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`).
# createAiToneRule
Source: https://www.plain.com/docs/graphql-reference/mutations/createAiToneRule
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.
# createApiKey
Source: https://www.plain.com/docs/graphql-reference/mutations/createApiKey
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.
# createAttachmentDownloadUrl
Source: https://www.plain.com/docs/graphql-reference/mutations/createAttachmentDownloadUrl
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.
# createAttachmentUploadUrl
Source: https://www.plain.com/docs/graphql-reference/mutations/createAttachmentUploadUrl
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.
# createAutoresponder
Source: https://www.plain.com/docs/graphql-reference/mutations/createAutoresponder
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.
# createBroadcast
Source: https://www.plain.com/docs/graphql-reference/mutations/createBroadcast
Creates a new broadcast in the workspace. The `content` is encoded as declared by
`contentFormat` and authored against the rendering rules of the given `type`.
Requires the `broadcast:create` permission.
# createBroadcastAudience
Source: https://www.plain.com/docs/graphql-reference/mutations/createBroadcastAudience
Creates a reusable broadcast audience. `filters` is validated against `type`, so a filter naming a
dimension that type cannot resolve is rejected here rather than resolving to nobody at send time.
Requires the `broadcastAudience:create` permission.
# createBusinessHoursSchedule
Source: https://www.plain.com/docs/graphql-reference/mutations/createBusinessHoursSchedule
Create a new named business hours schedule, optionally with its opening slots. Overlapping and adjacent slots are merged on write. Requires the `businessHours:create` permission.
# createChatApp
Source: https://www.plain.com/docs/graphql-reference/mutations/createChatApp
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.
# createChatAppSecret
Source: https://www.plain.com/docs/graphql-reference/mutations/createChatAppSecret
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.
# createCustomRole
Source: https://www.plain.com/docs/graphql-reference/mutations/createCustomRole
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.
# createCustomerCardConfig
Source: https://www.plain.com/docs/graphql-reference/mutations/createCustomerCardConfig
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.
# createCustomerEvent
Source: https://www.plain.com/docs/graphql-reference/mutations/createCustomerEvent
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.
# createCustomerGroup
Source: https://www.plain.com/docs/graphql-reference/mutations/createCustomerGroup
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.
# createCustomerSurvey
Source: https://www.plain.com/docs/graphql-reference/mutations/createCustomerSurvey
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.
# createDemoChannel
Source: https://www.plain.com/docs/graphql-reference/mutations/createDemoChannel
Creates (or reuses) a demo channel on the given integration's support channel (e.g. a demo Slack channel), invites the installing user, and seeds a demo customer thread so the integration can be tried out before going live.
# createDiscussion
Source: https://www.plain.com/docs/graphql-reference/mutations/createDiscussion
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.
# createEmailPreviewUrl
Source: https://www.plain.com/docs/graphql-reference/mutations/createEmailPreviewUrl
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.
# createEscalationPath
Source: https://www.plain.com/docs/graphql-reference/mutations/createEscalationPath
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.
# createGithubUserAuthIntegration
Source: https://www.plain.com/docs/graphql-reference/mutations/createGithubUserAuthIntegration
Connects the currently authenticated user's Plain account to their GitHub account using a Nango OAuth session. Requires the 'githubUserAuthIntegration:create' permission.
# createHelpCenter
Source: https://www.plain.com/docs/graphql-reference/mutations/createHelpCenter
Create a new help center with a subdomain, branding, and access settings. Requires the helpCenter:edit permission.
# createHelpCenterArticleGroup
Source: https://www.plain.com/docs/graphql-reference/mutations/createHelpCenterArticleGroup
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.
# createHyperlineBillingPortalSession
Source: https://www.plain.com/docs/graphql-reference/mutations/createHyperlineBillingPortalSession
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.
# createHyperlineComponentsAuthToken
Source: https://www.plain.com/docs/graphql-reference/mutations/createHyperlineComponentsAuthToken
Creates a short-lived authentication token for embedding Hyperline billing UI components directly in your own interface. Requires the `billing:edit` permission.
# createImportSync
Source: https://www.plain.com/docs/graphql-reference/mutations/createImportSync
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.
# createIndexedDocument
Source: https://www.plain.com/docs/graphql-reference/mutations/createIndexedDocument
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.
# createIssueTrackerIssue
Source: https://www.plain.com/docs/graphql-reference/mutations/createIssueTrackerIssue
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.
# createKnowledgeSource
Source: https://www.plain.com/docs/graphql-reference/mutations/createKnowledgeSource
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.
# createLabelType
Source: https://www.plain.com/docs/graphql-reference/mutations/createLabelType
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.
# createLinearAppIntegration
Source: https://www.plain.com/docs/graphql-reference/mutations/createLinearAppIntegration
# createMachineUser
Source: https://www.plain.com/docs/graphql-reference/mutations/createMachineUser
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.
# createMyFavoritePage
Source: https://www.plain.com/docs/graphql-reference/mutations/createMyFavoritePage
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.
# createMyLinearIntegration
Source: https://www.plain.com/docs/graphql-reference/mutations/createMyLinearIntegration
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.
# createMyMSTeamsIntegration
Source: https://www.plain.com/docs/graphql-reference/mutations/createMyMSTeamsIntegration
Connects the current user's personal Microsoft Teams account to Plain using the OAuth authorization code obtained after completing the installation flow from `myMSTeamsInstallationInfo`.
# createMySlackIntegration
Source: https://www.plain.com/docs/graphql-reference/mutations/createMySlackIntegration
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.
# createNote
Source: https://www.plain.com/docs/graphql-reference/mutations/createNote
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.
# createSavedThreadsView
Source: https://www.plain.com/docs/graphql-reference/mutations/createSavedThreadsView
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.
# createServiceLevelAgreement
Source: https://www.plain.com/docs/graphql-reference/mutations/createServiceLevelAgreement
Create a service level agreement (SLA) for a tier. Each SLA commits your team to a time target and can be scoped by thread priority and/or label type. Provide exactly one of `firstResponseTimeMinutes` (time from thread creation to first reply), `nextResponseTimeMinutes` (time from each customer message to the next reply), `firstResolutionTimeMinutes` (time from thread creation to first Done), or `totalResolutionTimeMinutes` (time from thread creation to Done, including after reopen). A `nextResponseTimeMinutes` SLA can only be created if a `firstResponseTimeMinutes` SLA already exists on the same tier. Requires the `serviceLevelAgreement:create` permission.
# createServiceLevelAgreementPolicy
Source: https://www.plain.com/docs/graphql-reference/mutations/createServiceLevelAgreementPolicy
Create an SLA policy together with its targets. A policy holds at most one target per type, and any combination of types is valid. Business hours are configured on the policy and apply to every target in it. A policy only starts tracking once it is applied to a thread. Requires the `serviceLevelAgreement:create` permission.
# createSidekickCustomSkill
Source: https://www.plain.com/docs/graphql-reference/mutations/createSidekickCustomSkill
Create a workspace custom skill. The name must be a unique URL-safe slug.
# createSidekickMcpServer
Source: https://www.plain.com/docs/graphql-reference/mutations/createSidekickMcpServer
Register a custom (customer-owned) MCP server for Sidekick and mint a Nango Connect session for in-browser OAuth consent. The server is created in an unreachable state; call completeSidekickMcpServerConnection after consent to discover its tools and mark it connected.
# createSnippet
Source: https://www.plain.com/docs/graphql-reference/mutations/createSnippet
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.
# createTask
Source: https://www.plain.com/docs/graphql-reference/mutations/createTask
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.
# createTenant
Source: https://www.plain.com/docs/graphql-reference/mutations/createTenant
Creates a new tenant. Fails with a validation error if a tenant already exists with the provided `externalId`.
Use `upsertTenant` instead if you want to create or update a tenant idempotently.
Requires the `tenant:create` permission.
# createThread
Source: https://www.plain.com/docs/graphql-reference/mutations/createThread
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.
# createThreadDiscussion
Source: https://www.plain.com/docs/graphql-reference/mutations/createThreadDiscussion
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.
# createThreadEvent
Source: https://www.plain.com/docs/graphql-reference/mutations/createThreadEvent
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.
# createThreadFieldSchema
Source: https://www.plain.com/docs/graphql-reference/mutations/createThreadFieldSchema
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.
# createThreadFromSlackMessage
Source: https://www.plain.com/docs/graphql-reference/mutations/createThreadFromSlackMessage
Creates a thread from a top-level message in a connected customer Slack channel, along with the
replies Plain has already received for it. This is the trigger for the `API_ONLY` ingestion
mode, and works in every other mode too. Ingestion runs asynchronously, so `thread` is
returned only when the message had already been ingested. Requires the `thread:create`
permission.
This is not a way to import historic conversations: only replies Plain saw over the Slack
events API are included, so a message from before the Plain bot joined the channel becomes a
thread missing everything that was said before then.
# createThreadLink
Source: https://www.plain.com/docs/graphql-reference/mutations/createThreadLink
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.
# createTier
Source: https://www.plain.com/docs/graphql-reference/mutations/createTier
Creates a new tier. You can optionally add tenant and company members at creation time. Requires the `tier:create` permission.
# createUserAccount
Source: https://www.plain.com/docs/graphql-reference/mutations/createUserAccount
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.
# createUserAuthDiscordChannelIntegration
Source: https://www.plain.com/docs/graphql-reference/mutations/createUserAuthDiscordChannelIntegration
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`.
# createUserAuthSlackIntegration
Source: https://www.plain.com/docs/graphql-reference/mutations/createUserAuthSlackIntegration
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.
# createWebhookTarget
Source: https://www.plain.com/docs/graphql-reference/mutations/createWebhookTarget
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. You can optionally supply custom HTTP headers (e.g. an `Authorization` header) to send with every delivery. Use `subscriptionEventTypes` to discover valid event type identifiers. Requires the `webhookTarget:create` permission.
# createWorkflow
Source: https://www.plain.com/docs/graphql-reference/mutations/createWorkflow
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.
# createWorkflowRule
Source: https://www.plain.com/docs/graphql-reference/mutations/createWorkflowRule
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.
# createWorkflowShareLink
Source: https://www.plain.com/docs/graphql-reference/mutations/createWorkflowShareLink
Create a content-addressed share link for a workflow. Returns a short token that can be used to load the workflow in another workspace.
# createWorkflowStep
Source: https://www.plain.com/docs/graphql-reference/mutations/createWorkflowStep
Add a single step to a workflow. For bulk changes to a workflow's step graph, prefer `bulkUpsertWorkflowSteps`.
# createWorkspaceCursorIntegration
Source: https://www.plain.com/docs/graphql-reference/mutations/createWorkspaceCursorIntegration
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.
# createWorkspaceEmailDomainSettings
Source: https://www.plain.com/docs/graphql-reference/mutations/createWorkspaceEmailDomainSettings
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.
# createWorkspaceFileDownloadUrl
Source: https://www.plain.com/docs/graphql-reference/mutations/createWorkspaceFileDownloadUrl
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.
# createWorkspaceFileUploadUrl
Source: https://www.plain.com/docs/graphql-reference/mutations/createWorkspaceFileUploadUrl
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.
# createWorkspaceSlackChannelIntegration
Source: https://www.plain.com/docs/graphql-reference/mutations/createWorkspaceSlackChannelIntegration
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.
# createWorkspaceSlackIntegration
Source: https://www.plain.com/docs/graphql-reference/mutations/createWorkspaceSlackIntegration
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.
# createWorkspaceSlackSidekickIntegration
Source: https://www.plain.com/docs/graphql-reference/mutations/createWorkspaceSlackSidekickIntegration
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 the Sidekick Slack channel, and persists the integration. The channel is normally #ask-plain; if that name is already taken in the Slack workspace the next free name is used instead, so read askSidekickSlackChannelName off the returned integration and show the user which channel was created rather than assuming #ask-plain. Fails only if every candidate name is taken. 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.
# deleteAiToneRules
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteAiToneRules
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.
# deleteApiKey
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteApiKey
Permanently revoke an API key. The deleted key is returned for confirmation. System-managed keys cannot be deleted. Requires the `apiKey:delete` permission.
# deleteAutoresponder
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteAutoresponder
Permanently delete an autoresponder. The deleted autoresponder is returned in the response. Requires the `autoresponder:delete` permission.
# deleteBroadcast
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteBroadcast
Soft-deletes a broadcast. Deleted broadcasts are excluded from `broadcasts`
and `searchBroadcasts` but remain fetchable by ID with `isDeleted: true`. Requires
the `broadcast:delete` permission.
# deleteBroadcastAudience
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteBroadcastAudience
Soft-deletes a broadcast audience. Rejected with `cannot_delete_broadcast_audience` while a
broadcast targeting it is scheduled or being sent, since past `SCHEDULED` its author can no longer
edit the reference away. Requires the `broadcastAudience:delete` permission.
# deleteBusinessHoursSchedule
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteBusinessHoursSchedule
Delete a business hours schedule and the slots belonging to it. Requires the `businessHours:delete` permission.
# deleteChatApp
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteChatApp
Permanently delete a chat app. This action cannot be undone.
Requires the `chatApp:delete` permission.
# deleteChatAppSecret
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteChatAppSecret
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.
# deleteCompany
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteCompany
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.
# deleteCustomRole
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteCustomRole
Permanently deletes a custom role by its ID. Returns the ID of the deleted role on success. Requires the `roles:edit` permission.
# deleteCustomer
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteCustomer
Permanently deletes a customer and all associated data (threads, timeline entries, etc.).
Deletion is asynchronous and cannot be reversed. Requires the `customer:delete` permission.
# deleteCustomerCardConfig
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteCustomerCardConfig
Permanently deletes a customer card config and stops loading that card for all customers.
# deleteCustomerGroup
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteCustomerGroup
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.
# deleteCustomerSurvey
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteCustomerSurvey
Permanently deletes a customer survey. Returns an error if the survey does not exist. Requires the `customerSurvey:delete` permission.
# deleteEmailSuppression
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteEmailSuppression
Removes a hard-bounce or manual suppression for one email address. Spam-complaint suppressions cannot be removed.
# deleteEscalationPath
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteEscalationPath
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.
# deleteGithubUserAuthIntegration
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteGithubUserAuthIntegration
Disconnects the currently authenticated user's GitHub integration, removing their stored credentials. Requires the 'githubUserAuthIntegration:delete' permission.
# deleteHelpCenter
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteHelpCenter
Permanently delete a help center and all its articles and article groups. This action cannot be undone. Requires the helpCenter:edit permission.
# deleteHelpCenterArticle
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteHelpCenterArticle
Permanently delete a help center article. Requires the helpCenter:edit permission.
# deleteHelpCenterArticleGroup
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteHelpCenterArticleGroup
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.
# deleteKnowledgeSource
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteKnowledgeSource
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.
# deleteLinearAppIntegration
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteLinearAppIntegration
# deleteMachineUser
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteMachineUser
Deletes a machine user and all of its API keys. The deleted machine user is returned in the response. Requires the `machineUser:delete` permission.
# deleteMyFavoritePage
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteMyFavoritePage
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.
# deleteMyLinearIntegration
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteMyLinearIntegration
Disconnects the current user's Linear integration. If no integration exists this is a no-op and succeeds.
# deleteMyMSTeamsIntegration
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteMyMSTeamsIntegration
Disconnects the current user's personal Microsoft Teams integration from Plain.
# deleteMyServiceAuthorization
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteMyServiceAuthorization
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.
# deleteMySlackIntegration
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteMySlackIntegration
Disconnects the current user's personal Slack notifications integration.
# deleteNote
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteNote
Soft-deletes a note. The note is marked as deleted but its record is retained. Requires the `note:delete` permission.
# deleteQueuedAgentSessionMessage
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteQueuedAgentSessionMessage
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.
# deleteSavedThreadsView
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteSavedThreadsView
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.
# deleteServiceAuthorization
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteServiceAuthorization
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.
# deleteServiceLevelAgreement
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteServiceLevelAgreement
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.
# deleteServiceLevelAgreementPolicy
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteServiceLevelAgreementPolicy
Delete an SLA policy. Its targets and their SLA trackers are deleted with it, and any thread the policy was applied to falls back to its tier's SLAs. Requires the `serviceLevelAgreement:delete` permission.
# deleteSetting
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteSetting
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.
# deleteSidekickCustomSkill
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteSidekickCustomSkill
Delete a workspace custom skill.
# deleteSidekickMcpServer
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteSidekickMcpServer
Delete a custom MCP server and best-effort revoke its Nango connection. Idempotent: deleting an unknown server is a no-op success.
# deleteSnippet
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteSnippet
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.
# deleteTask
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteTask
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.
# deleteTenant
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteTenant
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.
# deleteTenantField
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteTenantField
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.
# deleteTenantFieldSchema
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteTenantFieldSchema
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.
# deleteThread
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteThread
Permanently delete a thread and all its associated data from Plain. This action is irreversible — use with caution. Requires the `thread:delete` permission.
# deleteThreadChannelAssociation
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteThreadChannelAssociation
Removes a thread channel association, unlinking the connected Slack channel from its associated company or tenant. Requires the `threadChannelAssociation:delete` permission.
# deleteThreadDiscussion
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteThreadDiscussion
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.
# deleteThreadField
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteThreadField
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.
# deleteThreadFieldSchema
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteThreadFieldSchema
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.
# deleteThreadLink
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteThreadLink
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.
# deleteTier
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteTier
Deletes a tier permanently. All tenant and company memberships in that tier are also removed. Requires the `tier:delete` permission.
# deleteUser
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteUser
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.
# deleteUserAuthDiscordChannelIntegration
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteUserAuthDiscordChannelIntegration
Removes the current user's personal Discord authentication for the specified integration, preventing them from sending Discord messages until they re-authenticate.
# deleteUserAuthSlackIntegration
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteUserAuthSlackIntegration
Disconnects the current user's user-auth Slack integration for the specified Slack workspace.
# deleteWebhookTarget
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteWebhookTarget
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.
# deleteWorkflow
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteWorkflow
Permanently delete a workflow and all its steps. Existing executions are retained for audit purposes.
# deleteWorkflowRule
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteWorkflowRule
Permanently delete a workflow rule. This action cannot be undone.
# deleteWorkflowStep
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteWorkflowStep
Delete a single step from a workflow. You are responsible for updating any steps that reference this step in their transitions.
# deleteWorkspaceCursorIntegration
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteWorkspaceCursorIntegration
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.
# deleteWorkspaceDiscordChannelIntegration
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteWorkspaceDiscordChannelIntegration
Removes a workspace Discord channel integration, disconnecting the Discord guild from the workspace. Existing threads and messages are not deleted.
# deleteWorkspaceDiscordIntegration
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteWorkspaceDiscordIntegration
Removes a webhook-based workspace Discord integration by its ID.
# deleteWorkspaceEmailDomainSettings
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteWorkspaceEmailDomainSettings
Removes the workspace's custom email domain configuration. After deletion, email will no longer be routed through the custom domain.
# deleteWorkspaceFile
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteWorkspaceFile
Delete a workspace file by ID. Requires the `workspaceFile:create` permission.
# deleteWorkspaceInvite
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteWorkspaceInvite
Cancel and delete a pending workspace invite. The invited user will no longer be able to accept it. The deleted invite is returned.
# deleteWorkspaceMSTeamsIntegration
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteWorkspaceMSTeamsIntegration
Removes a workspace-level Microsoft Teams integration. Pass the integration's `id` as `integrationId`. Returns the deleted integration.
# deleteWorkspaceSlackChannelIntegration
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteWorkspaceSlackChannelIntegration
Disconnects and removes a workspace Slack channel integration. Connected channels for this integration will no longer be monitored.
# deleteWorkspaceSlackIntegration
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteWorkspaceSlackIntegration
Disconnects and removes a workspace-level Slack notifications integration.
# deleteWorkspaceSlackSidekickIntegration
Source: https://www.plain.com/docs/graphql-reference/mutations/deleteWorkspaceSlackSidekickIntegration
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.
# dismissSuggestedLabelTypes
Source: https://www.plain.com/docs/graphql-reference/mutations/dismissSuggestedLabelTypes
Dismisses one or more pending suggested label types without creating label types. All ids must currently be PENDING. Requires the `labelType:create` permission.
# editQueuedAgentSessionMessage
Source: https://www.plain.com/docs/graphql-reference/mutations/editQueuedAgentSessionMessage
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.
# escalateThread
Source: https://www.plain.com/docs/graphql-reference/mutations/escalateThread
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.
# exportWorkflowTemplate
Source: https://www.plain.com/docs/graphql-reference/mutations/exportWorkflowTemplate
Build the files for publishing a workflow as a template; writes nothing. Requires the
`workflowRule:create` permission.
# forkThread
Source: https://www.plain.com/docs/graphql-reference/mutations/forkThread
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.
# generateAiToneRulesFromDescription
Source: https://www.plain.com/docs/graphql-reference/mutations/generateAiToneRulesFromDescription
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.
# generateHelpCenterArticle
Source: https://www.plain.com/docs/graphql-reference/mutations/generateHelpCenterArticle
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.
# importCustomers
Source: https://www.plain.com/docs/graphql-reference/mutations/importCustomers
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.
# importTenantFieldSchemas
Source: https://www.plain.com/docs/graphql-reference/mutations/importTenantFieldSchemas
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.
# importTenantFieldSchemasFromService
Source: https://www.plain.com/docs/graphql-reference/mutations/importTenantFieldSchemasFromService
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.
# importTenants
Source: https://www.plain.com/docs/graphql-reference/mutations/importTenants
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.
# importThread
Source: https://www.plain.com/docs/graphql-reference/mutations/importThread
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.
# importThreadDiscussion
Source: https://www.plain.com/docs/graphql-reference/mutations/importThreadDiscussion
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.
# importThreadMessages
Source: https://www.plain.com/docs/graphql-reference/mutations/importThreadMessages
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.
# inviteUserToWorkspace
Source: https://www.plain.com/docs/graphql-reference/mutations/inviteUserToWorkspace
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.
# lockThread
Source: https://www.plain.com/docs/graphql-reference/mutations/lockThread
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.
# markCustomerAsSpam
Source: https://www.plain.com/docs/graphql-reference/mutations/markCustomerAsSpam
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.
# markThreadAsDone
Source: https://www.plain.com/docs/graphql-reference/mutations/markThreadAsDone
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.
# markThreadAsTodo
Source: https://www.plain.com/docs/graphql-reference/mutations/markThreadAsTodo
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.
# markThreadDiscussionRead
Source: https://www.plain.com/docs/graphql-reference/mutations/markThreadDiscussionRead
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.
# mintEmbedToken
Source: https://www.plain.com/docs/graphql-reference/mutations/mintEmbedToken
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.
# moveLabelType
Source: https://www.plain.com/docs/graphql-reference/mutations/moveLabelType
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.
# moveWorkflow
Source: https://www.plain.com/docs/graphql-reference/mutations/moveWorkflow
Move a workflow so it sits directly after another, or to the front of the list when `afterWorkflowId` is null. Only the moved workflow changes.
# previewBillingPlanChange
Source: https://www.plain.com/docs/graphql-reference/mutations/previewBillingPlanChange
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.
# purchaseCredits
Source: https://www.plain.com/docs/graphql-reference/mutations/purchaseCredits
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.
# refreshConnectedDiscordChannels
Source: https://www.plain.com/docs/graphql-reference/mutations/refreshConnectedDiscordChannels
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.
# refreshSidekickMcpServerTools
Source: https://www.plain.com/docs/graphql-reference/mutations/refreshSidekickMcpServerTools
Re-discover a custom MCP server's tools (tools/list) and reconcile the cached snapshot: newly-appeared tools start in the "ask" (approval-required) state.
# refreshWorkspaceSlackChannelIntegration
Source: https://www.plain.com/docs/graphql-reference/mutations/refreshWorkspaceSlackChannelIntegration
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.
# refreshWorkspaceSlackSidekickIntegration
Source: https://www.plain.com/docs/graphql-reference/mutations/refreshWorkspaceSlackSidekickIntegration
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.
# regenerateWorkspaceHmac
Source: https://www.plain.com/docs/graphql-reference/mutations/regenerateWorkspaceHmac
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.
# reindexKnowledgeSource
Source: https://www.plain.com/docs/graphql-reference/mutations/reindexKnowledgeSource
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.
# reloadCustomerCardInstance
Source: https://www.plain.com/docs/graphql-reference/mutations/reloadCustomerCardInstance
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.
# removeAdditionalAssignees
Source: https://www.plain.com/docs/graphql-reference/mutations/removeAdditionalAssignees
Remove one or more additional assignees from a thread. Does not affect the primary assignee. Requires the `thread:unassign` and `thread:read` permissions.
# removeCustomerFromCustomerGroups
Source: https://www.plain.com/docs/graphql-reference/mutations/removeCustomerFromCustomerGroups
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.
# removeCustomerFromTenants
Source: https://www.plain.com/docs/graphql-reference/mutations/removeCustomerFromTenants
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.
# removeLabels
Source: https://www.plain.com/docs/graphql-reference/mutations/removeLabels
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.
# removeLabelsFromUser
Source: https://www.plain.com/docs/graphql-reference/mutations/removeLabelsFromUser
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.
# removeMembersFromTier
Source: https://www.plain.com/docs/graphql-reference/mutations/removeMembersFromTier
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.
# removeTenantFieldSchemaMapping
Source: https://www.plain.com/docs/graphql-reference/mutations/removeTenantFieldSchemaMapping
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.
# removeUserFromActiveBillingRota
Source: https://www.plain.com/docs/graphql-reference/mutations/removeUserFromActiveBillingRota
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.
# removeWorkspaceAlternateSupportEmailAddress
Source: https://www.plain.com/docs/graphql-reference/mutations/removeWorkspaceAlternateSupportEmailAddress
Removes an alternate support email address from the workspace's custom domain configuration.
# reorderAutoresponders
Source: https://www.plain.com/docs/graphql-reference/mutations/reorderAutoresponders
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.
# reorderCustomerCardConfigs
Source: https://www.plain.com/docs/graphql-reference/mutations/reorderCustomerCardConfigs
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.
# reorderCustomerSurveys
Source: https://www.plain.com/docs/graphql-reference/mutations/reorderCustomerSurveys
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.
# reorderThreadFieldSchemas
Source: https://www.plain.com/docs/graphql-reference/mutations/reorderThreadFieldSchemas
Updates the display order of thread field schemas. You only need to include schemas whose order is changing — omitted schemas are left unchanged.
# replyToEmail
Source: https://www.plain.com/docs/graphql-reference/mutations/replyToEmail
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.
# replyToThread
Source: https://www.plain.com/docs/graphql-reference/mutations/replyToThread
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.
# resolveAgentApproval
Source: https://www.plain.com/docs/graphql-reference/mutations/resolveAgentApproval
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.
# resolveCustomerForMSTeamsChannel
Source: https://www.plain.com/docs/graphql-reference/mutations/resolveCustomerForMSTeamsChannel
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.
# resolveCustomerForSlackChannel
Source: https://www.plain.com/docs/graphql-reference/mutations/resolveCustomerForSlackChannel
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.
# scheduleBroadcast
Source: https://www.plain.com/docs/graphql-reference/mutations/scheduleBroadcast
Queues a broadcast to be sent, moving it from `DRAFT` to `SCHEDULED`. Calling this again on a
broadcast that has not started sending changes when it will go out; once the send has started it
is rejected. The broadcast must have a sender and at least one channel. A null `scheduledAt`
clears the schedule instead, returning the broadcast to `DRAFT`. Requires the
`broadcast:send` and `broadcast:read` permissions.
# sendBulkEmail
Source: https://www.plain.com/docs/graphql-reference/mutations/sendBulkEmail
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.
# sendChat
Source: https://www.plain.com/docs/graphql-reference/mutations/sendChat
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.
# sendCustomerChat
Source: https://www.plain.com/docs/graphql-reference/mutations/sendCustomerChat
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.
# sendDiscordMessage
Source: https://www.plain.com/docs/graphql-reference/mutations/sendDiscordMessage
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.
# sendDiscussionMessage
Source: https://www.plain.com/docs/graphql-reference/mutations/sendDiscussionMessage
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).
# sendMSTeamsMessage
Source: https://www.plain.com/docs/graphql-reference/mutations/sendMSTeamsMessage
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.
# sendNewEmail
Source: https://www.plain.com/docs/graphql-reference/mutations/sendNewEmail
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.
# sendSlackMessage
Source: https://www.plain.com/docs/graphql-reference/mutations/sendSlackMessage
Sends a Slack message on a thread. The message appears in the thread's associated Slack channel as a reply or new message.
# sendTestBroadcast
Source: https://www.plain.com/docs/graphql-reference/mutations/sendTestBroadcast
Sends the broadcast to the Slack channels you name, up to 10, so you can see what recipients will
get before the real audience does. It goes out through the same pipeline as a real send, under the
broadcast's own sender, so what arrives is what any channel would have received — with a notice
at the top marking it as a test, since everyone in the channels you choose will see it.
Every channel must already be connected to this workspace. They are independent of the broadcast's
own audience, which is left untouched: the broadcast needs a sender and a notification title, but it
does **not** need any channels of its own, because testing is what you do before choosing them.
Only available while the broadcast can still be edited: once its send has started, it can no
longer be tested.
A test send never changes the broadcast's `status`, and never unlocks or locks editing. Requires
the `broadcast:send` permission.
# sendThreadDiscussionMessage
Source: https://www.plain.com/docs/graphql-reference/mutations/sendThreadDiscussionMessage
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.
# setCustomerTenants
Source: https://www.plain.com/docs/graphql-reference/mutations/setCustomerTenants
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.
# setSlackAutoJoinRules
Source: https://www.plain.com/docs/graphql-reference/mutations/setSlackAutoJoinRules
Replaces all auto-join rules for a workspace slack channel integration.
The provided rules become the complete set of rules for the integration.
# setupTenantFieldSchemaMapping
Source: https://www.plain.com/docs/graphql-reference/mutations/setupTenantFieldSchemaMapping
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.
# shareThreadToUserInSlack
Source: https://www.plain.com/docs/graphql-reference/mutations/shareThreadToUserInSlack
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.
# snoozeThread
Source: https://www.plain.com/docs/graphql-reference/mutations/snoozeThread
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.
# startServiceAuthorization
Source: https://www.plain.com/docs/graphql-reference/mutations/startServiceAuthorization
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.
# syncBusinessHoursSlots
Source: https://www.plain.com/docs/graphql-reference/mutations/syncBusinessHoursSlots
Replace the workspace's business hours with the provided set of slots. Deprecated — use createBusinessHoursSchedule or updateBusinessHoursSchedule instead, which support multiple named schedules. Only works while the workspace has exactly one business hours schedule; otherwise it returns an error. Requires the `businessHours:edit` permission.
# syncUserWorkingHours
Source: https://www.plain.com/docs/graphql-reference/mutations/syncUserWorkingHours
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.
# toggleSlackMessageReaction
Source: https://www.plain.com/docs/graphql-reference/mutations/toggleSlackMessageReaction
Adds or removes a reaction from a slack message timeline entry.
# toggleWorkflowRulePublished
Source: https://www.plain.com/docs/graphql-reference/mutations/toggleWorkflowRulePublished
Toggle the published state of a workflow rule. Published rules are active and will fire automatically; unpublished rules are drafts.
# triggerWorkflow
Source: https://www.plain.com/docs/graphql-reference/mutations/triggerWorkflow
Manually trigger a workflow against a specific thread. Returns the resulting WorkflowExecution so you can track the run's status and step results.
# triggerWorkflowRule
Source: https://www.plain.com/docs/graphql-reference/mutations/triggerWorkflowRule
Manually trigger a workflow rule against a specific thread, bypassing its automatic trigger conditions. Useful for testing rules or applying them on demand.
# unarchiveLabelType
Source: https://www.plain.com/docs/graphql-reference/mutations/unarchiveLabelType
Restores an archived label type so it can be applied to threads again. Requires the `labelType:edit` permission.
# unassignThread
Source: https://www.plain.com/docs/graphql-reference/mutations/unassignThread
Remove the primary assignee from a thread, leaving it unassigned. Requires the `thread:unassign` and `thread:read` permissions.
# unmarkCustomerAsSpam
Source: https://www.plain.com/docs/graphql-reference/mutations/unmarkCustomerAsSpam
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.
# updateActiveBillingRota
Source: https://www.plain.com/docs/graphql-reference/mutations/updateActiveBillingRota
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.
# updateAgentSandboxToolPolicy
Source: https://www.plain.com/docs/graphql-reference/mutations/updateAgentSandboxToolPolicy
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.
# updateAiToneRules
Source: https://www.plain.com/docs/graphql-reference/mutations/updateAiToneRules
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.
# updateApiKey
Source: https://www.plain.com/docs/graphql-reference/mutations/updateApiKey
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.
# updateAutoresponder
Source: https://www.plain.com/docs/graphql-reference/mutations/updateAutoresponder
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.
# updateBroadcast
Source: https://www.plain.com/docs/graphql-reference/mutations/updateBroadcast
Updates one or more fields of an existing broadcast. Each field uses a wrapper input —
pass `{ value: ... }` to set it or omit it entirely to leave it unchanged. `type` is
not updatable. Requires the `broadcast:edit` permission.
# updateBroadcastAudience
Source: https://www.plain.com/docs/graphql-reference/mutations/updateBroadcastAudience
Updates a broadcast audience's name, filters, or both. Editing an audience changes who every
broadcast using it will reach on its next send. Requires the `broadcastAudience:edit` permission.
# updateBusinessHoursSchedule
Source: https://www.plain.com/docs/graphql-reference/mutations/updateBusinessHoursSchedule
Update a business hours schedule's name, its slots, or both. Omitted fields are left unchanged; passing an empty `slots` array clears the schedule's business hours. Overlapping and adjacent slots are merged on write. Requires the `businessHours:edit` permission.
# updateChatApp
Source: https://www.plain.com/docs/graphql-reference/mutations/updateChatApp
Update the name or logo of an existing chat app. Only the fields provided in the input are changed.
Requires the `chatApp:edit` permission.
# updateCompanyTier
Source: https://www.plain.com/docs/graphql-reference/mutations/updateCompanyTier
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.
# updateConnectedDiscordChannel
Source: https://www.plain.com/docs/graphql-reference/mutations/updateConnectedDiscordChannel
Updates settings for a connected Discord channel, such as enabling or disabling it. Disabled channels will not receive new messages from Plain.
# updateConnectedSlackChannel
Source: https://www.plain.com/docs/graphql-reference/mutations/updateConnectedSlackChannel
Updates the settings of a connected Slack channel, such as its channel type (customer or discussion) or whether it is enabled.
# updateCustomRole
Source: https://www.plain.com/docs/graphql-reference/mutations/updateCustomRole
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.
# updateCustomerCardConfig
Source: https://www.plain.com/docs/graphql-reference/mutations/updateCustomerCardConfig
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.
# updateCustomerCompany
Source: https://www.plain.com/docs/graphql-reference/mutations/updateCustomerCompany
Assigns a customer to a different company, or clears their company association. Requires the
`customer:edit` permission.
# updateCustomerGroup
Source: https://www.plain.com/docs/graphql-reference/mutations/updateCustomerGroup
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.
# updateCustomerSurvey
Source: https://www.plain.com/docs/graphql-reference/mutations/updateCustomerSurvey
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.
# updateEscalationPath
Source: https://www.plain.com/docs/graphql-reference/mutations/updateEscalationPath
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.
# updateGeneratedReply
Source: https://www.plain.com/docs/graphql-reference/mutations/updateGeneratedReply
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.
# updateHelpCenter
Source: https://www.plain.com/docs/graphql-reference/mutations/updateHelpCenter
Update the settings, branding, or access configuration of an existing help center. Only fields that are provided are updated. Requires the helpCenter:edit permission.
# updateHelpCenterArticleGroup
Source: https://www.plain.com/docs/graphql-reference/mutations/updateHelpCenterArticleGroup
Update the name of an existing article group. Requires the helpCenter:edit permission.
# updateHelpCenterCustomDomainName
Source: https://www.plain.com/docs/graphql-reference/mutations/updateHelpCenterCustomDomainName
Set or clear the custom domain name for a help center. After setting, use verifyHelpCenterCustomDomainName to confirm DNS propagation. Requires the helpCenter:edit permission.
# updateHelpCenterIndex
Source: https://www.plain.com/docs/graphql-reference/mutations/updateHelpCenterIndex
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.
# updateImportJobDefinition
Source: https://www.plain.com/docs/graphql-reference/mutations/updateImportJobDefinition
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.
# updateInternalNotifications
Source: https://www.plain.com/docs/graphql-reference/mutations/updateInternalNotifications
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.
# updateLabelType
Source: https://www.plain.com/docs/graphql-reference/mutations/updateLabelType
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.
# updateMachineUser
Source: https://www.plain.com/docs/graphql-reference/mutations/updateMachineUser
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.
# updateMyUser
Source: https://www.plain.com/docs/graphql-reference/mutations/updateMyUser
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.
# updateNote
Source: https://www.plain.com/docs/graphql-reference/mutations/updateNote
Updates the text, markdown, or attachments of an existing note. Requires the `note:edit` permission.
# updateSavedThreadsView
Source: https://www.plain.com/docs/graphql-reference/mutations/updateSavedThreadsView
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.
# updateServiceLevelAgreement
Source: https://www.plain.com/docs/graphql-reference/mutations/updateServiceLevelAgreement
Update an existing SLA's time target, priority or label filter, business-hours setting, or breach actions. You cannot change an SLA's type 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.
# updateServiceLevelAgreementPolicy
Source: https://www.plain.com/docs/graphql-reference/mutations/updateServiceLevelAgreementPolicy
Update an SLA policy and its targets. Pass the full set of targets you want the policy to have: a target of an existing type is updated in place, so live SLA trackers are unaffected, a new type is added, and a type you leave out is removed along with its trackers. Requires the `serviceLevelAgreement:edit` permission.
# updateSetting
Source: https://www.plain.com/docs/graphql-reference/mutations/updateSetting
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.
# updateSidekickCustomSkill
Source: https://www.plain.com/docs/graphql-reference/mutations/updateSidekickCustomSkill
Update a workspace custom skill. Only the provided fields are changed.
# updateSidekickGithubConfig
Source: https://www.plain.com/docs/graphql-reference/mutations/updateSidekickGithubConfig
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.
# updateSidekickMcpServer
Source: https://www.plain.com/docs/graphql-reference/mutations/updateSidekickMcpServer
Update a custom MCP server's name or operating instructions. The server URL and slug are immutable after creation.
# updateSidekickPosthogConfig
Source: https://www.plain.com/docs/graphql-reference/mutations/updateSidekickPosthogConfig
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.
# updateSidekickServiceConfig
Source: https://www.plain.com/docs/graphql-reference/mutations/updateSidekickServiceConfig
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.
# updateSidekickSettings
Source: https://www.plain.com/docs/graphql-reference/mutations/updateSidekickSettings
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.
# updateSidekickSlackConfig
Source: https://www.plain.com/docs/graphql-reference/mutations/updateSidekickSlackConfig
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.
# updateSnippet
Source: https://www.plain.com/docs/graphql-reference/mutations/updateSnippet
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.
# updateTask
Source: https://www.plain.com/docs/graphql-reference/mutations/updateTask
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.
# updateTenantTier
Source: https://www.plain.com/docs/graphql-reference/mutations/updateTenantTier
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.
# updateThreadAgentStatus
Source: https://www.plain.com/docs/graphql-reference/mutations/updateThreadAgentStatus
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.
# updateThreadChannelAssociationTenantMembershipPolicy
Source: https://www.plain.com/docs/graphql-reference/mutations/updateThreadChannelAssociationTenantMembershipPolicy
Changes how tenants are assigned for an existing thread channel association. Requires the `threadChannelAssociation:edit` permission.
# updateThreadEscalationPath
Source: https://www.plain.com/docs/graphql-reference/mutations/updateThreadEscalationPath
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.
# updateThreadExternalId
Source: https://www.plain.com/docs/graphql-reference/mutations/updateThreadExternalId
Update the external ID of a thread. Pass `externalId: null` to clear it. Requires the `thread:edit` permission.
# updateThreadFieldSchema
Source: https://www.plain.com/docs/graphql-reference/mutations/updateThreadFieldSchema
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.
# updateThreadServiceLevelAgreementPolicy
Source: https://www.plain.com/docs/graphql-reference/mutations/updateThreadServiceLevelAgreementPolicy
Assign an SLA policy to a thread. A policy takes precedence over the thread's tier when selecting SLAs. Pass `serviceLevelAgreementPolicyId: null` to detach the policy and fall back to the tier's SLAs. Requires the `thread:edit` permission.
# updateThreadSuggestedActionStatus
Source: https://www.plain.com/docs/graphql-reference/mutations/updateThreadSuggestedActionStatus
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.
# updateThreadTenant
Source: https://www.plain.com/docs/graphql-reference/mutations/updateThreadTenant
Move a thread to a different tenant, or pass `tenantIdentifier: null` to detach it from its current tenant. Requires the `thread:edit` permission.
# updateThreadTier
Source: https://www.plain.com/docs/graphql-reference/mutations/updateThreadTier
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.
# updateThreadTitle
Source: https://www.plain.com/docs/graphql-reference/mutations/updateThreadTitle
Update the title of a thread. Requires the `thread:edit` permission.
# updateTier
Source: https://www.plain.com/docs/graphql-reference/mutations/updateTier
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.
# updateUserDefaultSavedThreadsView
Source: https://www.plain.com/docs/graphql-reference/mutations/updateUserDefaultSavedThreadsView
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.
# updateWebhookTarget
Source: https://www.plain.com/docs/graphql-reference/mutations/updateWebhookTarget
Update an existing webhook target. You can change the URL, pause or resume deliveries via `isEnabled`, switch to a newer schema version, change which event types are subscribed to, or replace the custom HTTP headers. Scalar fields use a `{ value: ... }` wrapper so you can omit fields you do not want to change; `eventSubscriptions` and `headers` are full replacements of the previous lists, so include every entry the target should keep. At least one field must be provided. Requires the `webhookTarget:edit` permission.
# updateWorkflow
Source: https://www.plain.com/docs/graphql-reference/mutations/updateWorkflow
Update a workflow's name, trigger configuration, entry step, or published status. To change where a workflow sits in the list, use `moveWorkflow`.
# updateWorkflowRule
Source: https://www.plain.com/docs/graphql-reference/mutations/updateWorkflowRule
Update the name, payload, or display order of an existing workflow rule.
# updateWorkflowStep
Source: https://www.plain.com/docs/graphql-reference/mutations/updateWorkflowStep
Update a single workflow step's configuration, transitions, or canvas position. Replacing `transitions` is a full replacement of the array.
# updateWorkspace
Source: https://www.plain.com/docs/graphql-reference/mutations/updateWorkspace
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.
# updateWorkspaceEmailSettings
Source: https://www.plain.com/docs/graphql-reference/mutations/updateWorkspaceEmailSettings
Updates workspace-level email settings such as whether email is enabled and the list of BCC addresses that are copied on all outbound emails.
# upsertCompany
Source: https://www.plain.com/docs/graphql-reference/mutations/upsertCompany
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.
# upsertCustomer
Source: https://www.plain.com/docs/graphql-reference/mutations/upsertCustomer
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.
# upsertCustomerGroup
Source: https://www.plain.com/docs/graphql-reference/mutations/upsertCustomerGroup
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.
# upsertHelpCenterArticle
Source: https://www.plain.com/docs/graphql-reference/mutations/upsertHelpCenterArticle
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.
# upsertMyEmailSignature
Source: https://www.plain.com/docs/graphql-reference/mutations/upsertMyEmailSignature
Creates or updates the email signature for the currently authenticated user. The signature is appended automatically to outbound emails sent by that user.
# upsertRoleScopes
Source: https://www.plain.com/docs/graphql-reference/mutations/upsertRoleScopes
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.
# upsertTeamSettings
Source: https://www.plain.com/docs/graphql-reference/mutations/upsertTeamSettings
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.
# upsertTenant
Source: https://www.plain.com/docs/graphql-reference/mutations/upsertTenant
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.
# upsertTenantField
Source: https://www.plain.com/docs/graphql-reference/mutations/upsertTenantField
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.
# upsertTenantFieldSchema
Source: https://www.plain.com/docs/graphql-reference/mutations/upsertTenantFieldSchema
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.
# upsertThreadField
Source: https://www.plain.com/docs/graphql-reference/mutations/upsertThreadField
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.
# verifyHelpCenterCustomDomainName
Source: https://www.plain.com/docs/graphql-reference/mutations/verifyHelpCenterCustomDomainName
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.
# verifyWorkspaceEmailDnsSettings
Source: https://www.plain.com/docs/graphql-reference/mutations/verifyWorkspaceEmailDnsSettings
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.
# verifyWorkspaceEmailForwardingSettings
Source: https://www.plain.com/docs/graphql-reference/mutations/verifyWorkspaceEmailForwardingSettings
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.
# activeThreadCluster
Source: https://www.plain.com/docs/graphql-reference/queries/activeThreadCluster
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.
# agentSandboxToolPolicies
Source: https://www.plain.com/docs/graphql-reference/queries/agentSandboxToolPolicies
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").
# aiFeedback
Source: https://www.plain.com/docs/graphql-reference/queries/aiFeedback
Paginated list of the feedback your team has submitted on Plain's AI features, such as a thumbs up or down on an Ari reply, newest first. Use the optional `filters` argument to narrow results by feature, sentiment or submission time. Requires the `aiFeatureFeedback:read` permission.
# aiToneRules
Source: https://www.plain.com/docs/graphql-reference/queries/aiToneRules
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.
# autoresponder
Source: https://www.plain.com/docs/graphql-reference/queries/autoresponder
Fetch a single autoresponder by its ID. Returns null if no autoresponder with that ID exists.
# autoresponders
Source: https://www.plain.com/docs/graphql-reference/queries/autoresponders
List all autoresponders in the workspace, ordered by their configured priority order. Supports cursor-based pagination.
# billingPlans
Source: https://www.plain.com/docs/graphql-reference/queries/billingPlans
Returns the available billing plans that can be selected for self-serve checkout. Supports standard forward and backward pagination.
# broadcast
Source: https://www.plain.com/docs/graphql-reference/queries/broadcast
Fetches a single broadcast by its ID, or null if no broadcast with that ID exists.
Returns soft-deleted broadcasts (where `isDeleted` is true). Requires the
`broadcast:read` permission.
# broadcastAudience
Source: https://www.plain.com/docs/graphql-reference/queries/broadcastAudience
Fetches a single broadcast audience by its ID, or null if no audience with that ID exists.
Requires the `broadcastAudience:read` permission.
# broadcastAudiences
Source: https://www.plain.com/docs/graphql-reference/queries/broadcastAudiences
Returns a paginated list of the workspace's broadcast audiences, newest first. Soft-deleted
audiences are excluded. Requires the `broadcastAudience:read` permission.
# broadcastSendTargetRecipients
Source: https://www.plain.com/docs/graphql-reference/queries/broadcastSendTargetRecipients
Who a send target resolves to right now. Takes the target rather than an id, so it answers for a
filter still being written — before an audience is saved, or before a broadcast's target is
committed — as readily as for one about to be sent.
Point-in-time: the number moves as tenants, tiers and tenant fields change, and as channels are
connected or disabled. Resolved through the same path a send uses, so it is what would actually be
delivered to rather than an estimate assembled another way.
Resolves the target on every call. Requires the `broadcast:read` permission.
# broadcasts
Source: https://www.plain.com/docs/graphql-reference/queries/broadcasts
Returns a paginated list of the workspace's broadcasts, newest first. Soft-deleted
broadcasts are excluded. Optionally narrow the results with `filters` (status). Supports standard
forward and backward cursor pagination. Requires the `broadcast:read` permission.
# businessHoursSchedule
Source: https://www.plain.com/docs/graphql-reference/queries/businessHoursSchedule
Fetches a single business hours schedule by its ID, or null if no schedule with that ID exists. Requires the `businessHours:read` permission.
# businessHoursSchedules
Source: https://www.plain.com/docs/graphql-reference/queries/businessHoursSchedules
Returns a paginated list of the workspace's business hours schedules, sorted by name. Requires the `businessHours:read` permission.
# businessHoursSlots
Source: https://www.plain.com/docs/graphql-reference/queries/businessHoursSlots
Return all active business hours slots for the workspace, merged across every schedule. Deprecated — use businessHoursSchedules instead, which returns each schedule's slots separately. Requires the `businessHours:read` permission.
# chatApp
Source: https://www.plain.com/docs/graphql-reference/queries/chatApp
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.
# chatAppSecret
Source: https://www.plain.com/docs/graphql-reference/queries/chatAppSecret
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.
# chatApps
Source: https://www.plain.com/docs/graphql-reference/queries/chatApps
List all chat apps in the workspace. Supports cursor-based pagination.
Requires the `chatApp:read` permission.
# companies
Source: https://www.plain.com/docs/graphql-reference/queries/companies
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.
# company
Source: https://www.plain.com/docs/graphql-reference/queries/company
Fetches a single company by its ID. Returns null if no company with that ID exists. Requires the `company:read` permission.
# connectedDiscordChannels
Source: https://www.plain.com/docs/graphql-reference/queries/connectedDiscordChannels
Returns a paginated list of Discord channels synced from a specific guild. Use `refreshConnectedDiscordChannels` to pull the latest channel list from Discord before querying.
# connectedMSTeamsChannels
Source: https://www.plain.com/docs/graphql-reference/queries/connectedMSTeamsChannels
Returns a paginated list of Microsoft Teams channels that are connected to this workspace. Supports forward and backward cursor pagination.
# connectedSlackChannel
Source: https://www.plain.com/docs/graphql-reference/queries/connectedSlackChannel
Returns a single connected Slack channel by its Plain ID, or null if not found.
# connectedSlackChannels
Source: https://www.plain.com/docs/graphql-reference/queries/connectedSlackChannels
Gets all slack channels for this workspace, which match the specified filters.
# cursorRepositories
Source: https://www.plain.com/docs/graphql-reference/queries/cursorRepositories
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.
# customRole
Source: https://www.plain.com/docs/graphql-reference/queries/customRole
Returns a single custom role by its ID, or null if not found. Requires the `roles:read` permission.
# customRoles
Source: https://www.plain.com/docs/graphql-reference/queries/customRoles
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.
# customer
Source: https://www.plain.com/docs/graphql-reference/queries/customer
Fetch a single customer by their Plain customer ID. Returns null if no customer with that ID exists. Requires the `customer:read` permission.
# customerByEmail
Source: https://www.plain.com/docs/graphql-reference/queries/customerByEmail
Fetch a customer by their email address. Returns null if no customer with that email exists. Requires the `customer:read` permission.
# customerByExternalId
Source: https://www.plain.com/docs/graphql-reference/queries/customerByExternalId
Get a customer by its external ID. A customer's external ID is unique within a workspace.
# customerCardConfig
Source: https://www.plain.com/docs/graphql-reference/queries/customerCardConfig
Returns a single customer card config by ID. Returns null if no config with the given ID exists.
# customerCardConfigs
Source: https://www.plain.com/docs/graphql-reference/queries/customerCardConfigs
Returns all customer card configs for the workspace, ordered by their `order` field.
# customerCardInstances
Source: https://www.plain.com/docs/graphql-reference/queries/customerCardInstances
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.
# customerGroup
Source: https://www.plain.com/docs/graphql-reference/queries/customerGroup
Fetch a single customer group by its ID. Returns null if no group with the given ID exists in the workspace.
# customerGroups
Source: https://www.plain.com/docs/graphql-reference/queries/customerGroups
Fetch a paginated list of all customer groups in the workspace. Optionally filter by external IDs using the `filters` argument. Uses cursor-based pagination.
# customerSurvey
Source: https://www.plain.com/docs/graphql-reference/queries/customerSurvey
Fetches a single customer survey by its ID. Returns null if no survey with that ID exists.
# customerSurveys
Source: https://www.plain.com/docs/graphql-reference/queries/customerSurveys
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.
# customers
Source: https://www.plain.com/docs/graphql-reference/queries/customers
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.
# deletedThreads
Source: https://www.plain.com/docs/graphql-reference/queries/deletedThreads
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.
# discussion
Source: https://www.plain.com/docs/graphql-reference/queries/discussion
Fetch a single discussion by its ID. Preferred over threadDiscussion for new integrations.
# discussions
Source: https://www.plain.com/docs/graphql-reference/queries/discussions
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.
# emailSuppression
Source: https://www.plain.com/docs/graphql-reference/queries/emailSuppression
Returns the suppression for a specific email address, or null when the address is not suppressed.
# enabledAiToneRulesText
Source: https://www.plain.com/docs/graphql-reference/queries/enabledAiToneRulesText
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.
# escalationPath
Source: https://www.plain.com/docs/graphql-reference/queries/escalationPath
Fetches a single escalation path by its ID. Returns null if no escalation path with the given ID exists. Requires the `escalationPath:read` permission.
# escalationPaths
Source: https://www.plain.com/docs/graphql-reference/queries/escalationPaths
Returns a paginated list of all escalation paths configured in the workspace. Requires the `escalationPath:read` permission.
# generatedReplies
Source: https://www.plain.com/docs/graphql-reference/queries/generatedReplies
This API is in beta and may change without notice.
Returns the current suggested reply candidates for a thread, scoped to the most recent inbound message from the customer or from a machine user. Suggestions submitted via `addGeneratedReply` are always included. Suggestions generated by Plain AI are excluded when suggested responses are disabled or not available for the workspace, or when an externally submitted suggestion exists for the same message. Returns an empty list if the thread has no such message or no suggestion is available. Requires the `generatedReply:read` permission.
# getMSTeamsMembersForChannel
Source: https://www.plain.com/docs/graphql-reference/queries/getMSTeamsMembersForChannel
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.
# githubUserAuthIntegration
Source: https://www.plain.com/docs/graphql-reference/queries/githubUserAuthIntegration
Returns the GitHub user authentication integration for the currently authenticated user, or null if no integration exists.
# helpCenter
Source: https://www.plain.com/docs/graphql-reference/queries/helpCenter
Fetch a single help center by ID. Returns null if not found.
# helpCenterArticle
Source: https://www.plain.com/docs/graphql-reference/queries/helpCenterArticle
Fetch a single help center article by ID. Returns null if not found.
# helpCenterArticleBySlug
Source: https://www.plain.com/docs/graphql-reference/queries/helpCenterArticleBySlug
Fetch an article by its URL slug within a specific help center. Slugs are unique per help center. Returns null if no article matches.
# helpCenterArticleGroup
Source: https://www.plain.com/docs/graphql-reference/queries/helpCenterArticleGroup
Fetch a single article group by ID. Returns null if not found.
# helpCenterArticleGroupBySlug
Source: https://www.plain.com/docs/graphql-reference/queries/helpCenterArticleGroupBySlug
Fetch an article group by its URL slug within a specific help center. Returns null if no group matches.
# helpCenterIndex
Source: https://www.plain.com/docs/graphql-reference/queries/helpCenterIndex
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.
# helpCenters
Source: https://www.plain.com/docs/graphql-reference/queries/helpCenters
List all help centers in the workspace. Supports cursor-based pagination and optional filtering.
# importJobDefinition
Source: https://www.plain.com/docs/graphql-reference/queries/importJobDefinition
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.
# importJobs
Source: https://www.plain.com/docs/graphql-reference/queries/importJobs
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.
# importerTenantLists
Source: https://www.plain.com/docs/graphql-reference/queries/importerTenantLists
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.
# indexedDocuments
Source: https://www.plain.com/docs/graphql-reference/queries/indexedDocuments
List indexed documents across your workspace, newest first. Use the `knowledgeSourceId` filter to retrieve only the documents belonging to a specific knowledge source, and the `statuses` filter to retrieve only documents with a given ingestion status.
# issueTrackerFields
Source: https://www.plain.com/docs/graphql-reference/queries/issueTrackerFields
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.
# knowledgeGap
Source: https://www.plain.com/docs/graphql-reference/queries/knowledgeGap
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.
# knowledgeGaps
Source: https://www.plain.com/docs/graphql-reference/queries/knowledgeGaps
Paginated list of knowledge gaps detected in the workspace. Sort by signal count or recency. Defaults to descending signal count order. Requires the `knowledgeGap:read` permission.
# knowledgeSource
Source: https://www.plain.com/docs/graphql-reference/queries/knowledgeSource
Fetch a single knowledge source by its ID. Returns null if no knowledge source with the given ID exists.
# knowledgeSourceCitationsByThread
Source: https://www.plain.com/docs/graphql-reference/queries/knowledgeSourceCitationsByThread
Returns the knowledge sources cited by AI agent replies across a thread, each linked to the timeline entry it was cited in and carrying a snapshot title (and best-effort url) plus the live cited document when it still exists. Returns an empty list when the thread has no citations or is not visible to the caller. Requires the `thread:read` and `timeline:read` permissions.
# knowledgeSources
Source: https://www.plain.com/docs/graphql-reference/queries/knowledgeSources
List all knowledge sources in the workspace. Use the `type` filter to retrieve only sitemap or single-URL sources.
# labelType
Source: https://www.plain.com/docs/graphql-reference/queries/labelType
Returns a single label type by its ID. Returns null if no label type with that ID exists. Requires the `labelType:read` permission.
# labelTypeByExternalId
Source: https://www.plain.com/docs/graphql-reference/queries/labelTypeByExternalId
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.
# labelTypes
Source: https://www.plain.com/docs/graphql-reference/queries/labelTypes
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.
# linearAppIntegration
Source: https://www.plain.com/docs/graphql-reference/queries/linearAppIntegration
The workspace-level Linear app integration, if one has been authorised.
# machineUser
Source: https://www.plain.com/docs/graphql-reference/queries/machineUser
Fetches a single machine user by ID. Returns null if no machine user with the given ID exists in the workspace.
# machineUsers
Source: https://www.plain.com/docs/graphql-reference/queries/machineUsers
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).
# myBillingRota
Source: https://www.plain.com/docs/graphql-reference/queries/myBillingRota
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.
# myBillingSubscription
Source: https://www.plain.com/docs/graphql-reference/queries/myBillingSubscription
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.
# myEmailSignature
Source: https://www.plain.com/docs/graphql-reference/queries/myEmailSignature
Returns the email signature configured for the currently authenticated user, or null if none has been set.
# myFavoritePages
Source: https://www.plain.com/docs/graphql-reference/queries/myFavoritePages
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.
# myInternalNotifications
Source: https://www.plain.com/docs/graphql-reference/queries/myInternalNotifications
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.
# myJiraIntegrationToken
Source: https://www.plain.com/docs/graphql-reference/queries/myJiraIntegrationToken
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.
# myLinearInstallationInfo
Source: https://www.plain.com/docs/graphql-reference/queries/myLinearInstallationInfo
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.
# myLinearIntegration
Source: https://www.plain.com/docs/graphql-reference/queries/myLinearIntegration
Returns the current user's Linear integration, or null if no integration has been set up.
# myLinearIntegrationToken
Source: https://www.plain.com/docs/graphql-reference/queries/myLinearIntegrationToken
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.
# myMSTeamsInstallationInfo
Source: https://www.plain.com/docs/graphql-reference/queries/myMSTeamsInstallationInfo
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.
# myMSTeamsIntegration
Source: https://www.plain.com/docs/graphql-reference/queries/myMSTeamsIntegration
Returns the Microsoft Teams integration for the currently authenticated user, or null if the user has not connected their account.
# myMachineUser
Source: https://www.plain.com/docs/graphql-reference/queries/myMachineUser
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.
# myPermissions
Source: https://www.plain.com/docs/graphql-reference/queries/myPermissions
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.
# mySlackInstallationInfo
Source: https://www.plain.com/docs/graphql-reference/queries/mySlackInstallationInfo
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.
# mySlackIntegration
Source: https://www.plain.com/docs/graphql-reference/queries/mySlackIntegration
Returns the current user's personal Slack notifications integration, or null if none is connected.
# myUser
Source: https://www.plain.com/docs/graphql-reference/queries/myUser
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.
# myUserAccount
Source: https://www.plain.com/docs/graphql-reference/queries/myUserAccount
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.
# myWorkspace
Source: https://www.plain.com/docs/graphql-reference/queries/myWorkspace
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.
# myWorkspaceInvites
Source: https://www.plain.com/docs/graphql-reference/queries/myWorkspaceInvites
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.
# myWorkspaces
Source: https://www.plain.com/docs/graphql-reference/queries/myWorkspaces
Returns all workspaces the currently authenticated human user belongs to, paginated. Not available to machine users. Use this to let users switch between workspaces.
# permissions
Source: https://www.plain.com/docs/graphql-reference/queries/permissions
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.
# publicEventRequestBody
Source: https://www.plain.com/docs/graphql-reference/queries/publicEventRequestBody
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.
# relatedThreads
Source: https://www.plain.com/docs/graphql-reference/queries/relatedThreads
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.
# roles
Source: https://www.plain.com/docs/graphql-reference/queries/roles
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.
# savedThreadsView
Source: https://www.plain.com/docs/graphql-reference/queries/savedThreadsView
Fetches a single saved threads view by its ID. Returns null if no view with the given ID exists in the workspace.
# savedThreadsViews
Source: https://www.plain.com/docs/graphql-reference/queries/savedThreadsViews
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.
# searchBroadcasts
Source: https://www.plain.com/docs/graphql-reference/queries/searchBroadcasts
Searches broadcasts by name (case-insensitive partial match). Soft-deleted broadcasts
are excluded. Optionally narrow the results with `filters` (status), which applies on top of the
name match. Requires the `broadcast:read` permission.
# searchCompanies
Source: https://www.plain.com/docs/graphql-reference/queries/searchCompanies
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.
# searchCustomers
Source: https://www.plain.com/docs/graphql-reference/queries/searchCustomers
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. Optionally narrow the results with `filters`
(group membership, company, tenant, Slack channel and more). Requires the `customer:read` permission.
# searchKnowledgeSources
Source: https://www.plain.com/docs/graphql-reference/queries/searchKnowledgeSources
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.
# searchPlainHelpCenter
Source: https://www.plain.com/docs/graphql-reference/queries/searchPlainHelpCenter
Perform a semantic (vector) search across Plain's own public help center (help.plain.com).
Available to every workspace's Sidekick so it can answer questions about how Plain itself works.
Read-only and hard-restricted to Plain's public product documentation; it never returns the
caller's own data or any other workspace's content. Returns up to `pageSize` results
(default 10, max 50) ranked by relevance.
# searchSlackUsers
Source: https://www.plain.com/docs/graphql-reference/queries/searchSlackUsers
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.
# searchTenants
Source: https://www.plain.com/docs/graphql-reference/queries/searchTenants
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.
# searchThreadLinkCandidates
Source: https://www.plain.com/docs/graphql-reference/queries/searchThreadLinkCandidates
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.
# searchThreadSlackUsers
Source: https://www.plain.com/docs/graphql-reference/queries/searchThreadSlackUsers
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.
# searchThreads
Source: https://www.plain.com/docs/graphql-reference/queries/searchThreads
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.
# serviceAuthorization
Source: https://www.plain.com/docs/graphql-reference/queries/serviceAuthorization
Fetch a single completed service authorization by its ID. Returns null if not found or if the authorization is still pending.
# serviceAuthorizations
Source: https://www.plain.com/docs/graphql-reference/queries/serviceAuthorizations
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.
# serviceLevelAgreementPolicies
Source: https://www.plain.com/docs/graphql-reference/queries/serviceLevelAgreementPolicies
Returns the SLA policies in the workspace, sorted by name. Pass `filters` to narrow by name or by the target types a policy holds. Supports cursor-based pagination. Requires the `serviceLevelAgreement:read` permission.
# serviceLevelAgreementPolicy
Source: https://www.plain.com/docs/graphql-reference/queries/serviceLevelAgreementPolicy
Returns a single SLA policy by its Plain ID. Returns null if no policy with that ID exists. Requires the `serviceLevelAgreement:read` permission.
# setting
Source: https://www.plain.com/docs/graphql-reference/queries/setting
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.).
# sidekickCreditBalance
Source: https://www.plain.com/docs/graphql-reference/queries/sidekickCreditBalance
The workspace's current Sidekick (Plain) credit balance: the monthly allowance grant/used/remaining and when it resets, plus any purchased top-up remaining. Use it to answer how many credits are left. Requires the `billing:read` permission.
# sidekickCreditUsageByDay
Source: https://www.plain.com/docs/graphql-reference/queries/sidekickCreditUsageByDay
Credit usage for the workspace, bucketed by the UTC day each billable item resolved, over the given date range. Covers Sidekick sessions and work done by the workspace's own agents. Use it to show a per-day breakdown of credit consumption. Requires the `billing:read` permission.
# sidekickCustomSkill
Source: https://www.plain.com/docs/graphql-reference/queries/sidekickCustomSkill
A single workspace custom skill, including its instructions, for editing.
# sidekickGithubAccessibleRepos
Source: https://www.plain.com/docs/graphql-reference/queries/sidekickGithubAccessibleRepos
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.
# sidekickGithubServiceConfig
Source: https://www.plain.com/docs/graphql-reference/queries/sidekickGithubServiceConfig
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.
# sidekickMcpServer
Source: https://www.plain.com/docs/graphql-reference/queries/sidekickMcpServer
A single custom MCP server by id, or null if it does not exist in this workspace.
# sidekickMcpServers
Source: https://www.plain.com/docs/graphql-reference/queries/sidekickMcpServers
The custom (customer-owned) MCP servers registered for Sidekick in this workspace.
# sidekickPosthogServiceConfig
Source: https://www.plain.com/docs/graphql-reference/queries/sidekickPosthogServiceConfig
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.
# sidekickServiceConfig
Source: https://www.plain.com/docs/graphql-reference/queries/sidekickServiceConfig
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.
# sidekickSettings
Source: https://www.plain.com/docs/graphql-reference/queries/sidekickSettings
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.
# sidekickSkills
Source: https://www.plain.com/docs/graphql-reference/queries/sidekickSkills
Every skill available to Sidekick in this workspace: Plain's system skills
plus the workspace's custom skills, each with its effective enabled state.
# slackAutoJoinRules
Source: https://www.plain.com/docs/graphql-reference/queries/slackAutoJoinRules
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.
# slackUser
Source: https://www.plain.com/docs/graphql-reference/queries/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.
# snippet
Source: https://www.plain.com/docs/graphql-reference/queries/snippet
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.
# snippets
Source: https://www.plain.com/docs/graphql-reference/queries/snippets
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.
# subscriptionEventTypes
Source: https://www.plain.com/docs/graphql-reference/queries/subscriptionEventTypes
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.
# suggestedLabelTypes
Source: https://www.plain.com/docs/graphql-reference/queries/suggestedLabelTypes
Returns the PENDING AI-suggested label types awaiting review, most recently created first. Requires the `suggestedLabelType:read` permission.
# suggestedSlackTeammates
Source: https://www.plain.com/docs/graphql-reference/queries/suggestedSlackTeammates
Suggests teammates to invite to Plain, sourced from the members of the
workspace's connected Slack discussion channels. Only members of the
workspace's own Slack team are included (Slack Connect guests are excluded),
and anyone (matched by email, case-insensitive) who is already a member of
this workspace or has a pending invite to it is excluded. Returns an empty
list when there are no connected, enabled discussion channels for the
given slackTeamId (e.g. a Slack Connect channel integration).
# task
Source: https://www.plain.com/docs/graphql-reference/queries/task
Fetch a single task by its ID. Returns null if no task with that ID exists. Requires the `task:read` permission.
# taskByRef
Source: https://www.plain.com/docs/graphql-reference/queries/taskByRef
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.
# tasks
Source: https://www.plain.com/docs/graphql-reference/queries/tasks
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.
# teamSettings
Source: https://www.plain.com/docs/graphql-reference/queries/teamSettings
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.
# tenant
Source: https://www.plain.com/docs/graphql-reference/queries/tenant
Fetches a single tenant by its Plain-assigned ID. Returns null if no matching tenant is found. Requires the `tenant:read` permission.
# tenantFieldSchemas
Source: https://www.plain.com/docs/graphql-reference/queries/tenantFieldSchemas
Returns the list of tenant field schemas defined in the workspace. Use the `source` and `isVisible` filters to narrow results. Supports cursor-based pagination.
# tenants
Source: https://www.plain.com/docs/graphql-reference/queries/tenants
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.
# thread
Source: https://www.plain.com/docs/graphql-reference/queries/thread
Fetch a single thread by its Plain-assigned ID. Returns null if no thread with that ID exists. Requires the `thread:read` permission.
# threadByExternalId
Source: https://www.plain.com/docs/graphql-reference/queries/threadByExternalId
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.
# threadByRef
Source: https://www.plain.com/docs/graphql-reference/queries/threadByRef
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.
# threadBySlackPermalink
Source: https://www.plain.com/docs/graphql-reference/queries/threadBySlackPermalink
Resolves a Slack message permalink to the Plain thread it belongs to. Returns null if no thread is associated with the given Slack message.
# threadCluster
Source: https://www.plain.com/docs/graphql-reference/queries/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.
# threadClusters
Source: https://www.plain.com/docs/graphql-reference/queries/threadClusters
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.
# threadClustersPaginated
Source: https://www.plain.com/docs/graphql-reference/queries/threadClustersPaginated
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.
# threadDiscussion
Source: https://www.plain.com/docs/graphql-reference/queries/threadDiscussion
Fetch a single discussion by its ID. Returns null if no discussion with the given ID exists.
# threadFieldSchema
Source: https://www.plain.com/docs/graphql-reference/queries/threadFieldSchema
Fetches a single thread field schema by its ID. Returns null if no schema with the given ID exists. Selecting the `createdBy`/`updatedBy` actor down to its `user` additionally requires the `user:read` permission.
# threadFieldSchemas
Source: https://www.plain.com/docs/graphql-reference/queries/threadFieldSchemas
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. Selecting the `createdBy`/`updatedBy` actor down to its `user` additionally requires the `user:read` permission; a token without it can query an empty workspace but starts failing once a schema exists and that selection is present.
# threadHeatmapMetric
Source: https://www.plain.com/docs/graphql-reference/queries/threadHeatmapMetric
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`; filtering `assignedToUser` to anyone other than the caller
additionally requires `metricsAgent:read`.
# threadLinkGroups
Source: https://www.plain.com/docs/graphql-reference/queries/threadLinkGroups
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.
# threadSingleValueMetric
Source: https://www.plain.com/docs/graphql-reference/queries/threadSingleValueMetric
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`; grouping by assignee or filtering `assignedToUser` to anyone
other than the caller additionally requires `metricsAgent:read`.
# threadSlackUser
Source: https://www.plain.com/docs/graphql-reference/queries/threadSlackUser
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.
# threadTimeSeriesMetric
Source: https://www.plain.com/docs/graphql-reference/queries/threadTimeSeriesMetric
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`; grouping by assignee or filtering
`assignedToUser` to anyone other than the caller additionally requires `metricsAgent:read`.
# threads
Source: https://www.plain.com/docs/graphql-reference/queries/threads
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.
# threadsByExternalId
Source: https://www.plain.com/docs/graphql-reference/queries/threadsByExternalId
Fetch all threads that share the given external ID, returned as a paginated connection. Unlike `threadByExternalId`, this is not scoped to a customer: because `externalId` is only unique per-customer, the same external ID can appear on threads belonging to different customers, and this query returns all of them. Returns an empty connection if there are no matches. Requires the `thread:read` permission.
# tier
Source: https://www.plain.com/docs/graphql-reference/queries/tier
Returns a single tier by its Plain ID. Returns null if no tier with that ID exists. Requires the `tier:read` permission.
# tiers
Source: https://www.plain.com/docs/graphql-reference/queries/tiers
Returns all tiers in the workspace, sorted by creation date. Supports cursor-based pagination. Requires the `tier:read` permission.
# timelineEntries
Source: https://www.plain.com/docs/graphql-reference/queries/timelineEntries
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.
# timelineEntry
Source: https://www.plain.com/docs/graphql-reference/queries/timelineEntry
Fetches a single timeline entry by its ID within a customer's timeline. Returns null if the entry does not exist.
# user
Source: https://www.plain.com/docs/graphql-reference/queries/user
Fetch a single workspace member by their ID. Returns null if no user with that ID exists. Requires the `user:read` permission.
# userAuthDiscordChannelInstallationInfo
Source: https://www.plain.com/docs/graphql-reference/queries/userAuthDiscordChannelInstallationInfo
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`.
# userAuthDiscordChannelIntegration
Source: https://www.plain.com/docs/graphql-reference/queries/userAuthDiscordChannelIntegration
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.
# userAuthDiscordChannelIntegrations
Source: https://www.plain.com/docs/graphql-reference/queries/userAuthDiscordChannelIntegrations
Returns a paginated list of all personal Discord authentication integrations for the current user across all guilds.
# userAuthSlackInstallationInfo
Source: https://www.plain.com/docs/graphql-reference/queries/userAuthSlackInstallationInfo
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.
# userAuthSlackIntegration
Source: https://www.plain.com/docs/graphql-reference/queries/userAuthSlackIntegration
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.
# userAuthSlackIntegrationByThreadId
Source: https://www.plain.com/docs/graphql-reference/queries/userAuthSlackIntegrationByThreadId
Returns the current user's user-auth Slack integration for the Slack workspace associated with the given thread, or null if not connected.
# userByEmail
Source: https://www.plain.com/docs/graphql-reference/queries/userByEmail
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.
# userSlackChannelMemberships
Source: https://www.plain.com/docs/graphql-reference/queries/userSlackChannelMemberships
Returns the Slack channels the current user belongs to within the given Slack workspace. Useful for populating channel pickers in UI integrations.
# users
Source: https://www.plain.com/docs/graphql-reference/queries/users
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.
# webhookDeliveryAttempts
Source: https://www.plain.com/docs/graphql-reference/queries/webhookDeliveryAttempts
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.
# webhookTarget
Source: https://www.plain.com/docs/graphql-reference/queries/webhookTarget
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.
# webhookTargets
Source: https://www.plain.com/docs/graphql-reference/queries/webhookTargets
List all webhook targets registered in the workspace, paginated. Returns every endpoint Plain is configured to deliver events to. Requires the `webhookTarget:read` permission.
# webhookVersions
Source: https://www.plain.com/docs/graphql-reference/queries/webhookVersions
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.
# workOSConfiguration
Source: https://www.plain.com/docs/graphql-reference/queries/workOSConfiguration
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.
# workflow
Source: https://www.plain.com/docs/graphql-reference/queries/workflow
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.
# workflowCapabilities
Source: https://www.plain.com/docs/graphql-reference/queries/workflowCapabilities
Returns which blocks (condition/action/wait steps) are available for a workflow of the given
trigger type. The builder UI sources its palette from this so the allow-list lives in the
backend, not hardcoded in the UI. E.g. SCHEDULE workflows have no thread, so they allow no
conditions/waits and only a restricted set of action types.
# workflowExecution
Source: https://www.plain.com/docs/graphql-reference/queries/workflowExecution
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.
# workflowExecutions
Source: https://www.plain.com/docs/graphql-reference/queries/workflowExecutions
List executions for a specific workflow, paginated. Use this to audit the run history of a workflow and check for failures.
# workflowExecutionsByEntity
Source: https://www.plain.com/docs/graphql-reference/queries/workflowExecutionsByEntity
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.
# workflowExecutionsForWorkspace
Source: https://www.plain.com/docs/graphql-reference/queries/workflowExecutionsForWorkspace
List executions across every workflow in the workspace, newest first, paginated. Use this for a
workspace-wide view of what automation has been doing, or narrow it with `filters` — by workflow,
by the entity a run fired against, or by state. Executions that have not started yet sort to the top.
# workflowRule
Source: https://www.plain.com/docs/graphql-reference/queries/workflowRule
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.
# workflowRules
Source: https://www.plain.com/docs/graphql-reference/queries/workflowRules
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.
# workflowShareLink
Source: https://www.plain.com/docs/graphql-reference/queries/workflowShareLink
Fetch a workflow share link by its token. Returns null if no share link with the given token exists.
# workflowTemplate
Source: https://www.plain.com/docs/graphql-reference/queries/workflowTemplate
Null if no template with that ID is published. Deprecated templates still resolve here, which is
what keeps existing install links working.
Needs an authenticated caller and nothing more.
# workflowTemplateGallery
Source: https://www.plain.com/docs/graphql-reference/queries/workflowTemplateGallery
Published templates for the gallery, metadata only — deprecated ones are already absent because the
templates repo omits them. Use `workflowTemplate` for one with its workflows.
Needs an authenticated caller and nothing more: templates are one published document, identical
for every workspace, so there is nothing to scope by permission.
# workflows
Source: https://www.plain.com/docs/graphql-reference/queries/workflows
List workflows in the workspace, paginated. Use `filters` to narrow results by trigger type
(MANUAL, EVENTS or SCHEDULE), published status, or the events an `events` trigger listens for.
Ordered by `position` ascending unless `sortBy` says otherwise.
# workspace
Source: https://www.plain.com/docs/graphql-reference/queries/workspace
Fetch a workspace by its ID. Returns null if the workspace does not exist or the caller does not have access to it.
# workspaceChatSettings
Source: https://www.plain.com/docs/graphql-reference/queries/workspaceChatSettings
Returns the workspace-level chat settings, including whether the live-chat channel is enabled for this workspace.
# workspaceCursorIntegration
Source: https://www.plain.com/docs/graphql-reference/queries/workspaceCursorIntegration
Returns the workspace's Cursor integration, or null if none has been configured.
# workspaceDiscordChannelInstallationInfo
Source: https://www.plain.com/docs/graphql-reference/queries/workspaceDiscordChannelInstallationInfo
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`.
# workspaceDiscordChannelIntegration
Source: https://www.plain.com/docs/graphql-reference/queries/workspaceDiscordChannelIntegration
Fetches a single workspace Discord channel integration by its ID. Returns null if no integration with that ID exists.
# workspaceDiscordChannelIntegrations
Source: https://www.plain.com/docs/graphql-reference/queries/workspaceDiscordChannelIntegrations
Returns a paginated list of all Discord server (guild) channel integrations connected to this workspace.
# workspaceDiscordIntegration
Source: https://www.plain.com/docs/graphql-reference/queries/workspaceDiscordIntegration
Fetches a single webhook-based workspace Discord integration by its ID. Returns null if not found.
# workspaceDiscordIntegrations
Source: https://www.plain.com/docs/graphql-reference/queries/workspaceDiscordIntegrations
Returns a paginated list of all webhook-based Discord integrations configured for this workspace.
# workspaceEmailSettings
Source: https://www.plain.com/docs/graphql-reference/queries/workspaceEmailSettings
Returns the workspace's email settings, including whether email is enabled, any custom domain configuration, and BCC addresses.
# workspaceHmac
Source: https://www.plain.com/docs/graphql-reference/queries/workspaceHmac
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.
# workspaceInvites
Source: https://www.plain.com/docs/graphql-reference/queries/workspaceInvites
Returns all pending invites for the current workspace. Use this to view outstanding invitations and their status from an admin perspective.
# workspaceLinearInstallationInfo
Source: https://www.plain.com/docs/graphql-reference/queries/workspaceLinearInstallationInfo
Builds the Linear OAuth installation URL for the app actor flow (actor=app).
# workspaceMSTeamsInstallationInfo
Source: https://www.plain.com/docs/graphql-reference/queries/workspaceMSTeamsInstallationInfo
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.
# workspaceMSTeamsIntegration
Source: https://www.plain.com/docs/graphql-reference/queries/workspaceMSTeamsIntegration
Returns the workspace-level Microsoft Teams integration, or null if none has been created yet.
# workspaceSlackChannelInstallationInfo
Source: https://www.plain.com/docs/graphql-reference/queries/workspaceSlackChannelInstallationInfo
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.
# workspaceSlackChannelIntegration
Source: https://www.plain.com/docs/graphql-reference/queries/workspaceSlackChannelIntegration
Returns a single workspace Slack channel integration by ID, or null if not found.
# workspaceSlackChannelIntegrations
Source: https://www.plain.com/docs/graphql-reference/queries/workspaceSlackChannelIntegrations
Returns a paginated list of all workspace Slack channel integrations for this workspace.
# workspaceSlackInstallationInfo
Source: https://www.plain.com/docs/graphql-reference/queries/workspaceSlackInstallationInfo
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.
# workspaceSlackIntegration
Source: https://www.plain.com/docs/graphql-reference/queries/workspaceSlackIntegration
Returns a single workspace-level Slack notifications integration by ID, or null if not found.
# workspaceSlackIntegrations
Source: https://www.plain.com/docs/graphql-reference/queries/workspaceSlackIntegrations
Returns a paginated list of all workspace-level Slack notifications integrations for this workspace.
# workspaceSlackSidekickInstallationInfo
Source: https://www.plain.com/docs/graphql-reference/queries/workspaceSlackSidekickInstallationInfo
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.
# workspaceSlackSidekickIntegration
Source: https://www.plain.com/docs/graphql-reference/queries/workspaceSlackSidekickIntegration
Returns the current workspace's Slack Sidekick integration, or null if Sidekick has not been installed. Requires the workspaceSlackSidekickIntegration:read permission.
# AcceptSuggestedLabelTypesInput
Source: https://www.plain.com/docs/graphql-reference/types/AcceptSuggestedLabelTypesInput
# AcceptSuggestedLabelTypesOutput
Source: https://www.plain.com/docs/graphql-reference/types/AcceptSuggestedLabelTypesOutput
# AcceptWorkspaceInviteInput
Source: https://www.plain.com/docs/graphql-reference/types/AcceptWorkspaceInviteInput
# AcceptWorkspaceInviteOutput
Source: https://www.plain.com/docs/graphql-reference/types/AcceptWorkspaceInviteOutput
# Actor
Source: https://www.plain.com/docs/graphql-reference/types/Actor
# ActorConnection
Source: https://www.plain.com/docs/graphql-reference/types/ActorConnection
# ActorEdge
Source: https://www.plain.com/docs/graphql-reference/types/ActorEdge
# AddAdditionalAssigneesInput
Source: https://www.plain.com/docs/graphql-reference/types/AddAdditionalAssigneesInput
# AddAdditionalAssigneesOutput
Source: https://www.plain.com/docs/graphql-reference/types/AddAdditionalAssigneesOutput
# AddCustomerToCustomerGroupsInput
Source: https://www.plain.com/docs/graphql-reference/types/AddCustomerToCustomerGroupsInput
# AddCustomerToCustomerGroupsOutput
Source: https://www.plain.com/docs/graphql-reference/types/AddCustomerToCustomerGroupsOutput
# AddCustomerToTenantsInput
Source: https://www.plain.com/docs/graphql-reference/types/AddCustomerToTenantsInput
# AddCustomerToTenantsOutput
Source: https://www.plain.com/docs/graphql-reference/types/AddCustomerToTenantsOutput
# AddGeneratedReplyInput
Source: https://www.plain.com/docs/graphql-reference/types/AddGeneratedReplyInput
# AddGeneratedReplyOutput
Source: https://www.plain.com/docs/graphql-reference/types/AddGeneratedReplyOutput
# AddLabelsInput
Source: https://www.plain.com/docs/graphql-reference/types/AddLabelsInput
# AddLabelsOutput
Source: https://www.plain.com/docs/graphql-reference/types/AddLabelsOutput
# AddLabelsToUserInput
Source: https://www.plain.com/docs/graphql-reference/types/AddLabelsToUserInput
# AddLabelsToUserOutput
Source: https://www.plain.com/docs/graphql-reference/types/AddLabelsToUserOutput
# AddMembersToTierInput
Source: https://www.plain.com/docs/graphql-reference/types/AddMembersToTierInput
# AddMembersToTierOutput
Source: https://www.plain.com/docs/graphql-reference/types/AddMembersToTierOutput
# AddUserToActiveBillingRotaInput
Source: https://www.plain.com/docs/graphql-reference/types/AddUserToActiveBillingRotaInput
# AddUserToActiveBillingRotaOutput
Source: https://www.plain.com/docs/graphql-reference/types/AddUserToActiveBillingRotaOutput
# AddWorkspaceAlternateSupportEmailAddressInput
Source: https://www.plain.com/docs/graphql-reference/types/AddWorkspaceAlternateSupportEmailAddressInput
# AddWorkspaceAlternateSupportEmailAddressOutput
Source: https://www.plain.com/docs/graphql-reference/types/AddWorkspaceAlternateSupportEmailAddressOutput
# AgentApprovalDecision
Source: https://www.plain.com/docs/graphql-reference/types/AgentApprovalDecision
Subset of AgentApprovalStatus valid as a resolution input (excludes PENDING).
# AgentApprovalStatus
Source: https://www.plain.com/docs/graphql-reference/types/AgentApprovalStatus
# AgentSandboxToolMode
Source: https://www.plain.com/docs/graphql-reference/types/AgentSandboxToolMode
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.
# AgentSandboxToolPolicy
Source: https://www.plain.com/docs/graphql-reference/types/AgentSandboxToolPolicy
The resolved approval policy for one Sidekick tool: its effective mode plus the
factory default it derives from.
# AgentStatus
Source: https://www.plain.com/docs/graphql-reference/types/AgentStatus
# AgentStatusDetail
Source: https://www.plain.com/docs/graphql-reference/types/AgentStatusDetail
# AgentStatusDetailHandedOff
Source: https://www.plain.com/docs/graphql-reference/types/AgentStatusDetailHandedOff
# AgentStatusDetailHandled
Source: https://www.plain.com/docs/graphql-reference/types/AgentStatusDetailHandled
# AgentStatusDetailInProgress
Source: https://www.plain.com/docs/graphql-reference/types/AgentStatusDetailInProgress
# AgentStatusFilter
Source: https://www.plain.com/docs/graphql-reference/types/AgentStatusFilter
# AiAgentFeedbackDetails
Source: https://www.plain.com/docs/graphql-reference/types/AiAgentFeedbackDetails
# AiFeatureFeedbackDetails
Source: https://www.plain.com/docs/graphql-reference/types/AiFeatureFeedbackDetails
# AiFeedback
Source: https://www.plain.com/docs/graphql-reference/types/AiFeedback
A single piece of feedback a teammate submitted on a Plain AI feature result.
# AiFeedbackConnection
Source: https://www.plain.com/docs/graphql-reference/types/AiFeedbackConnection
# AiFeedbackEdge
Source: https://www.plain.com/docs/graphql-reference/types/AiFeedbackEdge
# AiFeedbackFeature
Source: https://www.plain.com/docs/graphql-reference/types/AiFeedbackFeature
The Plain AI feature a piece of feedback relates to.
# AiFeedbackFilter
Source: https://www.plain.com/docs/graphql-reference/types/AiFeedbackFilter
# AiFeedbackSentiment
Source: https://www.plain.com/docs/graphql-reference/types/AiFeedbackSentiment
# AiToneRule
Source: https://www.plain.com/docs/graphql-reference/types/AiToneRule
# AiToneRuleConnection
Source: https://www.plain.com/docs/graphql-reference/types/AiToneRuleConnection
# AiToneRuleEdge
Source: https://www.plain.com/docs/graphql-reference/types/AiToneRuleEdge
# AiToneRuleUpdate
Source: https://www.plain.com/docs/graphql-reference/types/AiToneRuleUpdate
# AiToneRulesFilter
Source: https://www.plain.com/docs/graphql-reference/types/AiToneRulesFilter
# ApiKey
Source: https://www.plain.com/docs/graphql-reference/types/ApiKey
# ApiKeyConnection
Source: https://www.plain.com/docs/graphql-reference/types/ApiKeyConnection
# ApiKeyEdge
Source: https://www.plain.com/docs/graphql-reference/types/ApiKeyEdge
# ApiKeyImpersonationAllowList
Source: https://www.plain.com/docs/graphql-reference/types/ApiKeyImpersonationAllowList
The workspace users, roles and custom roles an API key may reply as.
# ApiKeyImpersonationAllowListInput
Source: https://www.plain.com/docs/graphql-reference/types/ApiKeyImpersonationAllowListInput
# ArchiveLabelTypeInput
Source: https://www.plain.com/docs/graphql-reference/types/ArchiveLabelTypeInput
# ArchiveLabelTypeOutput
Source: https://www.plain.com/docs/graphql-reference/types/ArchiveLabelTypeOutput
# AriKnowledgeGapSignal
Source: https://www.plain.com/docs/graphql-reference/types/AriKnowledgeGapSignal
# AssignRolesToUserInput
Source: https://www.plain.com/docs/graphql-reference/types/AssignRolesToUserInput
# AssignRolesToUserOutput
Source: https://www.plain.com/docs/graphql-reference/types/AssignRolesToUserOutput
# AssignThreadInput
Source: https://www.plain.com/docs/graphql-reference/types/AssignThreadInput
# AssignThreadOutput
Source: https://www.plain.com/docs/graphql-reference/types/AssignThreadOutput
# Attachment
Source: https://www.plain.com/docs/graphql-reference/types/Attachment
# AttachmentDownloadUrl
Source: https://www.plain.com/docs/graphql-reference/types/AttachmentDownloadUrl
# AttachmentType
Source: https://www.plain.com/docs/graphql-reference/types/AttachmentType
# AttachmentUploadUrl
Source: https://www.plain.com/docs/graphql-reference/types/AttachmentUploadUrl
# AttachmentVirusScanResult
Source: https://www.plain.com/docs/graphql-reference/types/AttachmentVirusScanResult
# Autoresponder
Source: https://www.plain.com/docs/graphql-reference/types/Autoresponder
# AutoresponderBusinessHoursCondition
Source: https://www.plain.com/docs/graphql-reference/types/AutoresponderBusinessHoursCondition
Condition that matches threads based on whether the thread was created outside configured business hours.
# AutoresponderCondition
Source: https://www.plain.com/docs/graphql-reference/types/AutoresponderCondition
# AutoresponderConditionInput
Source: https://www.plain.com/docs/graphql-reference/types/AutoresponderConditionInput
# AutoresponderConnection
Source: https://www.plain.com/docs/graphql-reference/types/AutoresponderConnection
# AutoresponderEdge
Source: https://www.plain.com/docs/graphql-reference/types/AutoresponderEdge
# AutoresponderLabelCondition
Source: https://www.plain.com/docs/graphql-reference/types/AutoresponderLabelCondition
Condition that matches threads that have at least one of the specified labels applied.
# AutoresponderMessageSource
Source: https://www.plain.com/docs/graphql-reference/types/AutoresponderMessageSource
The channel through which an inbound thread was created. Used to restrict which autoresponders are eligible to fire.
# AutoresponderOrderInput
Source: https://www.plain.com/docs/graphql-reference/types/AutoresponderOrderInput
# AutoresponderPrioritiesCondition
Source: https://www.plain.com/docs/graphql-reference/types/AutoresponderPrioritiesCondition
Condition that matches threads at one of the specified priority levels.
# AutoresponderSupportEmailsCondition
Source: https://www.plain.com/docs/graphql-reference/types/AutoresponderSupportEmailsCondition
Condition that restricts the autoresponder to threads received on specific support email addresses.
# AutoresponderTierCondition
Source: https://www.plain.com/docs/graphql-reference/types/AutoresponderTierCondition
Condition that matches threads belonging to a specific tier.
# BeforeBreachAction
Source: https://www.plain.com/docs/graphql-reference/types/BeforeBreachAction
A breach action that triggers a notification a set number of minutes before the SLA deadline is reached.
# BeforeBreachActionInput
Source: https://www.plain.com/docs/graphql-reference/types/BeforeBreachActionInput
# BillingCreditBalance
Source: https://www.plain.com/docs/graphql-reference/types/BillingCreditBalance
# BillingCreditBalanceType
Source: https://www.plain.com/docs/graphql-reference/types/BillingCreditBalanceType
# BillingFeatureEntitlement
Source: https://www.plain.com/docs/graphql-reference/types/BillingFeatureEntitlement
# BillingInterval
Source: https://www.plain.com/docs/graphql-reference/types/BillingInterval
# BillingIntervalUnit
Source: https://www.plain.com/docs/graphql-reference/types/BillingIntervalUnit
# BillingMonthlyAllowanceCreditBalance
Source: https://www.plain.com/docs/graphql-reference/types/BillingMonthlyAllowanceCreditBalance
# BillingPlan
Source: https://www.plain.com/docs/graphql-reference/types/BillingPlan
# BillingPlanChangePreview
Source: https://www.plain.com/docs/graphql-reference/types/BillingPlanChangePreview
# BillingPlanConnection
Source: https://www.plain.com/docs/graphql-reference/types/BillingPlanConnection
# BillingPlanEdge
Source: https://www.plain.com/docs/graphql-reference/types/BillingPlanEdge
# BillingPlanKey
Source: https://www.plain.com/docs/graphql-reference/types/BillingPlanKey
# BillingRota
Source: https://www.plain.com/docs/graphql-reference/types/BillingRota
# BillingSeatType
Source: https://www.plain.com/docs/graphql-reference/types/BillingSeatType
# BillingSubscription
Source: https://www.plain.com/docs/graphql-reference/types/BillingSubscription
# BillingSubscriptionStatus
Source: https://www.plain.com/docs/graphql-reference/types/BillingSubscriptionStatus
# BillingTopupCreditBalance
Source: https://www.plain.com/docs/graphql-reference/types/BillingTopupCreditBalance
# BooleanInput
Source: https://www.plain.com/docs/graphql-reference/types/BooleanInput
# BooleanSetting
Source: https://www.plain.com/docs/graphql-reference/types/BooleanSetting
A boolean setting
# BreachAction
Source: https://www.plain.com/docs/graphql-reference/types/BreachAction
# BreachActionInput
Source: https://www.plain.com/docs/graphql-reference/types/BreachActionInput
# Broadcast
Source: https://www.plain.com/docs/graphql-reference/types/Broadcast
The authored content of a broadcast, before it is sent to any channel.
# BroadcastAudience
Source: https://www.plain.com/docs/graphql-reference/types/BroadcastAudience
A named, reusable set of broadcast recipients, resolved to concrete channels at send time.
# BroadcastAudienceConnection
Source: https://www.plain.com/docs/graphql-reference/types/BroadcastAudienceConnection
# BroadcastAudienceEdge
Source: https://www.plain.com/docs/graphql-reference/types/BroadcastAudienceEdge
# BroadcastAudienceFilter
Source: https://www.plain.com/docs/graphql-reference/types/BroadcastAudienceFilter
What a saved audience selects, and what a broadcast send target reaches. A tree: `and`, `or` and
`not` hold more of this same type, ANDed with the dimensions beside them. Dimensions on one node AND
together, values within a dimension OR. An empty dimension does not constrain the audience and cannot be
written — omit it instead.
# BroadcastAudienceFilterInput
Source: https://www.plain.com/docs/graphql-reference/types/BroadcastAudienceFilterInput
# BroadcastAudienceTenantFieldFilter
Source: https://www.plain.com/docs/graphql-reference/types/BroadcastAudienceTenantFieldFilter
Matches a tenant field against one value. Exactly one value field must be set.
# BroadcastAudienceTenantFieldFilterInput
Source: https://www.plain.com/docs/graphql-reference/types/BroadcastAudienceTenantFieldFilterInput
# BroadcastConnection
Source: https://www.plain.com/docs/graphql-reference/types/BroadcastConnection
# BroadcastContentFormat
Source: https://www.plain.com/docs/graphql-reference/types/BroadcastContentFormat
How the `content` of a broadcast is encoded.
# BroadcastContentFormatInput
Source: https://www.plain.com/docs/graphql-reference/types/BroadcastContentFormatInput
# BroadcastEdge
Source: https://www.plain.com/docs/graphql-reference/types/BroadcastEdge
# BroadcastReactionCount
Source: https://www.plain.com/docs/graphql-reference/types/BroadcastReactionCount
One emoji and how many times it was used on a broadcast's messages.
# BroadcastRecipientType
Source: https://www.plain.com/docs/graphql-reference/types/BroadcastRecipientType
The channel a recipient is reached on. A broadcast's own `type` decides which of these its target may name.
# BroadcastSend
Source: https://www.plain.com/docs/graphql-reference/types/BroadcastSend
One run of a broadcast: the recipients it resolved to and what became of each of them. A broadcast's
`status`, `scheduledAt`, `startedAt` and `completedAt` come from its most recent send, which is why
they read as null or `DRAFT` on a broadcast that has none.
# BroadcastSendConnection
Source: https://www.plain.com/docs/graphql-reference/types/BroadcastSendConnection
# BroadcastSendDeliveriesFilter
Source: https://www.plain.com/docs/graphql-reference/types/BroadcastSendDeliveriesFilter
Narrows a send's delivery list. Omit a field to leave that dimension unconstrained.
# BroadcastSendDelivery
Source: https://www.plain.com/docs/graphql-reference/types/BroadcastSendDelivery
One recipient's copy of a broadcast, and what became of it.
# BroadcastSendDeliveryConnection
Source: https://www.plain.com/docs/graphql-reference/types/BroadcastSendDeliveryConnection
# BroadcastSendDeliveryCounts
Source: https://www.plain.com/docs/graphql-reference/types/BroadcastSendDeliveryCounts
How many of a send's recipients are in each state. Read as one snapshot: the counts are taken by a
single query, so they always sum to `total`.
# BroadcastSendDeliveryEdge
Source: https://www.plain.com/docs/graphql-reference/types/BroadcastSendDeliveryEdge
# BroadcastSendDeliveryFailureReason
Source: https://www.plain.com/docs/graphql-reference/types/BroadcastSendDeliveryFailureReason
Why a delivery failed. Terminal in every case — a failure that was worth retrying was retried before
the delivery reached `FAILED`.
# BroadcastSendDeliveryRecipient
Source: https://www.plain.com/docs/graphql-reference/types/BroadcastSendDeliveryRecipient
Who one delivery was addressed to. The variant matches the channel the broadcast was authored for.
Slack channels are identified rather than named — resolve the name through `connectedSlackChannels`,
bearing in mind that a `CHANNEL_NOT_CONNECTED` failure means there is no channel there to name.
# BroadcastSendDeliveryStatus
Source: https://www.plain.com/docs/graphql-reference/types/BroadcastSendDeliveryStatus
Where one recipient's copy of a broadcast is in its lifecycle.
# BroadcastSendEdge
Source: https://www.plain.com/docs/graphql-reference/types/BroadcastSendEdge
# BroadcastSendStatus
Source: https://www.plain.com/docs/graphql-reference/types/BroadcastSendStatus
Where one run of a broadcast is in its lifecycle. The same values `BroadcastStatus` carries apart
from `DRAFT`, which describes a broadcast with no send rather than a send.
# BroadcastSendTarget
Source: https://www.plain.com/docs/graphql-reference/types/BroadcastSendTarget
# BroadcastSendTargetInput
Source: https://www.plain.com/docs/graphql-reference/types/BroadcastSendTargetInput
# BroadcastSendTargetRecipient
Source: https://www.plain.com/docs/graphql-reference/types/BroadcastSendTargetRecipient
Someone a broadcast is aimed at by name rather than by filter.
Flat with a `type` rather than a union, because GraphQL has no input unions and the input and output
shapes should not diverge. The per-channel fields are nullable for the same reason: an email recipient
will carry an address and no channel, and making them nullable now is what keeps that from being a
breaking change later. Read `type` first.
# BroadcastSendTargetRecipientConnection
Source: https://www.plain.com/docs/graphql-reference/types/BroadcastSendTargetRecipientConnection
Its own connection rather than `ConnectedSlackChannelConnection`, which carries the filters it was
queried by. An audience with `and`/`or`/`not` is not expressible as one channel filter, so there is
no honest value to put there.
The edges are Slack channels because a broadcast can only be authored for Slack today. A second
broadcast type widens the node rather than adding a second connection.
# BroadcastSendTargetRecipientInput
Source: https://www.plain.com/docs/graphql-reference/types/BroadcastSendTargetRecipientInput
Mirrors `BroadcastSendTargetRecipient`. Supply the fields belonging to the `type` given.
# BroadcastSendTargetRecipients
Source: https://www.plain.com/docs/graphql-reference/types/BroadcastSendTargetRecipients
How many recipients a target resolves to right now. Point-in-time: it moves as tenants, tiers and
tenant fields change, and as channels are connected or disabled, so it is not a promise about who
will receive a send made later.
Resolved through the same path a send uses, so this is the number that would actually be delivered
to rather than an estimate assembled another way.
# BroadcastSendTargetRecipientsEmptyReason
Source: https://www.plain.com/docs/graphql-reference/types/BroadcastSendTargetRecipientsEmptyReason
Why a broadcast or audience currently resolves to nobody.
# BroadcastSendTargetRecipientsInput
Source: https://www.plain.com/docs/graphql-reference/types/BroadcastSendTargetRecipientsInput
# BroadcastSendTargetScope
Source: https://www.plain.com/docs/graphql-reference/types/BroadcastSendTargetScope
# BroadcastSender
Source: https://www.plain.com/docs/graphql-reference/types/BroadcastSender
Who a broadcast is sent as. Which sender a broadcast carries is decided by the
channel it is authored for, so each channel contributes its own variant.
# BroadcastSenderType
Source: https://www.plain.com/docs/graphql-reference/types/BroadcastSenderType
Who a broadcast is sent as. Set alongside the matching id on input: `PLAIN_USER` requires
`senderUserId`, `PLAIN_WORKSPACE` takes none.
# BroadcastSenderTypeInput
Source: https://www.plain.com/docs/graphql-reference/types/BroadcastSenderTypeInput
Nullable, unlike `BroadcastContentFormatInput`: clearing the sender is how a broadcast goes back to having none.
# BroadcastSendsFilter
Source: https://www.plain.com/docs/graphql-reference/types/BroadcastSendsFilter
Narrows a broadcast's send list. Omit a field to leave that dimension unconstrained.
# BroadcastSlackChannelRef
Source: https://www.plain.com/docs/graphql-reference/types/BroadcastSlackChannelRef
A Slack channel. A channel id is only unique within its team, so both are needed to name one.
# BroadcastSlackChannelRefInput
Source: https://www.plain.com/docs/graphql-reference/types/BroadcastSlackChannelRefInput
# BroadcastStatus
Source: https://www.plain.com/docs/graphql-reference/types/BroadcastStatus
Where a broadcast is in its lifecycle. Derived from the broadcast's latest real send, so it is
never stored on the broadcast itself. Test sends do not affect it.
# BroadcastType
Source: https://www.plain.com/docs/graphql-reference/types/BroadcastType
The channel a broadcast is authored for. Content is written against a specific
channel's rendering rules, so it is not portable between channels.
# BroadcastsFilter
Source: https://www.plain.com/docs/graphql-reference/types/BroadcastsFilter
Narrows a list of broadcasts. Omit a field to leave that dimension unconstrained.
# BroadcastsSearchQuery
Source: https://www.plain.com/docs/graphql-reference/types/BroadcastsSearchQuery
Query to search for broadcasts.
# BulkJoinSlackChannelsInput
Source: https://www.plain.com/docs/graphql-reference/types/BulkJoinSlackChannelsInput
# BulkJoinSlackChannelsOutput
Source: https://www.plain.com/docs/graphql-reference/types/BulkJoinSlackChannelsOutput
# BulkUpdateConnectedSlackChannelItem
Source: https://www.plain.com/docs/graphql-reference/types/BulkUpdateConnectedSlackChannelItem
A single update applied by the `bulkUpdateConnectedSlackChannels` mutation.
Mirrors `UpdateConnectedSlackChannelInput`.
# BulkUpdateConnectedSlackChannelResult
Source: https://www.plain.com/docs/graphql-reference/types/BulkUpdateConnectedSlackChannelResult
The outcome of a single update inside `bulkUpdateConnectedSlackChannels`.
# BulkUpdateConnectedSlackChannelsInput
Source: https://www.plain.com/docs/graphql-reference/types/BulkUpdateConnectedSlackChannelsInput
An input provided to the `bulkUpdateConnectedSlackChannels` mutation.
# BulkUpdateConnectedSlackChannelsOutput
Source: https://www.plain.com/docs/graphql-reference/types/BulkUpdateConnectedSlackChannelsOutput
An output type provided by the `bulkUpdateConnectedSlackChannels` mutation.
Returns per-update results so the caller can distinguish successful writes
from failed ones.
# BulkUpdateSlackChannelSettingItem
Source: https://www.plain.com/docs/graphql-reference/types/BulkUpdateSlackChannelSettingItem
A single update applied by the `bulkUpdateSlackChannelSettings` mutation.
# BulkUpdateSlackChannelSettingResult
Source: https://www.plain.com/docs/graphql-reference/types/BulkUpdateSlackChannelSettingResult
The outcome of a single update inside `bulkUpdateSlackChannelSettings`.
# BulkUpdateSlackChannelSettingsInput
Source: https://www.plain.com/docs/graphql-reference/types/BulkUpdateSlackChannelSettingsInput
An input provided to the `bulkUpdateSlackChannelSettings` mutation.
# BulkUpdateSlackChannelSettingsOutput
Source: https://www.plain.com/docs/graphql-reference/types/BulkUpdateSlackChannelSettingsOutput
An output type provided by the `bulkUpdateSlackChannelSettings` mutation.
Returns per-update results so the caller can distinguish successful writes
from failed ones.
# BulkUpsertThreadFieldResult
Source: https://www.plain.com/docs/graphql-reference/types/BulkUpsertThreadFieldResult
# BulkUpsertThreadFieldsInput
Source: https://www.plain.com/docs/graphql-reference/types/BulkUpsertThreadFieldsInput
# BulkUpsertThreadFieldsOutput
Source: https://www.plain.com/docs/graphql-reference/types/BulkUpsertThreadFieldsOutput
# BulkUpsertWorkflowStepInput
Source: https://www.plain.com/docs/graphql-reference/types/BulkUpsertWorkflowStepInput
Input for a single step in the bulk upsert operation.
# BulkUpsertWorkflowStepResult
Source: https://www.plain.com/docs/graphql-reference/types/BulkUpsertWorkflowStepResult
# BulkUpsertWorkflowStepResultItem
Source: https://www.plain.com/docs/graphql-reference/types/BulkUpsertWorkflowStepResultItem
# BulkUpsertWorkflowStepsInput
Source: https://www.plain.com/docs/graphql-reference/types/BulkUpsertWorkflowStepsInput
# BulkUpsertWorkflowStepsOutput
Source: https://www.plain.com/docs/graphql-reference/types/BulkUpsertWorkflowStepsOutput
# BusinessHoursSchedule
Source: https://www.plain.com/docs/graphql-reference/types/BusinessHoursSchedule
A named set of business hours slots. A workspace can have several schedules, each covering a different team or region.
# BusinessHoursScheduleConnection
Source: https://www.plain.com/docs/graphql-reference/types/BusinessHoursScheduleConnection
# BusinessHoursScheduleEdge
Source: https://www.plain.com/docs/graphql-reference/types/BusinessHoursScheduleEdge
# BusinessHoursSlot
Source: https://www.plain.com/docs/graphql-reference/types/BusinessHoursSlot
# BusinessHoursSlotInput
Source: https://www.plain.com/docs/graphql-reference/types/BusinessHoursSlotInput
# CalculateRoleChangeCostInput
Source: https://www.plain.com/docs/graphql-reference/types/CalculateRoleChangeCostInput
# CalculateRoleChangeCostOutput
Source: https://www.plain.com/docs/graphql-reference/types/CalculateRoleChangeCostOutput
# ChangeBillingPlanInput
Source: https://www.plain.com/docs/graphql-reference/types/ChangeBillingPlanInput
# ChangeBillingPlanOutput
Source: https://www.plain.com/docs/graphql-reference/types/ChangeBillingPlanOutput
# ChangeThreadCustomerInput
Source: https://www.plain.com/docs/graphql-reference/types/ChangeThreadCustomerInput
# ChangeThreadCustomerOutput
Source: https://www.plain.com/docs/graphql-reference/types/ChangeThreadCustomerOutput
# ChangeThreadDiscussionStatusInput
Source: https://www.plain.com/docs/graphql-reference/types/ChangeThreadDiscussionStatusInput
# ChangeThreadDiscussionStatusOutput
Source: https://www.plain.com/docs/graphql-reference/types/ChangeThreadDiscussionStatusOutput
# ChangeThreadPriorityInput
Source: https://www.plain.com/docs/graphql-reference/types/ChangeThreadPriorityInput
# ChangeThreadPriorityOutput
Source: https://www.plain.com/docs/graphql-reference/types/ChangeThreadPriorityOutput
# ChangeUserStatusInput
Source: https://www.plain.com/docs/graphql-reference/types/ChangeUserStatusInput
# ChangeUserStatusOutput
Source: https://www.plain.com/docs/graphql-reference/types/ChangeUserStatusOutput
# Chat
Source: https://www.plain.com/docs/graphql-reference/types/Chat
# ChatApp
Source: https://www.plain.com/docs/graphql-reference/types/ChatApp
# ChatAppConnection
Source: https://www.plain.com/docs/graphql-reference/types/ChatAppConnection
# ChatAppEdge
Source: https://www.plain.com/docs/graphql-reference/types/ChatAppEdge
# ChatAppHiddenSecret
Source: https://www.plain.com/docs/graphql-reference/types/ChatAppHiddenSecret
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`.
# ChatAppSecret
Source: https://www.plain.com/docs/graphql-reference/types/ChatAppSecret
# ChatEntry
Source: https://www.plain.com/docs/graphql-reference/types/ChatEntry
# ChatThreadChannelDetails
Source: https://www.plain.com/docs/graphql-reference/types/ChatThreadChannelDetails
# ChildThreadDetails
Source: https://www.plain.com/docs/graphql-reference/types/ChildThreadDetails
# CompaniesFilter
Source: https://www.plain.com/docs/graphql-reference/types/CompaniesFilter
# CompaniesSearchQuery
Source: https://www.plain.com/docs/graphql-reference/types/CompaniesSearchQuery
Query to search for companies.
# Company
Source: https://www.plain.com/docs/graphql-reference/types/Company
# CompanyConnection
Source: https://www.plain.com/docs/graphql-reference/types/CompanyConnection
# CompanyEdge
Source: https://www.plain.com/docs/graphql-reference/types/CompanyEdge
# CompanyIdentifierInput
Source: https://www.plain.com/docs/graphql-reference/types/CompanyIdentifierInput
# CompanySearchResult
Source: https://www.plain.com/docs/graphql-reference/types/CompanySearchResult
# CompanySearchResultConnection
Source: https://www.plain.com/docs/graphql-reference/types/CompanySearchResultConnection
# CompanySearchResultEdge
Source: https://www.plain.com/docs/graphql-reference/types/CompanySearchResultEdge
# CompanyTierMembership
Source: https://www.plain.com/docs/graphql-reference/types/CompanyTierMembership
# CompleteJiraAuthorizationInput
Source: https://www.plain.com/docs/graphql-reference/types/CompleteJiraAuthorizationInput
# CompleteServiceAuthorizationInput
Source: https://www.plain.com/docs/graphql-reference/types/CompleteServiceAuthorizationInput
# CompleteServiceAuthorizationOutput
Source: https://www.plain.com/docs/graphql-reference/types/CompleteServiceAuthorizationOutput
# CompleteSidekickMcpServerConnectionInput
Source: https://www.plain.com/docs/graphql-reference/types/CompleteSidekickMcpServerConnectionInput
# CompleteSidekickMcpServerConnectionOutput
Source: https://www.plain.com/docs/graphql-reference/types/CompleteSidekickMcpServerConnectionOutput
# ComponentBadge
Source: https://www.plain.com/docs/graphql-reference/types/ComponentBadge
# ComponentBadgeColor
Source: https://www.plain.com/docs/graphql-reference/types/ComponentBadgeColor
# ComponentBadgeInput
Source: https://www.plain.com/docs/graphql-reference/types/ComponentBadgeInput
# ComponentContainer
Source: https://www.plain.com/docs/graphql-reference/types/ComponentContainer
# ComponentContainerContent
Source: https://www.plain.com/docs/graphql-reference/types/ComponentContainerContent
# ComponentContainerContentInput
Source: https://www.plain.com/docs/graphql-reference/types/ComponentContainerContentInput
# ComponentContainerInput
Source: https://www.plain.com/docs/graphql-reference/types/ComponentContainerInput
# ComponentCopyButton
Source: https://www.plain.com/docs/graphql-reference/types/ComponentCopyButton
# ComponentCopyButtonInput
Source: https://www.plain.com/docs/graphql-reference/types/ComponentCopyButtonInput
# ComponentDateTime
Source: https://www.plain.com/docs/graphql-reference/types/ComponentDateTime
# ComponentDateTimeInput
Source: https://www.plain.com/docs/graphql-reference/types/ComponentDateTimeInput
# ComponentDivider
Source: https://www.plain.com/docs/graphql-reference/types/ComponentDivider
# ComponentDividerInput
Source: https://www.plain.com/docs/graphql-reference/types/ComponentDividerInput
# ComponentDividerSpacingSize
Source: https://www.plain.com/docs/graphql-reference/types/ComponentDividerSpacingSize
# ComponentInput
Source: https://www.plain.com/docs/graphql-reference/types/ComponentInput
# ComponentLinkButton
Source: https://www.plain.com/docs/graphql-reference/types/ComponentLinkButton
# ComponentLinkButtonInput
Source: https://www.plain.com/docs/graphql-reference/types/ComponentLinkButtonInput
# ComponentPlainText
Source: https://www.plain.com/docs/graphql-reference/types/ComponentPlainText
# ComponentPlainTextColor
Source: https://www.plain.com/docs/graphql-reference/types/ComponentPlainTextColor
# ComponentPlainTextInput
Source: https://www.plain.com/docs/graphql-reference/types/ComponentPlainTextInput
# ComponentPlainTextSize
Source: https://www.plain.com/docs/graphql-reference/types/ComponentPlainTextSize
# ComponentRow
Source: https://www.plain.com/docs/graphql-reference/types/ComponentRow
# ComponentRowContent
Source: https://www.plain.com/docs/graphql-reference/types/ComponentRowContent
# ComponentRowContentInput
Source: https://www.plain.com/docs/graphql-reference/types/ComponentRowContentInput
# ComponentRowInput
Source: https://www.plain.com/docs/graphql-reference/types/ComponentRowInput
# ComponentSpacer
Source: https://www.plain.com/docs/graphql-reference/types/ComponentSpacer
# ComponentSpacerInput
Source: https://www.plain.com/docs/graphql-reference/types/ComponentSpacerInput
# ComponentSpacerSize
Source: https://www.plain.com/docs/graphql-reference/types/ComponentSpacerSize
# ComponentText
Source: https://www.plain.com/docs/graphql-reference/types/ComponentText
# ComponentTextColor
Source: https://www.plain.com/docs/graphql-reference/types/ComponentTextColor
# ComponentTextInput
Source: https://www.plain.com/docs/graphql-reference/types/ComponentTextInput
# ComponentTextSize
Source: https://www.plain.com/docs/graphql-reference/types/ComponentTextSize
# ComponentUser
Source: https://www.plain.com/docs/graphql-reference/types/ComponentUser
# ComponentUserInput
Source: https://www.plain.com/docs/graphql-reference/types/ComponentUserInput
# ComponentWorkflowButton
Source: https://www.plain.com/docs/graphql-reference/types/ComponentWorkflowButton
# ComponentWorkflowButtonInput
Source: https://www.plain.com/docs/graphql-reference/types/ComponentWorkflowButtonInput
# ComponentWorkflowButtonWorkflowIdentifier
Source: https://www.plain.com/docs/graphql-reference/types/ComponentWorkflowButtonWorkflowIdentifier
# ComponentWorkflowButtonWorkflowIdentifierInput
Source: https://www.plain.com/docs/graphql-reference/types/ComponentWorkflowButtonWorkflowIdentifierInput
# ConnectedDiscordChannel
Source: https://www.plain.com/docs/graphql-reference/types/ConnectedDiscordChannel
# ConnectedDiscordChannelConnection
Source: https://www.plain.com/docs/graphql-reference/types/ConnectedDiscordChannelConnection
# ConnectedDiscordChannelEdge
Source: https://www.plain.com/docs/graphql-reference/types/ConnectedDiscordChannelEdge
# ConnectedMSTeamsChannel
Source: https://www.plain.com/docs/graphql-reference/types/ConnectedMSTeamsChannel
# ConnectedMSTeamsChannelConnection
Source: https://www.plain.com/docs/graphql-reference/types/ConnectedMSTeamsChannelConnection
# ConnectedMSTeamsChannelEdge
Source: https://www.plain.com/docs/graphql-reference/types/ConnectedMSTeamsChannelEdge
# ConnectedSlackChannel
Source: https://www.plain.com/docs/graphql-reference/types/ConnectedSlackChannel
# ConnectedSlackChannelConnection
Source: https://www.plain.com/docs/graphql-reference/types/ConnectedSlackChannelConnection
# ConnectedSlackChannelEdge
Source: https://www.plain.com/docs/graphql-reference/types/ConnectedSlackChannelEdge
# ConnectedSlackChannelType
Source: https://www.plain.com/docs/graphql-reference/types/ConnectedSlackChannelType
# ConnectedSlackChannelsFilter
Source: https://www.plain.com/docs/graphql-reference/types/ConnectedSlackChannelsFilter
# CreateAiFeedbackInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateAiFeedbackInput
# CreateAiFeedbackOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateAiFeedbackOutput
# CreateAiToneRuleInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateAiToneRuleInput
# CreateAiToneRuleOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateAiToneRuleOutput
# CreateApiKeyInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateApiKeyInput
# CreateApiKeyOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateApiKeyOutput
# CreateAttachmentDownloadUrlInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateAttachmentDownloadUrlInput
# CreateAttachmentDownloadUrlOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateAttachmentDownloadUrlOutput
# CreateAttachmentUploadUrlInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateAttachmentUploadUrlInput
# CreateAttachmentUploadUrlOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateAttachmentUploadUrlOutput
# CreateAutoresponderInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateAutoresponderInput
# CreateAutoresponderOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateAutoresponderOutput
# CreateBroadcastAudienceInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateBroadcastAudienceInput
# CreateBroadcastAudienceOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateBroadcastAudienceOutput
# CreateBroadcastInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateBroadcastInput
# CreateBroadcastOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateBroadcastOutput
# CreateBusinessHoursScheduleInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateBusinessHoursScheduleInput
# CreateBusinessHoursScheduleOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateBusinessHoursScheduleOutput
# CreateChatAppInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateChatAppInput
# CreateChatAppOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateChatAppOutput
# CreateChatAppSecretInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateChatAppSecretInput
# CreateChatAppSecretOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateChatAppSecretOutput
# CreateCustomRoleInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateCustomRoleInput
# CreateCustomRoleOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateCustomRoleOutput
# CreateCustomerCardConfigInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateCustomerCardConfigInput
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).
# CreateCustomerCardConfigOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateCustomerCardConfigOutput
# CreateCustomerEventInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateCustomerEventInput
# CreateCustomerEventOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateCustomerEventOutput
# CreateCustomerGroupInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateCustomerGroupInput
# CreateCustomerGroupOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateCustomerGroupOutput
# CreateCustomerSurveyInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateCustomerSurveyInput
# CreateCustomerSurveyOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateCustomerSurveyOutput
# CreateDemoChannelInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateDemoChannelInput
# CreateDemoChannelOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateDemoChannelOutput
# CreateDiscussionInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateDiscussionInput
# CreateDiscussionOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateDiscussionOutput
# CreateEmailPreviewUrlInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateEmailPreviewUrlInput
# CreateEmailPreviewUrlOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateEmailPreviewUrlOutput
# CreateEscalationPathInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateEscalationPathInput
# CreateEscalationPathOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateEscalationPathOutput
# CreateGithubUserAuthIntegrationInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateGithubUserAuthIntegrationInput
# CreateGithubUserAuthIntegrationOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateGithubUserAuthIntegrationOutput
# CreateHelpCenterArticleGroupInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateHelpCenterArticleGroupInput
# CreateHelpCenterArticleGroupOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateHelpCenterArticleGroupOutput
# CreateHelpCenterInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateHelpCenterInput
# CreateHelpCenterOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateHelpCenterOutput
# CreateHyperlineBillingPortalSessionOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateHyperlineBillingPortalSessionOutput
# CreateHyperlineComponentsAuthTokenOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateHyperlineComponentsAuthTokenOutput
# CreateImportSyncInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateImportSyncInput
# CreateImportSyncOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateImportSyncOutput
# CreateIndexedDocumentInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateIndexedDocumentInput
# CreateIndexedDocumentOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateIndexedDocumentOutput
# CreateIssueTrackerIssueInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateIssueTrackerIssueInput
# CreateIssueTrackerIssueOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateIssueTrackerIssueOutput
# CreateKnowledgeSourceInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateKnowledgeSourceInput
# CreateKnowledgeSourceOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateKnowledgeSourceOutput
# CreateLabelTypeInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateLabelTypeInput
# CreateLabelTypeOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateLabelTypeOutput
# CreateLinearAppIntegrationInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateLinearAppIntegrationInput
# CreateLinearAppIntegrationOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateLinearAppIntegrationOutput
# CreateMachineUserInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateMachineUserInput
# CreateMachineUserOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateMachineUserOutput
# CreateMyFavoritePageInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateMyFavoritePageInput
# CreateMyFavoritePageOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateMyFavoritePageOutput
# CreateMyLinearIntegrationInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateMyLinearIntegrationInput
# CreateMyLinearIntegrationOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateMyLinearIntegrationOutput
# CreateMyMSTeamsIntegrationInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateMyMSTeamsIntegrationInput
# CreateMyMSTeamsIntegrationOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateMyMSTeamsIntegrationOutput
# CreateMySlackIntegrationInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateMySlackIntegrationInput
# CreateMySlackIntegrationOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateMySlackIntegrationOutput
# CreateNoteInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateNoteInput
# CreateNoteOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateNoteOutput
# CreateSavedThreadsViewInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateSavedThreadsViewInput
# CreateSavedThreadsViewOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateSavedThreadsViewOutput
# CreateServiceLevelAgreementInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateServiceLevelAgreementInput
# CreateServiceLevelAgreementOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateServiceLevelAgreementOutput
# CreateServiceLevelAgreementPolicyInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateServiceLevelAgreementPolicyInput
# CreateServiceLevelAgreementPolicyOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateServiceLevelAgreementPolicyOutput
# CreateSidekickCustomSkillInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateSidekickCustomSkillInput
# CreateSidekickCustomSkillOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateSidekickCustomSkillOutput
# CreateSidekickMcpServerInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateSidekickMcpServerInput
# CreateSidekickMcpServerOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateSidekickMcpServerOutput
# CreateSnippetInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateSnippetInput
# CreateSnippetOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateSnippetOutput
# CreateTaskAssignedToInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateTaskAssignedToInput
Only one of the fields can be set.
# CreateTaskInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateTaskInput
# CreateTaskOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateTaskOutput
# CreateTenantInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateTenantInput
# CreateTenantOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateTenantOutput
# CreateThreadAssignedToInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateThreadAssignedToInput
Only one of the fields can be set.
# CreateThreadDiscussionInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateThreadDiscussionInput
# CreateThreadDiscussionOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateThreadDiscussionOutput
# CreateThreadDiscussionType
Source: https://www.plain.com/docs/graphql-reference/types/CreateThreadDiscussionType
# CreateThreadEventInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateThreadEventInput
# CreateThreadEventOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateThreadEventOutput
# CreateThreadFieldOnThreadInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateThreadFieldOnThreadInput
# CreateThreadFieldSchemaInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateThreadFieldSchemaInput
# CreateThreadFieldSchemaOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateThreadFieldSchemaOutput
# CreateThreadFromSlackMessageInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateThreadFromSlackMessageInput
Identifies the Slack message to ingest. Optional thread attributes are applied only when a new
thread is created. An already-ingested message returns the existing thread and ignores them.
When set, these values win over the Slack channel association and other inferred defaults.
# CreateThreadFromSlackMessageOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateThreadFromSlackMessageOutput
# CreateThreadInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateThreadInput
# CreateThreadLinkInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateThreadLinkInput
# CreateThreadLinkOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateThreadLinkOutput
# CreateThreadOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateThreadOutput
# CreateTierInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateTierInput
# CreateTierOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateTierOutput
# CreateUserAccountInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateUserAccountInput
# CreateUserAccountOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateUserAccountOutput
# CreateUserAuthDiscordChannelIntegrationInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateUserAuthDiscordChannelIntegrationInput
# CreateUserAuthDiscordChannelIntegrationOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateUserAuthDiscordChannelIntegrationOutput
# CreateUserAuthSlackIntegrationInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateUserAuthSlackIntegrationInput
# CreateUserAuthSlackIntegrationOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateUserAuthSlackIntegrationOutput
# CreateWebhookTargetInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateWebhookTargetInput
# CreateWebhookTargetOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateWebhookTargetOutput
# CreateWorkflowInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateWorkflowInput
# CreateWorkflowOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateWorkflowOutput
# CreateWorkflowRuleInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateWorkflowRuleInput
# CreateWorkflowRuleOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateWorkflowRuleOutput
# CreateWorkflowShareLinkInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateWorkflowShareLinkInput
# CreateWorkflowShareLinkOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateWorkflowShareLinkOutput
# CreateWorkflowStepInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateWorkflowStepInput
# CreateWorkflowStepOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateWorkflowStepOutput
# CreateWorkspaceCursorIntegrationInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateWorkspaceCursorIntegrationInput
# CreateWorkspaceCursorIntegrationOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateWorkspaceCursorIntegrationOutput
# CreateWorkspaceEmailDomainSettingsInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateWorkspaceEmailDomainSettingsInput
# CreateWorkspaceEmailDomainSettingsOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateWorkspaceEmailDomainSettingsOutput
# CreateWorkspaceFileDownloadUrlInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateWorkspaceFileDownloadUrlInput
# CreateWorkspaceFileDownloadUrlOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateWorkspaceFileDownloadUrlOutput
# CreateWorkspaceFileUploadUrlInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateWorkspaceFileUploadUrlInput
# CreateWorkspaceFileUploadUrlOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateWorkspaceFileUploadUrlOutput
# CreateWorkspaceSlackChannelIntegrationInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateWorkspaceSlackChannelIntegrationInput
# CreateWorkspaceSlackChannelIntegrationOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateWorkspaceSlackChannelIntegrationOutput
# CreateWorkspaceSlackIntegrationInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateWorkspaceSlackIntegrationInput
# CreateWorkspaceSlackIntegrationOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateWorkspaceSlackIntegrationOutput
# CreateWorkspaceSlackSidekickIntegrationInput
Source: https://www.plain.com/docs/graphql-reference/types/CreateWorkspaceSlackSidekickIntegrationInput
# CreateWorkspaceSlackSidekickIntegrationOutput
Source: https://www.plain.com/docs/graphql-reference/types/CreateWorkspaceSlackSidekickIntegrationOutput
# CsatCustomerSurveyTemplate
Source: https://www.plain.com/docs/graphql-reference/types/CsatCustomerSurveyTemplate
# CsatCustomerSurveyTemplateInput
Source: https://www.plain.com/docs/graphql-reference/types/CsatCustomerSurveyTemplateInput
# CurrencyCode
Source: https://www.plain.com/docs/graphql-reference/types/CurrencyCode
# CursorRepository
Source: https://www.plain.com/docs/graphql-reference/types/CursorRepository
# CustomEntry
Source: https://www.plain.com/docs/graphql-reference/types/CustomEntry
# CustomRole
Source: https://www.plain.com/docs/graphql-reference/types/CustomRole
# CustomRoleConnection
Source: https://www.plain.com/docs/graphql-reference/types/CustomRoleConnection
# CustomRoleEdge
Source: https://www.plain.com/docs/graphql-reference/types/CustomRoleEdge
# CustomSidekickSkill
Source: https://www.plain.com/docs/graphql-reference/types/CustomSidekickSkill
A workspace-authored skill, as it appears in the skills list.
# CustomTimelineEntryComponent
Source: https://www.plain.com/docs/graphql-reference/types/CustomTimelineEntryComponent
# Customer
Source: https://www.plain.com/docs/graphql-reference/types/Customer
The core customer entity. A customer only exists (ideally) once.
Uniqueness is guaranteed on both of these fields:
1. `externalId` if provided
2. `email`
# CustomerActor
Source: https://www.plain.com/docs/graphql-reference/types/CustomerActor
# CustomerCardComponent
Source: https://www.plain.com/docs/graphql-reference/types/CustomerCardComponent
# CustomerCardConfig
Source: https://www.plain.com/docs/graphql-reference/types/CustomerCardConfig
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.
# CustomerCardConfigApiHeader
Source: https://www.plain.com/docs/graphql-reference/types/CustomerCardConfigApiHeader
An API header that will be sent to the configured API URL.
# CustomerCardConfigApiHeaderInput
Source: https://www.plain.com/docs/graphql-reference/types/CustomerCardConfigApiHeaderInput
An API header that will be sent to the configured API URL.
# CustomerCardConfigOrderInput
Source: https://www.plain.com/docs/graphql-reference/types/CustomerCardConfigOrderInput
# CustomerCardInstance
Source: https://www.plain.com/docs/graphql-reference/types/CustomerCardInstance
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.
# CustomerCardInstanceCardTooBigErrorDetail
Source: https://www.plain.com/docs/graphql-reference/types/CustomerCardInstanceCardTooBigErrorDetail
The card exceeded the maximum allowed size.
# CustomerCardInstanceError
Source: https://www.plain.com/docs/graphql-reference/types/CustomerCardInstanceError
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.
# CustomerCardInstanceErrorDetail
Source: https://www.plain.com/docs/graphql-reference/types/CustomerCardInstanceErrorDetail
Details for the reasons why the customer card failed to load.
# CustomerCardInstanceLoaded
Source: https://www.plain.com/docs/graphql-reference/types/CustomerCardInstanceLoaded
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.
# CustomerCardInstanceLoading
Source: https://www.plain.com/docs/graphql-reference/types/CustomerCardInstanceLoading
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`.
# CustomerCardInstanceMissingCardErrorDetail
Source: https://www.plain.com/docs/graphql-reference/types/CustomerCardInstanceMissingCardErrorDetail
The configured API URL didn't return a requested card key.
# CustomerCardInstanceRequestErrorDetail
Source: https://www.plain.com/docs/graphql-reference/types/CustomerCardInstanceRequestErrorDetail
Plain failed to make the request to the configured API URL.
# CustomerCardInstanceResponseBodyErrorDetail
Source: https://www.plain.com/docs/graphql-reference/types/CustomerCardInstanceResponseBodyErrorDetail
An invalid response body was returned from the configured API URL.
# CustomerCardInstanceStatusCodeErrorDetail
Source: https://www.plain.com/docs/graphql-reference/types/CustomerCardInstanceStatusCodeErrorDetail
A non-200 status code was returned from the configured API URL.
# CustomerCardInstanceTimeoutErrorDetail
Source: https://www.plain.com/docs/graphql-reference/types/CustomerCardInstanceTimeoutErrorDetail
The card failed to load within the timeout.
# CustomerCardInstanceUnknownErrorDetail
Source: https://www.plain.com/docs/graphql-reference/types/CustomerCardInstanceUnknownErrorDetail
An unknown error occurred. If this error is persistent, please contact our support.
# CustomerConnection
Source: https://www.plain.com/docs/graphql-reference/types/CustomerConnection
# CustomerEdge
Source: https://www.plain.com/docs/graphql-reference/types/CustomerEdge
# CustomerEmailActor
Source: https://www.plain.com/docs/graphql-reference/types/CustomerEmailActor
# CustomerEvent
Source: https://www.plain.com/docs/graphql-reference/types/CustomerEvent
# CustomerEventEntry
Source: https://www.plain.com/docs/graphql-reference/types/CustomerEventEntry
# CustomerGroup
Source: https://www.plain.com/docs/graphql-reference/types/CustomerGroup
# CustomerGroupConnection
Source: https://www.plain.com/docs/graphql-reference/types/CustomerGroupConnection
# CustomerGroupEdge
Source: https://www.plain.com/docs/graphql-reference/types/CustomerGroupEdge
# CustomerGroupIdentifier
Source: https://www.plain.com/docs/graphql-reference/types/CustomerGroupIdentifier
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.
# CustomerGroupMembership
Source: https://www.plain.com/docs/graphql-reference/types/CustomerGroupMembership
# CustomerGroupMembershipConnection
Source: https://www.plain.com/docs/graphql-reference/types/CustomerGroupMembershipConnection
# CustomerGroupMembershipEdge
Source: https://www.plain.com/docs/graphql-reference/types/CustomerGroupMembershipEdge
# CustomerGroupMembershipsFilter
Source: https://www.plain.com/docs/graphql-reference/types/CustomerGroupMembershipsFilter
# CustomerGroupsFilter
Source: https://www.plain.com/docs/graphql-reference/types/CustomerGroupsFilter
# CustomerIdentifierInput
Source: https://www.plain.com/docs/graphql-reference/types/CustomerIdentifierInput
Only one of the fields can be set.
# CustomerIdentity
Source: https://www.plain.com/docs/graphql-reference/types/CustomerIdentity
# CustomerImpersonationInput
Source: https://www.plain.com/docs/graphql-reference/types/CustomerImpersonationInput
# CustomerSearchCondition
Source: https://www.plain.com/docs/graphql-reference/types/CustomerSearchCondition
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.
# CustomerSearchConnection
Source: https://www.plain.com/docs/graphql-reference/types/CustomerSearchConnection
# CustomerSearchEdge
Source: https://www.plain.com/docs/graphql-reference/types/CustomerSearchEdge
# CustomerStatus
Source: https://www.plain.com/docs/graphql-reference/types/CustomerStatus
Deprecated: customer-level status has been replaced by per-thread status. Use `Thread.status` and
`ThreadStatus` instead.
# CustomerSurvey
Source: https://www.plain.com/docs/graphql-reference/types/CustomerSurvey
# CustomerSurveyCondition
Source: https://www.plain.com/docs/graphql-reference/types/CustomerSurveyCondition
# CustomerSurveyConditionInput
Source: https://www.plain.com/docs/graphql-reference/types/CustomerSurveyConditionInput
# CustomerSurveyConditionOperator
Source: https://www.plain.com/docs/graphql-reference/types/CustomerSurveyConditionOperator
# CustomerSurveyConditionsOperator
Source: https://www.plain.com/docs/graphql-reference/types/CustomerSurveyConditionsOperator
# CustomerSurveyConnection
Source: https://www.plain.com/docs/graphql-reference/types/CustomerSurveyConnection
# CustomerSurveyEdge
Source: https://www.plain.com/docs/graphql-reference/types/CustomerSurveyEdge
# CustomerSurveyLabelCondition
Source: https://www.plain.com/docs/graphql-reference/types/CustomerSurveyLabelCondition
# CustomerSurveyMessageSourceCondition
Source: https://www.plain.com/docs/graphql-reference/types/CustomerSurveyMessageSourceCondition
# CustomerSurveyOrderInput
Source: https://www.plain.com/docs/graphql-reference/types/CustomerSurveyOrderInput
# CustomerSurveyPrioritiesCondition
Source: https://www.plain.com/docs/graphql-reference/types/CustomerSurveyPrioritiesCondition
# CustomerSurveyRequestedEntry
Source: https://www.plain.com/docs/graphql-reference/types/CustomerSurveyRequestedEntry
# CustomerSurveySupportEmailsCondition
Source: https://www.plain.com/docs/graphql-reference/types/CustomerSurveySupportEmailsCondition
# CustomerSurveyTemplate
Source: https://www.plain.com/docs/graphql-reference/types/CustomerSurveyTemplate
# CustomerSurveyTemplateInput
Source: https://www.plain.com/docs/graphql-reference/types/CustomerSurveyTemplateInput
# CustomerSurveyTiersCondition
Source: https://www.plain.com/docs/graphql-reference/types/CustomerSurveyTiersCondition
# CustomerTenantMembership
Source: https://www.plain.com/docs/graphql-reference/types/CustomerTenantMembership
Represents a customer's membership in a tenant. Returned on the `tenantMemberships` sub-query of a `Customer`.
# CustomerTenantMembershipConnection
Source: https://www.plain.com/docs/graphql-reference/types/CustomerTenantMembershipConnection
# CustomerTenantMembershipEdge
Source: https://www.plain.com/docs/graphql-reference/types/CustomerTenantMembershipEdge
# CustomersFilter
Source: https://www.plain.com/docs/graphql-reference/types/CustomersFilter
# CustomersSearchQuery
Source: https://www.plain.com/docs/graphql-reference/types/CustomersSearchQuery
A query to search for customers. Search queries are combinations of search conditions, as defined
below. At least one search condition must be provided.
# CustomersSort
Source: https://www.plain.com/docs/graphql-reference/types/CustomersSort
# CustomersSortField
Source: https://www.plain.com/docs/graphql-reference/types/CustomersSortField
# DateTime
Source: https://www.plain.com/docs/graphql-reference/types/DateTime
# DatetimeFilter
Source: https://www.plain.com/docs/graphql-reference/types/DatetimeFilter
# DatetimeFilterOutput
Source: https://www.plain.com/docs/graphql-reference/types/DatetimeFilterOutput
# DefaultServiceIntegration
Source: https://www.plain.com/docs/graphql-reference/types/DefaultServiceIntegration
# DeleteAiToneRulesInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteAiToneRulesInput
# DeleteAiToneRulesOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteAiToneRulesOutput
# DeleteApiKeyInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteApiKeyInput
# DeleteApiKeyOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteApiKeyOutput
# DeleteAutoresponderInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteAutoresponderInput
# DeleteAutoresponderOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteAutoresponderOutput
# DeleteBroadcastAudienceInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteBroadcastAudienceInput
# DeleteBroadcastAudienceOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteBroadcastAudienceOutput
# DeleteBroadcastInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteBroadcastInput
# DeleteBroadcastOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteBroadcastOutput
# DeleteBusinessHoursScheduleInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteBusinessHoursScheduleInput
# DeleteBusinessHoursScheduleOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteBusinessHoursScheduleOutput
# DeleteChatAppInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteChatAppInput
# DeleteChatAppOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteChatAppOutput
# DeleteChatAppSecretInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteChatAppSecretInput
# DeleteChatAppSecretOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteChatAppSecretOutput
# DeleteCompanyInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteCompanyInput
# DeleteCompanyOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteCompanyOutput
# DeleteCustomRoleInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteCustomRoleInput
# DeleteCustomRoleOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteCustomRoleOutput
# DeleteCustomerCardConfigInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteCustomerCardConfigInput
# DeleteCustomerCardConfigOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteCustomerCardConfigOutput
# DeleteCustomerGroupInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteCustomerGroupInput
# DeleteCustomerGroupOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteCustomerGroupOutput
# DeleteCustomerInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteCustomerInput
# DeleteCustomerOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteCustomerOutput
# DeleteCustomerSurveyInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteCustomerSurveyInput
# DeleteCustomerSurveyOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteCustomerSurveyOutput
# DeleteEmailSuppressionInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteEmailSuppressionInput
# DeleteEmailSuppressionOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteEmailSuppressionOutput
# DeleteEscalationPathInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteEscalationPathInput
# DeleteEscalationPathOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteEscalationPathOutput
# DeleteGithubUserAuthIntegrationOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteGithubUserAuthIntegrationOutput
# DeleteHelpCenterArticleGroupInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteHelpCenterArticleGroupInput
# DeleteHelpCenterArticleGroupOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteHelpCenterArticleGroupOutput
# DeleteHelpCenterArticleInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteHelpCenterArticleInput
# DeleteHelpCenterArticleOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteHelpCenterArticleOutput
# DeleteHelpCenterInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteHelpCenterInput
# DeleteHelpCenterOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteHelpCenterOutput
# DeleteKnowledgeSourceInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteKnowledgeSourceInput
# DeleteKnowledgeSourceOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteKnowledgeSourceOutput
# DeleteLinearAppIntegrationOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteLinearAppIntegrationOutput
# DeleteMachineUserInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteMachineUserInput
# DeleteMachineUserOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteMachineUserOutput
# DeleteMyFavoritePageInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteMyFavoritePageInput
# DeleteMyFavoritePageOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteMyFavoritePageOutput
# DeleteMyLinearIntegrationOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteMyLinearIntegrationOutput
# DeleteMyMSTeamsIntegrationOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteMyMSTeamsIntegrationOutput
# DeleteMyServiceAuthorizationInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteMyServiceAuthorizationInput
# DeleteMyServiceAuthorizationOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteMyServiceAuthorizationOutput
# DeleteMySlackIntegrationOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteMySlackIntegrationOutput
# DeleteNoteInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteNoteInput
# DeleteNoteOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteNoteOutput
# DeleteQueuedAgentSessionMessageInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteQueuedAgentSessionMessageInput
# DeleteQueuedAgentSessionMessageOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteQueuedAgentSessionMessageOutput
# DeleteSavedThreadsViewInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteSavedThreadsViewInput
# DeleteSavedThreadsViewOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteSavedThreadsViewOutput
# DeleteServiceAuthorizationInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteServiceAuthorizationInput
# DeleteServiceAuthorizationOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteServiceAuthorizationOutput
# DeleteServiceLevelAgreementInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteServiceLevelAgreementInput
# DeleteServiceLevelAgreementOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteServiceLevelAgreementOutput
# DeleteServiceLevelAgreementPolicyInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteServiceLevelAgreementPolicyInput
# DeleteServiceLevelAgreementPolicyOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteServiceLevelAgreementPolicyOutput
# DeleteSettingInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteSettingInput
An input provided to the `deleteSetting` mutation.
# DeleteSettingOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteSettingOutput
An output type provided by the `deleteSetting` mutation.
Returns the deleted setting (or null if it did not exist) or an error.
# DeleteSidekickCustomSkillInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteSidekickCustomSkillInput
# DeleteSidekickCustomSkillOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteSidekickCustomSkillOutput
# DeleteSidekickMcpServerInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteSidekickMcpServerInput
# DeleteSidekickMcpServerOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteSidekickMcpServerOutput
# DeleteSnippetInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteSnippetInput
# DeleteSnippetOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteSnippetOutput
# DeleteTaskInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteTaskInput
# DeleteTaskOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteTaskOutput
# DeleteTenantFieldInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteTenantFieldInput
# DeleteTenantFieldOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteTenantFieldOutput
# DeleteTenantFieldSchemaInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteTenantFieldSchemaInput
# DeleteTenantFieldSchemaOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteTenantFieldSchemaOutput
# DeleteTenantInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteTenantInput
# DeleteTenantOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteTenantOutput
# DeleteThreadChannelAssociationInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteThreadChannelAssociationInput
# DeleteThreadChannelAssociationOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteThreadChannelAssociationOutput
# DeleteThreadDiscussionInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteThreadDiscussionInput
# DeleteThreadDiscussionOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteThreadDiscussionOutput
# DeleteThreadFieldIdentifier
Source: https://www.plain.com/docs/graphql-reference/types/DeleteThreadFieldIdentifier
# DeleteThreadFieldInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteThreadFieldInput
# DeleteThreadFieldOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteThreadFieldOutput
# DeleteThreadFieldSchemaInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteThreadFieldSchemaInput
# DeleteThreadFieldSchemaOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteThreadFieldSchemaOutput
# DeleteThreadInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteThreadInput
# DeleteThreadLinkInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteThreadLinkInput
# DeleteThreadLinkOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteThreadLinkOutput
# DeleteThreadOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteThreadOutput
# DeleteTierInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteTierInput
# DeleteTierOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteTierOutput
# DeleteUserAuthDiscordChannelIntegrationInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteUserAuthDiscordChannelIntegrationInput
# DeleteUserAuthDiscordChannelIntegrationOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteUserAuthDiscordChannelIntegrationOutput
# DeleteUserAuthSlackIntegrationInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteUserAuthSlackIntegrationInput
# DeleteUserAuthSlackIntegrationOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteUserAuthSlackIntegrationOutput
# DeleteUserInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteUserInput
# DeleteUserOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteUserOutput
# DeleteWebhookTargetInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteWebhookTargetInput
# DeleteWebhookTargetOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteWebhookTargetOutput
# DeleteWorkflowInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteWorkflowInput
# DeleteWorkflowOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteWorkflowOutput
# DeleteWorkflowRuleInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteWorkflowRuleInput
# DeleteWorkflowRuleOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteWorkflowRuleOutput
# DeleteWorkflowStepInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteWorkflowStepInput
# DeleteWorkflowStepOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteWorkflowStepOutput
# DeleteWorkspaceCursorIntegrationOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteWorkspaceCursorIntegrationOutput
# DeleteWorkspaceDiscordChannelIntegrationInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteWorkspaceDiscordChannelIntegrationInput
# DeleteWorkspaceDiscordChannelIntegrationOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteWorkspaceDiscordChannelIntegrationOutput
# DeleteWorkspaceDiscordIntegrationInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteWorkspaceDiscordIntegrationInput
# DeleteWorkspaceDiscordIntegrationOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteWorkspaceDiscordIntegrationOutput
# DeleteWorkspaceEmailDomainSettingsOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteWorkspaceEmailDomainSettingsOutput
# DeleteWorkspaceFileInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteWorkspaceFileInput
# DeleteWorkspaceFileOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteWorkspaceFileOutput
# DeleteWorkspaceInviteInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteWorkspaceInviteInput
# DeleteWorkspaceInviteOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteWorkspaceInviteOutput
# DeleteWorkspaceMSTeamsIntegrationInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteWorkspaceMSTeamsIntegrationInput
# DeleteWorkspaceMSTeamsIntegrationOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteWorkspaceMSTeamsIntegrationOutput
# DeleteWorkspaceSlackChannelIntegrationInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteWorkspaceSlackChannelIntegrationInput
# DeleteWorkspaceSlackChannelIntegrationOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteWorkspaceSlackChannelIntegrationOutput
# DeleteWorkspaceSlackIntegrationInput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteWorkspaceSlackIntegrationInput
# DeleteWorkspaceSlackIntegrationOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteWorkspaceSlackIntegrationOutput
# DeleteWorkspaceSlackSidekickIntegrationOutput
Source: https://www.plain.com/docs/graphql-reference/types/DeleteWorkspaceSlackSidekickIntegrationOutput
# DeletedCustomerActor
Source: https://www.plain.com/docs/graphql-reference/types/DeletedCustomerActor
# DeletedCustomerEmailActor
Source: https://www.plain.com/docs/graphql-reference/types/DeletedCustomerEmailActor
# DeletedThread
Source: https://www.plain.com/docs/graphql-reference/types/DeletedThread
A record of a thread that has been deleted, returned by the `deletedThreads` query.
# DeletedThreadConnection
Source: https://www.plain.com/docs/graphql-reference/types/DeletedThreadConnection
# DeletedThreadEdge
Source: https://www.plain.com/docs/graphql-reference/types/DeletedThreadEdge
# DeletedThreadsFilter
Source: https://www.plain.com/docs/graphql-reference/types/DeletedThreadsFilter
# DemoChannelType
Source: https://www.plain.com/docs/graphql-reference/types/DemoChannelType
# DependsOnLabelType
Source: https://www.plain.com/docs/graphql-reference/types/DependsOnLabelType
# DependsOnThreadFieldInput
Source: https://www.plain.com/docs/graphql-reference/types/DependsOnThreadFieldInput
Makes a thread field conditional on a sibling field's value. Nesting is capped at one level: `threadFieldSchemaId` must reference a field that does not itself depend on another thread field, otherwise the mutation fails with `cannot_create_thread_field_schema` (the dependent nesting level is too deep).
# DependsOnThreadFieldType
Source: https://www.plain.com/docs/graphql-reference/types/DependsOnThreadFieldType
# DiscordCustomerIdentity
Source: https://www.plain.com/docs/graphql-reference/types/DiscordCustomerIdentity
# DiscordMessage
Source: https://www.plain.com/docs/graphql-reference/types/DiscordMessage
# DiscordMessageEntry
Source: https://www.plain.com/docs/graphql-reference/types/DiscordMessageEntry
# DiscordThreadChannelDetails
Source: https://www.plain.com/docs/graphql-reference/types/DiscordThreadChannelDetails
# DiscussionAgentType
Source: https://www.plain.com/docs/graphql-reference/types/DiscussionAgentType
Which kind of agent is working on a discussion. This is about who runs the agent, not which
channel the discussion is on: a Sidekick session can run in Plain, in a workflow or in Slack.
# DiscussionResolvedNotificationDetail
Source: https://www.plain.com/docs/graphql-reference/types/DiscussionResolvedNotificationDetail
# DiscussionSourceEntityType
Source: https://www.plain.com/docs/graphql-reference/types/DiscussionSourceEntityType
# DiscussionSourceType
Source: https://www.plain.com/docs/graphql-reference/types/DiscussionSourceType
What kind of source a discussion was started from. CHART is a reporting chart/page (carries sourcePageLink, no sourceEntityId); ENTITY carries sourceEntityId + sourceEntityType; PAGE carries just sourcePageLink; THREAD is thread-scoped.
# DiscussionToolCallStatus
Source: https://www.plain.com/docs/graphql-reference/types/DiscussionToolCallStatus
PENDING while in flight, then SUCCESS or ERROR (final).
# DiscussionType
Source: https://www.plain.com/docs/graphql-reference/types/DiscussionType
# DiscussionsFilter
Source: https://www.plain.com/docs/graphql-reference/types/DiscussionsFilter
# DiscussionsSort
Source: https://www.plain.com/docs/graphql-reference/types/DiscussionsSort
# DiscussionsSortField
Source: https://www.plain.com/docs/graphql-reference/types/DiscussionsSortField
# DismissSuggestedLabelTypesInput
Source: https://www.plain.com/docs/graphql-reference/types/DismissSuggestedLabelTypesInput
# DismissSuggestedLabelTypesOutput
Source: https://www.plain.com/docs/graphql-reference/types/DismissSuggestedLabelTypesOutput
# DnsRecord
Source: https://www.plain.com/docs/graphql-reference/types/DnsRecord
# DoneStatusDetail
Source: https://www.plain.com/docs/graphql-reference/types/DoneStatusDetail
# EditQueuedAgentSessionMessageInput
Source: https://www.plain.com/docs/graphql-reference/types/EditQueuedAgentSessionMessageInput
# EditQueuedAgentSessionMessageOutput
Source: https://www.plain.com/docs/graphql-reference/types/EditQueuedAgentSessionMessageOutput
# Email
Source: https://www.plain.com/docs/graphql-reference/types/Email
# EmailActor
Source: https://www.plain.com/docs/graphql-reference/types/EmailActor
# EmailAddress
Source: https://www.plain.com/docs/graphql-reference/types/EmailAddress
An object modelling an email address and if it's been verified.
# EmailAddressInput
Source: https://www.plain.com/docs/graphql-reference/types/EmailAddressInput
# EmailAuthenticity
Source: https://www.plain.com/docs/graphql-reference/types/EmailAuthenticity
# EmailBounce
Source: https://www.plain.com/docs/graphql-reference/types/EmailBounce
# EmailBounceNotificationDetail
Source: https://www.plain.com/docs/graphql-reference/types/EmailBounceNotificationDetail
# EmailBounceReason
Source: https://www.plain.com/docs/graphql-reference/types/EmailBounceReason
# EmailBroadcastSendDeliveryRecipient
Source: https://www.plain.com/docs/graphql-reference/types/EmailBroadcastSendDeliveryRecipient
# EmailCategory
Source: https://www.plain.com/docs/graphql-reference/types/EmailCategory
# EmailCustomerIdentity
Source: https://www.plain.com/docs/graphql-reference/types/EmailCustomerIdentity
# EmailEntry
Source: https://www.plain.com/docs/graphql-reference/types/EmailEntry
# EmailParticipant
Source: https://www.plain.com/docs/graphql-reference/types/EmailParticipant
# EmailParticipantInput
Source: https://www.plain.com/docs/graphql-reference/types/EmailParticipantInput
# EmailPreviewUrl
Source: https://www.plain.com/docs/graphql-reference/types/EmailPreviewUrl
# EmailSendStatus
Source: https://www.plain.com/docs/graphql-reference/types/EmailSendStatus
# EmailSignature
Source: https://www.plain.com/docs/graphql-reference/types/EmailSignature
# EmailSuppression
Source: https://www.plain.com/docs/graphql-reference/types/EmailSuppression
# EmailSuppressionReason
Source: https://www.plain.com/docs/graphql-reference/types/EmailSuppressionReason
# EmbedToken
Source: https://www.plain.com/docs/graphql-reference/types/EmbedToken
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`.
# Entry
Source: https://www.plain.com/docs/graphql-reference/types/Entry
A union of all possible entries that can appear in a timeline.
# EscalateThreadInput
Source: https://www.plain.com/docs/graphql-reference/types/EscalateThreadInput
# EscalateThreadOutput
Source: https://www.plain.com/docs/graphql-reference/types/EscalateThreadOutput
# EscalationPath
Source: https://www.plain.com/docs/graphql-reference/types/EscalationPath
# EscalationPathConnection
Source: https://www.plain.com/docs/graphql-reference/types/EscalationPathConnection
# EscalationPathEdge
Source: https://www.plain.com/docs/graphql-reference/types/EscalationPathEdge
# EscalationPathStep
Source: https://www.plain.com/docs/graphql-reference/types/EscalationPathStep
A single step in an escalation path. Each step either targets a specific user or all owners of a label type.
# EscalationPathStepInput
Source: https://www.plain.com/docs/graphql-reference/types/EscalationPathStepInput
# EscalationPathStepLabelType
Source: https://www.plain.com/docs/graphql-reference/types/EscalationPathStepLabelType
An escalation step that assigns the thread the given label type, routing it to that label type's owners.
# EscalationPathStepType
Source: https://www.plain.com/docs/graphql-reference/types/EscalationPathStepType
# EscalationPathStepUser
Source: https://www.plain.com/docs/graphql-reference/types/EscalationPathStepUser
An escalation step that directly assigns the thread to a specific user.
# EventComponent
Source: https://www.plain.com/docs/graphql-reference/types/EventComponent
# EventComponentInput
Source: https://www.plain.com/docs/graphql-reference/types/EventComponentInput
# ExportWorkflowTemplateInput
Source: https://www.plain.com/docs/graphql-reference/types/ExportWorkflowTemplateInput
# ExportWorkflowTemplateOutput
Source: https://www.plain.com/docs/graphql-reference/types/ExportWorkflowTemplateOutput
# FavoritePage
Source: https://www.plain.com/docs/graphql-reference/types/FavoritePage
# FavoritePageConnection
Source: https://www.plain.com/docs/graphql-reference/types/FavoritePageConnection
# FavoritePageEdge
Source: https://www.plain.com/docs/graphql-reference/types/FavoritePageEdge
# FeatureKey
Source: https://www.plain.com/docs/graphql-reference/types/FeatureKey
# FileSize
Source: https://www.plain.com/docs/graphql-reference/types/FileSize
# FirstResolutionTimeServiceLevelAgreement
Source: https://www.plain.com/docs/graphql-reference/types/FirstResolutionTimeServiceLevelAgreement
An SLA that tracks the time from thread creation until the thread is first marked Done. Reopening a thread does not start a new tracker.
# FirstResponseTimeServiceLevelAgreement
Source: https://www.plain.com/docs/graphql-reference/types/FirstResponseTimeServiceLevelAgreement
An SLA that tracks the time from thread creation until a teammate sends the first reply.
# ForkThreadInput
Source: https://www.plain.com/docs/graphql-reference/types/ForkThreadInput
# ForkThreadOutput
Source: https://www.plain.com/docs/graphql-reference/types/ForkThreadOutput
# GenerateAiToneRulesFromDescriptionInput
Source: https://www.plain.com/docs/graphql-reference/types/GenerateAiToneRulesFromDescriptionInput
# GenerateAiToneRulesFromDescriptionOutput
Source: https://www.plain.com/docs/graphql-reference/types/GenerateAiToneRulesFromDescriptionOutput
# GenerateHelpCenterArticleInput
Source: https://www.plain.com/docs/graphql-reference/types/GenerateHelpCenterArticleInput
# GenerateHelpCenterArticleOutput
Source: https://www.plain.com/docs/graphql-reference/types/GenerateHelpCenterArticleOutput
# GenerateReplyOption
Source: https://www.plain.com/docs/graphql-reference/types/GenerateReplyOption
# GeneratedReply
Source: https://www.plain.com/docs/graphql-reference/types/GeneratedReply
# GeneratedReplyFeedbackInput
Source: https://www.plain.com/docs/graphql-reference/types/GeneratedReplyFeedbackInput
# GeneratedReplyFeedbackType
Source: https://www.plain.com/docs/graphql-reference/types/GeneratedReplyFeedbackType
# GenericThreadLink
Source: https://www.plain.com/docs/graphql-reference/types/GenericThreadLink
# GithubUserAuthIntegration
Source: https://www.plain.com/docs/graphql-reference/types/GithubUserAuthIntegration
# HeatmapMetricName
Source: https://www.plain.com/docs/graphql-reference/types/HeatmapMetricName
# HelpCenter
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenter
# HelpCenterAccessSettings
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterAccessSettings
# HelpCenterAccessSettingsInput
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterAccessSettingsInput
# HelpCenterAiConversationMessageEntry
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterAiConversationMessageEntry
# HelpCenterArticle
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterArticle
# HelpCenterArticleConnection
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterArticleConnection
# HelpCenterArticleCopyOptionSettings
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterArticleCopyOptionSettings
# HelpCenterArticleCopyOptionSettingsInput
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterArticleCopyOptionSettingsInput
# HelpCenterArticleCopyOptions
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterArticleCopyOptions
# HelpCenterArticleCopyOptionsInput
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterArticleCopyOptionsInput
# HelpCenterArticleDocument
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterArticleDocument
A help center article as a unit of citable knowledge.
# HelpCenterArticleEdge
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterArticleEdge
# HelpCenterArticleGroup
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterArticleGroup
# HelpCenterArticleGroupConnection
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterArticleGroupConnection
# HelpCenterArticleGroupEdge
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterArticleGroupEdge
# HelpCenterArticleSearchResult
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterArticleSearchResult
# HelpCenterArticleStatus
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterArticleStatus
# HelpCenterAuthMechanism
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterAuthMechanism
# HelpCenterAuthMechanismInput
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterAuthMechanismInput
# HelpCenterAuthMechanismType
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterAuthMechanismType
# HelpCenterAuthMechanismWorkosAuthkit
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterAuthMechanismWorkosAuthkit
# HelpCenterAuthMechanismWorkosConnect
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterAuthMechanismWorkosConnect
# HelpCenterConnection
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterConnection
# HelpCenterDomainNameVerificationTxtRecord
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterDomainNameVerificationTxtRecord
# HelpCenterDomainSettings
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterDomainSettings
# HelpCenterEdge
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterEdge
# HelpCenterIndex
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterIndex
# HelpCenterIndexItem
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterIndexItem
# HelpCenterIndexItemInput
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterIndexItemInput
# HelpCenterIndexItemType
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterIndexItemType
# HelpCenterPortalSettings
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterPortalSettings
# HelpCenterPortalSettingsDropdownFormField
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterPortalSettingsDropdownFormField
# HelpCenterPortalSettingsDropdownOption
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterPortalSettingsDropdownOption
# HelpCenterPortalSettingsDropdownOptionInput
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterPortalSettingsDropdownOptionInput
# HelpCenterPortalSettingsFormField
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterPortalSettingsFormField
# HelpCenterPortalSettingsFormFieldInput
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterPortalSettingsFormFieldInput
# HelpCenterPortalSettingsFormFieldType
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterPortalSettingsFormFieldType
# HelpCenterPortalSettingsInput
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterPortalSettingsInput
# HelpCenterPortalSettingsOverrideCustomerCompany
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterPortalSettingsOverrideCustomerCompany
# HelpCenterPortalSettingsOverrideCustomerCompanyInput
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterPortalSettingsOverrideCustomerCompanyInput
# HelpCenterPortalSettingsOverrideCustomerTenants
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterPortalSettingsOverrideCustomerTenants
# HelpCenterPortalSettingsOverrideCustomerTenantsInput
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterPortalSettingsOverrideCustomerTenantsInput
# HelpCenterPortalSettingsTextFormField
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterPortalSettingsTextFormField
# HelpCenterPortalSettingsThreadDetails
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterPortalSettingsThreadDetails
# HelpCenterPortalSettingsThreadDetailsInput
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterPortalSettingsThreadDetailsInput
# HelpCenterPortalSettingsThreadFields
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterPortalSettingsThreadFields
# HelpCenterPortalSettingsThreadFieldsInput
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterPortalSettingsThreadFieldsInput
# HelpCenterPortalSettingsThreadVisibility
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterPortalSettingsThreadVisibility
# HelpCenterPortalSettingsThreadVisibilityInput
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterPortalSettingsThreadVisibilityInput
# HelpCenterThemedImage
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterThemedImage
# HelpCenterThemedImageInput
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterThemedImageInput
# HelpCenterType
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterType
# HelpCenterWorkosAuthkitAuthMechanismInput
Source: https://www.plain.com/docs/graphql-reference/types/HelpCenterWorkosAuthkitAuthMechanismInput
# HelpCentersFilter
Source: https://www.plain.com/docs/graphql-reference/types/HelpCentersFilter
# HyperlineCheckoutSession
Source: https://www.plain.com/docs/graphql-reference/types/HyperlineCheckoutSession
# HyperlineCheckoutSessionStatus
Source: https://www.plain.com/docs/graphql-reference/types/HyperlineCheckoutSessionStatus
# ImpersonationInput
Source: https://www.plain.com/docs/graphql-reference/types/ImpersonationInput
Exactly one field must be set.
# ImportCustomerInput
Source: https://www.plain.com/docs/graphql-reference/types/ImportCustomerInput
# ImportCustomerTenantInput
Source: https://www.plain.com/docs/graphql-reference/types/ImportCustomerTenantInput
# ImportCustomersInput
Source: https://www.plain.com/docs/graphql-reference/types/ImportCustomersInput
# ImportCustomersOutput
Source: https://www.plain.com/docs/graphql-reference/types/ImportCustomersOutput
# ImportJob
Source: https://www.plain.com/docs/graphql-reference/types/ImportJob
# ImportJobConnection
Source: https://www.plain.com/docs/graphql-reference/types/ImportJobConnection
# ImportJobDefinition
Source: https://www.plain.com/docs/graphql-reference/types/ImportJobDefinition
# ImportJobDefinitionList
Source: https://www.plain.com/docs/graphql-reference/types/ImportJobDefinitionList
# ImportJobDefinitionMetadata
Source: https://www.plain.com/docs/graphql-reference/types/ImportJobDefinitionMetadata
# ImportJobEdge
Source: https://www.plain.com/docs/graphql-reference/types/ImportJobEdge
# ImportJobsFilter
Source: https://www.plain.com/docs/graphql-reference/types/ImportJobsFilter
# ImportResult
Source: https://www.plain.com/docs/graphql-reference/types/ImportResult
# ImportRun
Source: https://www.plain.com/docs/graphql-reference/types/ImportRun
# ImportSyncFiltersInput
Source: https://www.plain.com/docs/graphql-reference/types/ImportSyncFiltersInput
# ImportTenantFieldSchemaInput
Source: https://www.plain.com/docs/graphql-reference/types/ImportTenantFieldSchemaInput
# ImportTenantFieldSchemasFromServiceInput
Source: https://www.plain.com/docs/graphql-reference/types/ImportTenantFieldSchemasFromServiceInput
# ImportTenantFieldSchemasFromServiceOutput
Source: https://www.plain.com/docs/graphql-reference/types/ImportTenantFieldSchemasFromServiceOutput
# ImportTenantFieldSchemasInput
Source: https://www.plain.com/docs/graphql-reference/types/ImportTenantFieldSchemasInput
# ImportTenantFieldSchemasOutput
Source: https://www.plain.com/docs/graphql-reference/types/ImportTenantFieldSchemasOutput
# ImportTenantFieldValueInput
Source: https://www.plain.com/docs/graphql-reference/types/ImportTenantFieldValueInput
# ImportTenantInput
Source: https://www.plain.com/docs/graphql-reference/types/ImportTenantInput
# ImportTenantsInput
Source: https://www.plain.com/docs/graphql-reference/types/ImportTenantsInput
# ImportTenantsOutput
Source: https://www.plain.com/docs/graphql-reference/types/ImportTenantsOutput
# ImportThreadAssignedToInput
Source: https://www.plain.com/docs/graphql-reference/types/ImportThreadAssignedToInput
Only one of the fields can be set.
# ImportThreadChannelDetails
Source: https://www.plain.com/docs/graphql-reference/types/ImportThreadChannelDetails
# ImportThreadDiscussionInput
Source: https://www.plain.com/docs/graphql-reference/types/ImportThreadDiscussionInput
# ImportThreadDiscussionOutput
Source: https://www.plain.com/docs/graphql-reference/types/ImportThreadDiscussionOutput
# ImportThreadDiscussionSlackDetailsInput
Source: https://www.plain.com/docs/graphql-reference/types/ImportThreadDiscussionSlackDetailsInput
# ImportThreadDiscussionType
Source: https://www.plain.com/docs/graphql-reference/types/ImportThreadDiscussionType
# ImportThreadInput
Source: https://www.plain.com/docs/graphql-reference/types/ImportThreadInput
# ImportThreadMessageAuthorInput
Source: https://www.plain.com/docs/graphql-reference/types/ImportThreadMessageAuthorInput
Only one of customerId or userId must be provided.
# ImportThreadMessageInput
Source: https://www.plain.com/docs/graphql-reference/types/ImportThreadMessageInput
# ImportThreadMessageResult
Source: https://www.plain.com/docs/graphql-reference/types/ImportThreadMessageResult
# ImportThreadMessageType
Source: https://www.plain.com/docs/graphql-reference/types/ImportThreadMessageType
# ImportThreadMessagesInput
Source: https://www.plain.com/docs/graphql-reference/types/ImportThreadMessagesInput
# ImportThreadMessagesOutput
Source: https://www.plain.com/docs/graphql-reference/types/ImportThreadMessagesOutput
# ImportThreadOutput
Source: https://www.plain.com/docs/graphql-reference/types/ImportThreadOutput
# ImportThreadStatusDetail
Source: https://www.plain.com/docs/graphql-reference/types/ImportThreadStatusDetail
# ImportThreadStatusDetailInput
Source: https://www.plain.com/docs/graphql-reference/types/ImportThreadStatusDetailInput
# ImportedThreadMessage
Source: https://www.plain.com/docs/graphql-reference/types/ImportedThreadMessage
# ImporterEntityType
Source: https://www.plain.com/docs/graphql-reference/types/ImporterEntityType
# ImporterStatus
Source: https://www.plain.com/docs/graphql-reference/types/ImporterStatus
# ImporterTenantList
Source: https://www.plain.com/docs/graphql-reference/types/ImporterTenantList
A list or view of tenants/companies available in a connected external service, used to scope which records are synced during an import.
# ImporterTenantListConnection
Source: https://www.plain.com/docs/graphql-reference/types/ImporterTenantListConnection
# ImporterTenantListEdge
Source: https://www.plain.com/docs/graphql-reference/types/ImporterTenantListEdge
# IndexedDocument
Source: https://www.plain.com/docs/graphql-reference/types/IndexedDocument
# IndexedDocumentConnection
Source: https://www.plain.com/docs/graphql-reference/types/IndexedDocumentConnection
# IndexedDocumentEdge
Source: https://www.plain.com/docs/graphql-reference/types/IndexedDocumentEdge
# IndexedDocumentSearchResult
Source: https://www.plain.com/docs/graphql-reference/types/IndexedDocumentSearchResult
# IndexedDocumentStatus
Source: https://www.plain.com/docs/graphql-reference/types/IndexedDocumentStatus
# IndexedDocumentStatusFailed
Source: https://www.plain.com/docs/graphql-reference/types/IndexedDocumentStatusFailed
# IndexedDocumentStatusIndexed
Source: https://www.plain.com/docs/graphql-reference/types/IndexedDocumentStatusIndexed
# IndexedDocumentStatusPending
Source: https://www.plain.com/docs/graphql-reference/types/IndexedDocumentStatusPending
# IndexedDocumentStatusType
Source: https://www.plain.com/docs/graphql-reference/types/IndexedDocumentStatusType
The ingestion status of an indexed document, used to filter documents in the `indexedDocuments` query.
# IndexedDocumentsFilter
Source: https://www.plain.com/docs/graphql-reference/types/IndexedDocumentsFilter
# IndexingStatus
Source: https://www.plain.com/docs/graphql-reference/types/IndexingStatus
# IndexingStatusFailed
Source: https://www.plain.com/docs/graphql-reference/types/IndexingStatusFailed
# IndexingStatusIndexed
Source: https://www.plain.com/docs/graphql-reference/types/IndexingStatusIndexed
# IndexingStatusPending
Source: https://www.plain.com/docs/graphql-reference/types/IndexingStatusPending
# IntArrayInput
Source: https://www.plain.com/docs/graphql-reference/types/IntArrayInput
# IntInput
Source: https://www.plain.com/docs/graphql-reference/types/IntInput
# IntegrationReauthRequiredNotificationDetail
Source: https://www.plain.com/docs/graphql-reference/types/IntegrationReauthRequiredNotificationDetail
An integration's stored credentials can no longer be refreshed, so it must be reconnected
before it will work again.
# InternalActor
Source: https://www.plain.com/docs/graphql-reference/types/InternalActor
# InternalNotification
Source: https://www.plain.com/docs/graphql-reference/types/InternalNotification
An internal notification displayed to workspace members in the Plain app.
Each notification is for a specific user.
# InternalNotificationConnection
Source: https://www.plain.com/docs/graphql-reference/types/InternalNotificationConnection
# InternalNotificationDetail
Source: https://www.plain.com/docs/graphql-reference/types/InternalNotificationDetail
# InternalNotificationEdge
Source: https://www.plain.com/docs/graphql-reference/types/InternalNotificationEdge
# InternalNotificationsFilter
Source: https://www.plain.com/docs/graphql-reference/types/InternalNotificationsFilter
# InviteUserToWorkspaceInput
Source: https://www.plain.com/docs/graphql-reference/types/InviteUserToWorkspaceInput
# InviteUserToWorkspaceOutput
Source: https://www.plain.com/docs/graphql-reference/types/InviteUserToWorkspaceOutput
# IssueTrackerField
Source: https://www.plain.com/docs/graphql-reference/types/IssueTrackerField
# IssueTrackerFieldInput
Source: https://www.plain.com/docs/graphql-reference/types/IssueTrackerFieldInput
# IssueTrackerFieldOption
Source: https://www.plain.com/docs/graphql-reference/types/IssueTrackerFieldOption
# IssueTrackerFieldType
Source: https://www.plain.com/docs/graphql-reference/types/IssueTrackerFieldType
# JiraIntegrationToken
Source: https://www.plain.com/docs/graphql-reference/types/JiraIntegrationToken
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.
# JiraIssueThreadLink
Source: https://www.plain.com/docs/graphql-reference/types/JiraIssueThreadLink
# JiraIssueThreadLinkInput
Source: https://www.plain.com/docs/graphql-reference/types/JiraIssueThreadLinkInput
# JiraIssueType
Source: https://www.plain.com/docs/graphql-reference/types/JiraIssueType
# JiraSite
Source: https://www.plain.com/docs/graphql-reference/types/JiraSite
# JiraSiteIntegration
Source: https://www.plain.com/docs/graphql-reference/types/JiraSiteIntegration
# KnowledgeDocument
Source: https://www.plain.com/docs/graphql-reference/types/KnowledgeDocument
A single unit of indexed, retrievable, citable knowledge (the content), as opposed to a
KnowledgeSource which is the configuration of where that content is ingested from. Reusable
anywhere a knowledge document is returned, e.g. citations today and knowledge document listings
in future.
# KnowledgeGap
Source: https://www.plain.com/docs/graphql-reference/types/KnowledgeGap
# KnowledgeGapConnection
Source: https://www.plain.com/docs/graphql-reference/types/KnowledgeGapConnection
# KnowledgeGapEdge
Source: https://www.plain.com/docs/graphql-reference/types/KnowledgeGapEdge
# KnowledgeGapFeedbackDetails
Source: https://www.plain.com/docs/graphql-reference/types/KnowledgeGapFeedbackDetails
# KnowledgeGapSignal
Source: https://www.plain.com/docs/graphql-reference/types/KnowledgeGapSignal
# KnowledgeGapSignalConnection
Source: https://www.plain.com/docs/graphql-reference/types/KnowledgeGapSignalConnection
# KnowledgeGapSignalEdge
Source: https://www.plain.com/docs/graphql-reference/types/KnowledgeGapSignalEdge
# KnowledgeGapSignalType
Source: https://www.plain.com/docs/graphql-reference/types/KnowledgeGapSignalType
# KnowledgeGapStatus
Source: https://www.plain.com/docs/graphql-reference/types/KnowledgeGapStatus
# KnowledgeGapTaskLink
Source: https://www.plain.com/docs/graphql-reference/types/KnowledgeGapTaskLink
# KnowledgeGapsFilter
Source: https://www.plain.com/docs/graphql-reference/types/KnowledgeGapsFilter
# KnowledgeGapsSort
Source: https://www.plain.com/docs/graphql-reference/types/KnowledgeGapsSort
# KnowledgeGapsSortField
Source: https://www.plain.com/docs/graphql-reference/types/KnowledgeGapsSortField
# KnowledgeSource
Source: https://www.plain.com/docs/graphql-reference/types/KnowledgeSource
# KnowledgeSourceCitation
Source: https://www.plain.com/docs/graphql-reference/types/KnowledgeSourceCitation
A knowledge document that an AI agent reply cited, linked to the timeline entry it was cited in.
One reply can produce multiple citations; each is returned as its own entry.
# KnowledgeSourceCitationType
Source: https://www.plain.com/docs/graphql-reference/types/KnowledgeSourceCitationType
The kind of knowledge source an AI agent citation points at.
# KnowledgeSourceConnection
Source: https://www.plain.com/docs/graphql-reference/types/KnowledgeSourceConnection
# KnowledgeSourceEdge
Source: https://www.plain.com/docs/graphql-reference/types/KnowledgeSourceEdge
# KnowledgeSourceSearchResult
Source: https://www.plain.com/docs/graphql-reference/types/KnowledgeSourceSearchResult
# KnowledgeSourceSearchResultType
Source: https://www.plain.com/docs/graphql-reference/types/KnowledgeSourceSearchResultType
# KnowledgeSourceSitemap
Source: https://www.plain.com/docs/graphql-reference/types/KnowledgeSourceSitemap
# KnowledgeSourceSitemapIndexedDocumentCounts
Source: https://www.plain.com/docs/graphql-reference/types/KnowledgeSourceSitemapIndexedDocumentCounts
Counts of a sitemap's crawled URLs, broken down by indexing status.
# KnowledgeSourceType
Source: https://www.plain.com/docs/graphql-reference/types/KnowledgeSourceType
# KnowledgeSourceUrl
Source: https://www.plain.com/docs/graphql-reference/types/KnowledgeSourceUrl
# KnowledgeSourcesFilter
Source: https://www.plain.com/docs/graphql-reference/types/KnowledgeSourcesFilter
# Label
Source: https://www.plain.com/docs/graphql-reference/types/Label
A label applied to a thread or user. Each label is an instance of a LabelType and records who applied it and when.
# LabelType
Source: https://www.plain.com/docs/graphql-reference/types/LabelType
# LabelTypeConnection
Source: https://www.plain.com/docs/graphql-reference/types/LabelTypeConnection
# LabelTypeEdge
Source: https://www.plain.com/docs/graphql-reference/types/LabelTypeEdge
# LabelTypeFilter
Source: https://www.plain.com/docs/graphql-reference/types/LabelTypeFilter
# LabelTypeType
Source: https://www.plain.com/docs/graphql-reference/types/LabelTypeType
# LinearIntegrationToken
Source: https://www.plain.com/docs/graphql-reference/types/LinearIntegrationToken
# LinearIssueState
Source: https://www.plain.com/docs/graphql-reference/types/LinearIssueState
# LinearIssueStateType
Source: https://www.plain.com/docs/graphql-reference/types/LinearIssueStateType
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
# LinearIssueThreadLink
Source: https://www.plain.com/docs/graphql-reference/types/LinearIssueThreadLink
# LinearIssueThreadLinkInput
Source: https://www.plain.com/docs/graphql-reference/types/LinearIssueThreadLinkInput
# LinearIssueThreadLinkStateTransitionedEntry
Source: https://www.plain.com/docs/graphql-reference/types/LinearIssueThreadLinkStateTransitionedEntry
# LockThreadInput
Source: https://www.plain.com/docs/graphql-reference/types/LockThreadInput
# LockThreadOutput
Source: https://www.plain.com/docs/graphql-reference/types/LockThreadOutput
# MSTeamsChannelMember
Source: https://www.plain.com/docs/graphql-reference/types/MSTeamsChannelMember
# MSTeamsChannelMemberRole
Source: https://www.plain.com/docs/graphql-reference/types/MSTeamsChannelMemberRole
# MSTeamsChannelMembers
Source: https://www.plain.com/docs/graphql-reference/types/MSTeamsChannelMembers
# MSTeamsMessage
Source: https://www.plain.com/docs/graphql-reference/types/MSTeamsMessage
# MSTeamsMessageEntry
Source: https://www.plain.com/docs/graphql-reference/types/MSTeamsMessageEntry
# MSTeamsMessageType
Source: https://www.plain.com/docs/graphql-reference/types/MSTeamsMessageType
# MSTeamsThreadChannelDetails
Source: https://www.plain.com/docs/graphql-reference/types/MSTeamsThreadChannelDetails
# MSTeamsThreadChannelDetailsInput
Source: https://www.plain.com/docs/graphql-reference/types/MSTeamsThreadChannelDetailsInput
# MachineUser
Source: https://www.plain.com/docs/graphql-reference/types/MachineUser
# MachineUserActor
Source: https://www.plain.com/docs/graphql-reference/types/MachineUserActor
# MachineUserConnection
Source: https://www.plain.com/docs/graphql-reference/types/MachineUserConnection
# MachineUserEdge
Source: https://www.plain.com/docs/graphql-reference/types/MachineUserEdge
# MachineUserType
Source: https://www.plain.com/docs/graphql-reference/types/MachineUserType
# MachineUsersFilter
Source: https://www.plain.com/docs/graphql-reference/types/MachineUsersFilter
# MarkCustomerAsSpamInput
Source: https://www.plain.com/docs/graphql-reference/types/MarkCustomerAsSpamInput
# MarkCustomerAsSpamOutput
Source: https://www.plain.com/docs/graphql-reference/types/MarkCustomerAsSpamOutput
# MarkThreadAsDoneInput
Source: https://www.plain.com/docs/graphql-reference/types/MarkThreadAsDoneInput
# MarkThreadAsDoneOutput
Source: https://www.plain.com/docs/graphql-reference/types/MarkThreadAsDoneOutput
# MarkThreadAsTodoInput
Source: https://www.plain.com/docs/graphql-reference/types/MarkThreadAsTodoInput
# MarkThreadAsTodoOutput
Source: https://www.plain.com/docs/graphql-reference/types/MarkThreadAsTodoOutput
# MarkThreadDiscussionReadInput
Source: https://www.plain.com/docs/graphql-reference/types/MarkThreadDiscussionReadInput
# MarkThreadDiscussionReadOutput
Source: https://www.plain.com/docs/graphql-reference/types/MarkThreadDiscussionReadOutput
# MentionInput
Source: https://www.plain.com/docs/graphql-reference/types/MentionInput
# MergedThreadMessageEntry
Source: https://www.plain.com/docs/graphql-reference/types/MergedThreadMessageEntry
# MessageSource
Source: https://www.plain.com/docs/graphql-reference/types/MessageSource
# MeteredFeatureEntitlement
Source: https://www.plain.com/docs/graphql-reference/types/MeteredFeatureEntitlement
# MinimalThreadWithDistance
Source: https://www.plain.com/docs/graphql-reference/types/MinimalThreadWithDistance
# MintEmbedTokenInput
Source: https://www.plain.com/docs/graphql-reference/types/MintEmbedTokenInput
# MintEmbedTokenOutput
Source: https://www.plain.com/docs/graphql-reference/types/MintEmbedTokenOutput
# MoveLabelTypeInput
Source: https://www.plain.com/docs/graphql-reference/types/MoveLabelTypeInput
# MoveLabelTypeOutput
Source: https://www.plain.com/docs/graphql-reference/types/MoveLabelTypeOutput
# MoveWorkflowInput
Source: https://www.plain.com/docs/graphql-reference/types/MoveWorkflowInput
# MoveWorkflowOutput
Source: https://www.plain.com/docs/graphql-reference/types/MoveWorkflowOutput
# MutationError
Source: https://www.plain.com/docs/graphql-reference/types/MutationError
A type indicating an error has occurred while making a mutation.
# MutationErrorType
Source: https://www.plain.com/docs/graphql-reference/types/MutationErrorType
An enum for why the mutation failed overall.
# MutationFieldError
Source: https://www.plain.com/docs/graphql-reference/types/MutationFieldError
A type indicating an error has occurred with a specific field in the input.
# MutationFieldErrorType
Source: https://www.plain.com/docs/graphql-reference/types/MutationFieldErrorType
An enum specific to each field, explaining why validation failed.
# NextResponseTimeServiceLevelAgreement
Source: https://www.plain.com/docs/graphql-reference/types/NextResponseTimeServiceLevelAgreement
An SLA that tracks the time from each new customer message until a teammate replies. On a tier, this requires a first-response-time SLA to exist on the same tier; in an SLA policy it stands on its own.
# Note
Source: https://www.plain.com/docs/graphql-reference/types/Note
# NoteEntry
Source: https://www.plain.com/docs/graphql-reference/types/NoteEntry
# NoteMentionNotificationDetail
Source: https://www.plain.com/docs/graphql-reference/types/NoteMentionNotificationDetail
# NumberSetting
Source: https://www.plain.com/docs/graphql-reference/types/NumberSetting
A number setting
# OptionalBooleanInput
Source: https://www.plain.com/docs/graphql-reference/types/OptionalBooleanInput
# OptionalDateTimeInput
Source: https://www.plain.com/docs/graphql-reference/types/OptionalDateTimeInput
# OptionalDependsOnThreadFieldInput
Source: https://www.plain.com/docs/graphql-reference/types/OptionalDependsOnThreadFieldInput
# OptionalFloatInput
Source: https://www.plain.com/docs/graphql-reference/types/OptionalFloatInput
# OptionalGeneratedReplyFeedbackInput
Source: https://www.plain.com/docs/graphql-reference/types/OptionalGeneratedReplyFeedbackInput
# OptionalStringInput
Source: https://www.plain.com/docs/graphql-reference/types/OptionalStringInput
# PageInfo
Source: https://www.plain.com/docs/graphql-reference/types/PageInfo
# PerSeatRecurringPrice
Source: https://www.plain.com/docs/graphql-reference/types/PerSeatRecurringPrice
# Permissions
Source: https://www.plain.com/docs/graphql-reference/types/Permissions
A list of permission strings granted to a user or machine user in the current workspace context.
# PlainHelpCenterArticleSearchResult
Source: https://www.plain.com/docs/graphql-reference/types/PlainHelpCenterArticleSearchResult
A result from a semantic search over Plain's own public help center (help.plain.com).
# PlainTaskThreadLink
Source: https://www.plain.com/docs/graphql-reference/types/PlainTaskThreadLink
# PlainTaskThreadLinkInput
Source: https://www.plain.com/docs/graphql-reference/types/PlainTaskThreadLinkInput
# PlainThreadLinkInput
Source: https://www.plain.com/docs/graphql-reference/types/PlainThreadLinkInput
# PlainThreadThreadLink
Source: https://www.plain.com/docs/graphql-reference/types/PlainThreadThreadLink
# PlainWorkspaceBroadcastSender
Source: https://www.plain.com/docs/graphql-reference/types/PlainWorkspaceBroadcastSender
The workspace itself, rather than any one person. Its name and logo are applied to the Slack
message, so the broadcast arrives from the company rather than from Plain.
# PreviewBillingPlanChangeInput
Source: https://www.plain.com/docs/graphql-reference/types/PreviewBillingPlanChangeInput
# PreviewBillingPlanChangeOutput
Source: https://www.plain.com/docs/graphql-reference/types/PreviewBillingPlanChangeOutput
# Price
Source: https://www.plain.com/docs/graphql-reference/types/Price
# PriceTier
Source: https://www.plain.com/docs/graphql-reference/types/PriceTier
# PurchaseCreditsInput
Source: https://www.plain.com/docs/graphql-reference/types/PurchaseCreditsInput
# PurchaseCreditsOutput
Source: https://www.plain.com/docs/graphql-reference/types/PurchaseCreditsOutput
# QueuedAgentSessionMessage
Source: https://www.plain.com/docs/graphql-reference/types/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.
# RecurringPrice
Source: https://www.plain.com/docs/graphql-reference/types/RecurringPrice
# RefreshConnectedDiscordChannelsInput
Source: https://www.plain.com/docs/graphql-reference/types/RefreshConnectedDiscordChannelsInput
# RefreshConnectedDiscordChannelsOutput
Source: https://www.plain.com/docs/graphql-reference/types/RefreshConnectedDiscordChannelsOutput
# RefreshSidekickMcpServerToolsInput
Source: https://www.plain.com/docs/graphql-reference/types/RefreshSidekickMcpServerToolsInput
# RefreshSidekickMcpServerToolsOutput
Source: https://www.plain.com/docs/graphql-reference/types/RefreshSidekickMcpServerToolsOutput
# RefreshWorkspaceSlackChannelIntegrationInput
Source: https://www.plain.com/docs/graphql-reference/types/RefreshWorkspaceSlackChannelIntegrationInput
# RefreshWorkspaceSlackChannelIntegrationOutput
Source: https://www.plain.com/docs/graphql-reference/types/RefreshWorkspaceSlackChannelIntegrationOutput
# RefreshWorkspaceSlackSidekickIntegrationInput
Source: https://www.plain.com/docs/graphql-reference/types/RefreshWorkspaceSlackSidekickIntegrationInput
# RefreshWorkspaceSlackSidekickIntegrationOutput
Source: https://www.plain.com/docs/graphql-reference/types/RefreshWorkspaceSlackSidekickIntegrationOutput
# RegenerateWorkspaceHmacOutput
Source: https://www.plain.com/docs/graphql-reference/types/RegenerateWorkspaceHmacOutput
# ReindexKnowledgeSourceInput
Source: https://www.plain.com/docs/graphql-reference/types/ReindexKnowledgeSourceInput
# ReindexKnowledgeSourceOutput
Source: https://www.plain.com/docs/graphql-reference/types/ReindexKnowledgeSourceOutput
# ReloadCustomerCardInstanceInput
Source: https://www.plain.com/docs/graphql-reference/types/ReloadCustomerCardInstanceInput
# ReloadCustomerCardInstanceOutput
Source: https://www.plain.com/docs/graphql-reference/types/ReloadCustomerCardInstanceOutput
# RemoveAdditionalAssigneesInput
Source: https://www.plain.com/docs/graphql-reference/types/RemoveAdditionalAssigneesInput
# RemoveAdditionalAssigneesOutput
Source: https://www.plain.com/docs/graphql-reference/types/RemoveAdditionalAssigneesOutput
# RemoveCustomerFromCustomerGroupsInput
Source: https://www.plain.com/docs/graphql-reference/types/RemoveCustomerFromCustomerGroupsInput
# RemoveCustomerFromCustomerGroupsOutput
Source: https://www.plain.com/docs/graphql-reference/types/RemoveCustomerFromCustomerGroupsOutput
# RemoveCustomerFromTenantsInput
Source: https://www.plain.com/docs/graphql-reference/types/RemoveCustomerFromTenantsInput
# RemoveCustomerFromTenantsOutput
Source: https://www.plain.com/docs/graphql-reference/types/RemoveCustomerFromTenantsOutput
# RemoveLabelsFromUserInput
Source: https://www.plain.com/docs/graphql-reference/types/RemoveLabelsFromUserInput
# RemoveLabelsFromUserOutput
Source: https://www.plain.com/docs/graphql-reference/types/RemoveLabelsFromUserOutput
# RemoveLabelsInput
Source: https://www.plain.com/docs/graphql-reference/types/RemoveLabelsInput
# RemoveLabelsOutput
Source: https://www.plain.com/docs/graphql-reference/types/RemoveLabelsOutput
# RemoveMembersFromTierInput
Source: https://www.plain.com/docs/graphql-reference/types/RemoveMembersFromTierInput
# RemoveMembersFromTierOutput
Source: https://www.plain.com/docs/graphql-reference/types/RemoveMembersFromTierOutput
# RemoveTenantFieldSchemaMappingInput
Source: https://www.plain.com/docs/graphql-reference/types/RemoveTenantFieldSchemaMappingInput
# RemoveTenantFieldSchemaMappingOutput
Source: https://www.plain.com/docs/graphql-reference/types/RemoveTenantFieldSchemaMappingOutput
# RemoveUserFromActiveBillingRotaInput
Source: https://www.plain.com/docs/graphql-reference/types/RemoveUserFromActiveBillingRotaInput
# RemoveUserFromActiveBillingRotaOutput
Source: https://www.plain.com/docs/graphql-reference/types/RemoveUserFromActiveBillingRotaOutput
# RemoveWorkspaceAlternateSupportEmailAddressInput
Source: https://www.plain.com/docs/graphql-reference/types/RemoveWorkspaceAlternateSupportEmailAddressInput
# RemoveWorkspaceAlternateSupportEmailAddressOutput
Source: https://www.plain.com/docs/graphql-reference/types/RemoveWorkspaceAlternateSupportEmailAddressOutput
# ReorderAutorespondersInput
Source: https://www.plain.com/docs/graphql-reference/types/ReorderAutorespondersInput
# ReorderAutorespondersOutput
Source: https://www.plain.com/docs/graphql-reference/types/ReorderAutorespondersOutput
# ReorderCustomerCardConfigsInput
Source: https://www.plain.com/docs/graphql-reference/types/ReorderCustomerCardConfigsInput
# ReorderCustomerCardConfigsOutput
Source: https://www.plain.com/docs/graphql-reference/types/ReorderCustomerCardConfigsOutput
# ReorderCustomerSurveysInput
Source: https://www.plain.com/docs/graphql-reference/types/ReorderCustomerSurveysInput
# ReorderCustomerSurveysOutput
Source: https://www.plain.com/docs/graphql-reference/types/ReorderCustomerSurveysOutput
# ReorderThreadFieldSchemasInput
Source: https://www.plain.com/docs/graphql-reference/types/ReorderThreadFieldSchemasInput
# ReorderThreadFieldSchemasOutput
Source: https://www.plain.com/docs/graphql-reference/types/ReorderThreadFieldSchemasOutput
# ReplyToEmailInput
Source: https://www.plain.com/docs/graphql-reference/types/ReplyToEmailInput
# ReplyToEmailOutput
Source: https://www.plain.com/docs/graphql-reference/types/ReplyToEmailOutput
# ReplyToThreadChannelSpecificOptionsInput
Source: https://www.plain.com/docs/graphql-reference/types/ReplyToThreadChannelSpecificOptionsInput
# ReplyToThreadEmailChannelSpecificOptionsInput
Source: https://www.plain.com/docs/graphql-reference/types/ReplyToThreadEmailChannelSpecificOptionsInput
# ReplyToThreadInput
Source: https://www.plain.com/docs/graphql-reference/types/ReplyToThreadInput
# ReplyToThreadOutput
Source: https://www.plain.com/docs/graphql-reference/types/ReplyToThreadOutput
# ResolveAgentApprovalInput
Source: https://www.plain.com/docs/graphql-reference/types/ResolveAgentApprovalInput
# ResolveAgentApprovalOutput
Source: https://www.plain.com/docs/graphql-reference/types/ResolveAgentApprovalOutput
# ResolveCustomerForMSTeamsChannelInput
Source: https://www.plain.com/docs/graphql-reference/types/ResolveCustomerForMSTeamsChannelInput
# ResolveCustomerForMSTeamsChannelOutput
Source: https://www.plain.com/docs/graphql-reference/types/ResolveCustomerForMSTeamsChannelOutput
# ResolveCustomerForSlackChannelInput
Source: https://www.plain.com/docs/graphql-reference/types/ResolveCustomerForSlackChannelInput
# ResolveCustomerForSlackChannelOutput
Source: https://www.plain.com/docs/graphql-reference/types/ResolveCustomerForSlackChannelOutput
# Role
Source: https://www.plain.com/docs/graphql-reference/types/Role
# RoleChangeCost
Source: https://www.plain.com/docs/graphql-reference/types/RoleChangeCost
# RoleConnection
Source: https://www.plain.com/docs/graphql-reference/types/RoleConnection
# RoleEdge
Source: https://www.plain.com/docs/graphql-reference/types/RoleEdge
# RoleFilter
Source: https://www.plain.com/docs/graphql-reference/types/RoleFilter
# RoleKey
Source: https://www.plain.com/docs/graphql-reference/types/RoleKey
Stable identifiers for the built-in workspace roles.
# RoleScope
Source: https://www.plain.com/docs/graphql-reference/types/RoleScope
# RoleScopeAccessMode
Source: https://www.plain.com/docs/graphql-reference/types/RoleScopeAccessMode
# RoleScopeDefinition
Source: https://www.plain.com/docs/graphql-reference/types/RoleScopeDefinition
# RoleScopeResourceType
Source: https://www.plain.com/docs/graphql-reference/types/RoleScopeResourceType
The type of resource that a role scope applies to. Currently only threads are supported.
# SavedThreadsView
Source: https://www.plain.com/docs/graphql-reference/types/SavedThreadsView
# SavedThreadsViewConnection
Source: https://www.plain.com/docs/graphql-reference/types/SavedThreadsViewConnection
# SavedThreadsViewEdge
Source: https://www.plain.com/docs/graphql-reference/types/SavedThreadsViewEdge
# SavedThreadsViewFilter
Source: https://www.plain.com/docs/graphql-reference/types/SavedThreadsViewFilter
# SavedThreadsViewFilterInput
Source: https://www.plain.com/docs/graphql-reference/types/SavedThreadsViewFilterInput
# SavedThreadsViewFilterTenantField
Source: https://www.plain.com/docs/graphql-reference/types/SavedThreadsViewFilterTenantField
# SavedThreadsViewFilterThreadField
Source: https://www.plain.com/docs/graphql-reference/types/SavedThreadsViewFilterThreadField
# SavedThreadsViewFilterThreadFieldDate
Source: https://www.plain.com/docs/graphql-reference/types/SavedThreadsViewFilterThreadFieldDate
# SavedThreadsViewFilterThreadFieldNumber
Source: https://www.plain.com/docs/graphql-reference/types/SavedThreadsViewFilterThreadFieldNumber
# SavedThreadsViewFilterThreadLinkSource
Source: https://www.plain.com/docs/graphql-reference/types/SavedThreadsViewFilterThreadLinkSource
Filters threads by a specific external link source, combining the source system type and the source's identifier.
# SavedThreadsViewNestedFilter
Source: https://www.plain.com/docs/graphql-reference/types/SavedThreadsViewNestedFilter
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.
# SavedThreadsViewSort
Source: https://www.plain.com/docs/graphql-reference/types/SavedThreadsViewSort
# ScheduleBroadcastInput
Source: https://www.plain.com/docs/graphql-reference/types/ScheduleBroadcastInput
# ScheduleBroadcastOutput
Source: https://www.plain.com/docs/graphql-reference/types/ScheduleBroadcastOutput
# ScopeConditionInput
Source: https://www.plain.com/docs/graphql-reference/types/ScopeConditionInput
# SearchKnowledgeSourcesOptions
Source: https://www.plain.com/docs/graphql-reference/types/SearchKnowledgeSourcesOptions
# SelectedIssueTrackerField
Source: https://www.plain.com/docs/graphql-reference/types/SelectedIssueTrackerField
# SendBulkEmailInput
Source: https://www.plain.com/docs/graphql-reference/types/SendBulkEmailInput
# SendBulkEmailOutput
Source: https://www.plain.com/docs/graphql-reference/types/SendBulkEmailOutput
# SendBulkEmailSkippedThread
Source: https://www.plain.com/docs/graphql-reference/types/SendBulkEmailSkippedThread
# SendBulkEmailSkippedThreadReason
Source: https://www.plain.com/docs/graphql-reference/types/SendBulkEmailSkippedThreadReason
# SendChatInput
Source: https://www.plain.com/docs/graphql-reference/types/SendChatInput
# SendChatOutput
Source: https://www.plain.com/docs/graphql-reference/types/SendChatOutput
# SendCustomerChatInput
Source: https://www.plain.com/docs/graphql-reference/types/SendCustomerChatInput
# SendCustomerChatOutput
Source: https://www.plain.com/docs/graphql-reference/types/SendCustomerChatOutput
# SendDiscordMessageInput
Source: https://www.plain.com/docs/graphql-reference/types/SendDiscordMessageInput
# SendDiscordMessageOutput
Source: https://www.plain.com/docs/graphql-reference/types/SendDiscordMessageOutput
# SendDiscussionMessageInput
Source: https://www.plain.com/docs/graphql-reference/types/SendDiscussionMessageInput
# SendDiscussionMessageOutput
Source: https://www.plain.com/docs/graphql-reference/types/SendDiscussionMessageOutput
# SendMSTeamsMessageInput
Source: https://www.plain.com/docs/graphql-reference/types/SendMSTeamsMessageInput
# SendMSTeamsMessageOutput
Source: https://www.plain.com/docs/graphql-reference/types/SendMSTeamsMessageOutput
# SendNewEmailInput
Source: https://www.plain.com/docs/graphql-reference/types/SendNewEmailInput
# SendNewEmailOutput
Source: https://www.plain.com/docs/graphql-reference/types/SendNewEmailOutput
# SendSlackMessageInput
Source: https://www.plain.com/docs/graphql-reference/types/SendSlackMessageInput
# SendSlackMessageOutput
Source: https://www.plain.com/docs/graphql-reference/types/SendSlackMessageOutput
# SendTestBroadcastInput
Source: https://www.plain.com/docs/graphql-reference/types/SendTestBroadcastInput
# SendTestBroadcastOutput
Source: https://www.plain.com/docs/graphql-reference/types/SendTestBroadcastOutput
# SendThreadDiscussionMessageChannelDetailsInput
Source: https://www.plain.com/docs/graphql-reference/types/SendThreadDiscussionMessageChannelDetailsInput
# SendThreadDiscussionMessageInput
Source: https://www.plain.com/docs/graphql-reference/types/SendThreadDiscussionMessageInput
# SendThreadDiscussionMessageOutput
Source: https://www.plain.com/docs/graphql-reference/types/SendThreadDiscussionMessageOutput
# SendThreadDiscussionMessageSlackChannelDetailsInput
Source: https://www.plain.com/docs/graphql-reference/types/SendThreadDiscussionMessageSlackChannelDetailsInput
# SentimentType
Source: https://www.plain.com/docs/graphql-reference/types/SentimentType
# ServiceAuthorization
Source: https://www.plain.com/docs/graphql-reference/types/ServiceAuthorization
# ServiceAuthorizationConnection
Source: https://www.plain.com/docs/graphql-reference/types/ServiceAuthorizationConnection
# ServiceAuthorizationConnectionDetails
Source: https://www.plain.com/docs/graphql-reference/types/ServiceAuthorizationConnectionDetails
# ServiceAuthorizationEdge
Source: https://www.plain.com/docs/graphql-reference/types/ServiceAuthorizationEdge
# ServiceAuthorizationStatus
Source: https://www.plain.com/docs/graphql-reference/types/ServiceAuthorizationStatus
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.
# ServiceAuthorizationsFilter
Source: https://www.plain.com/docs/graphql-reference/types/ServiceAuthorizationsFilter
# ServiceIntegration
Source: https://www.plain.com/docs/graphql-reference/types/ServiceIntegration
# ServiceLevelAgreement
Source: https://www.plain.com/docs/graphql-reference/types/ServiceLevelAgreement
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.
# ServiceLevelAgreementFilter
Source: https://www.plain.com/docs/graphql-reference/types/ServiceLevelAgreementFilter
# ServiceLevelAgreementInput
Source: https://www.plain.com/docs/graphql-reference/types/ServiceLevelAgreementInput
# ServiceLevelAgreementPauseStatusDetailType
Source: https://www.plain.com/docs/graphql-reference/types/ServiceLevelAgreementPauseStatusDetailType
Thread status details a resolution-time SLA can pause on.
# ServiceLevelAgreementPauseStatusDetailTypeArrayInput
Source: https://www.plain.com/docs/graphql-reference/types/ServiceLevelAgreementPauseStatusDetailTypeArrayInput
# ServiceLevelAgreementPoliciesFilter
Source: https://www.plain.com/docs/graphql-reference/types/ServiceLevelAgreementPoliciesFilter
Narrows the SLA policy list. Omit a field to leave that dimension unconstrained.
# ServiceLevelAgreementPolicy
Source: https://www.plain.com/docs/graphql-reference/types/ServiceLevelAgreementPolicy
A named group of SLA targets that a workflow or a user applies to a thread. Unlike tier SLAs, a policy's targets are selected by type alone, so a policy holds at most one target per type.
# ServiceLevelAgreementPolicyConnection
Source: https://www.plain.com/docs/graphql-reference/types/ServiceLevelAgreementPolicyConnection
# ServiceLevelAgreementPolicyEdge
Source: https://www.plain.com/docs/graphql-reference/types/ServiceLevelAgreementPolicyEdge
# ServiceLevelAgreementPolicyRef
Source: https://www.plain.com/docs/graphql-reference/types/ServiceLevelAgreementPolicyRef
An SLA policy as it was when a timeline entry was written. Policies can be renamed or deleted afterwards, so this is a snapshot rather than a link.
# ServiceLevelAgreementPolicyTarget
Source: https://www.plain.com/docs/graphql-reference/types/ServiceLevelAgreementPolicyTarget
One time-to-target promise inside an SLA policy. A policy holds at most one target of each type.
# ServiceLevelAgreementPolicyTargetInput
Source: https://www.plain.com/docs/graphql-reference/types/ServiceLevelAgreementPolicyTargetInput
# ServiceLevelAgreementStatus
Source: https://www.plain.com/docs/graphql-reference/types/ServiceLevelAgreementStatus
The lifecycle status of an SLA tracker for a given thread.
# ServiceLevelAgreementStatusDetail
Source: https://www.plain.com/docs/graphql-reference/types/ServiceLevelAgreementStatusDetail
# ServiceLevelAgreementStatusDetailAchieved
Source: https://www.plain.com/docs/graphql-reference/types/ServiceLevelAgreementStatusDetailAchieved
# ServiceLevelAgreementStatusDetailBreached
Source: https://www.plain.com/docs/graphql-reference/types/ServiceLevelAgreementStatusDetailBreached
# ServiceLevelAgreementStatusDetailBreaching
Source: https://www.plain.com/docs/graphql-reference/types/ServiceLevelAgreementStatusDetailBreaching
# ServiceLevelAgreementStatusDetailCancelled
Source: https://www.plain.com/docs/graphql-reference/types/ServiceLevelAgreementStatusDetailCancelled
# ServiceLevelAgreementStatusDetailImminentBreach
Source: https://www.plain.com/docs/graphql-reference/types/ServiceLevelAgreementStatusDetailImminentBreach
# ServiceLevelAgreementStatusDetailPending
Source: https://www.plain.com/docs/graphql-reference/types/ServiceLevelAgreementStatusDetailPending
# ServiceLevelAgreementStatusSummary
Source: https://www.plain.com/docs/graphql-reference/types/ServiceLevelAgreementStatusSummary
A snapshot of the current SLA tracking status for a thread, broken down by SLA type.
# ServiceLevelAgreementStatusTransitionedEntry
Source: https://www.plain.com/docs/graphql-reference/types/ServiceLevelAgreementStatusTransitionedEntry
A timeline entry recording a change in the SLA tracking status for a thread.
# ServiceLevelAgreementThreadLabelTypeIdFilter
Source: https://www.plain.com/docs/graphql-reference/types/ServiceLevelAgreementThreadLabelTypeIdFilter
Restricts which threads an SLA applies to based on their label types.
# ServiceLevelAgreementThreadLabelTypeIdFilterInput
Source: https://www.plain.com/docs/graphql-reference/types/ServiceLevelAgreementThreadLabelTypeIdFilterInput
# ServiceLevelAgreementType
Source: https://www.plain.com/docs/graphql-reference/types/ServiceLevelAgreementType
# ServiceLevelAgreementWarnBefore
Source: https://www.plain.com/docs/graphql-reference/types/ServiceLevelAgreementWarnBefore
How far ahead of an SLA target's deadline the thread is treated as close to breaching.
# ServiceLevelAgreementWarnBeforeInput
Source: https://www.plain.com/docs/graphql-reference/types/ServiceLevelAgreementWarnBeforeInput
# SetCustomerTenantsInput
Source: https://www.plain.com/docs/graphql-reference/types/SetCustomerTenantsInput
# SetCustomerTenantsOutput
Source: https://www.plain.com/docs/graphql-reference/types/SetCustomerTenantsOutput
# SetSlackAutoJoinRulesInput
Source: https://www.plain.com/docs/graphql-reference/types/SetSlackAutoJoinRulesInput
An input provided to the `setSlackAutoJoinRules` mutation.
# SetSlackAutoJoinRulesOutput
Source: https://www.plain.com/docs/graphql-reference/types/SetSlackAutoJoinRulesOutput
An output type provided by the `setSlackAutoJoinRules` mutation.
# Setting
Source: https://www.plain.com/docs/graphql-reference/types/Setting
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.
# SettingScope
Source: https://www.plain.com/docs/graphql-reference/types/SettingScope
Identifies the context to which a setting value is attached. The combination of `scopeType` and `id` uniquely addresses one setting row.
# SettingScopeInput
Source: https://www.plain.com/docs/graphql-reference/types/SettingScopeInput
An input to specify the scope for a setting.
# SettingScopeType
Source: https://www.plain.com/docs/graphql-reference/types/SettingScopeType
An enum to describe the type of scope the setting is for.
# SettingValueInput
Source: https://www.plain.com/docs/graphql-reference/types/SettingValueInput
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.
# SetupTenantFieldSchemaMappingInput
Source: https://www.plain.com/docs/graphql-reference/types/SetupTenantFieldSchemaMappingInput
# SetupTenantFieldSchemaMappingOutput
Source: https://www.plain.com/docs/graphql-reference/types/SetupTenantFieldSchemaMappingOutput
# ShareThreadToUserInSlackInput
Source: https://www.plain.com/docs/graphql-reference/types/ShareThreadToUserInSlackInput
# ShareThreadToUserInSlackOutput
Source: https://www.plain.com/docs/graphql-reference/types/ShareThreadToUserInSlackOutput
# SidekickConnectRequestStatus
Source: https://www.plain.com/docs/graphql-reference/types/SidekickConnectRequestStatus
# SidekickConnectRequestTool
Source: https://www.plain.com/docs/graphql-reference/types/SidekickConnectRequestTool
# SidekickCreditAllowanceBalance
Source: https://www.plain.com/docs/graphql-reference/types/SidekickCreditAllowanceBalance
The monthly allowance credit bucket.
# SidekickCreditBalance
Source: https://www.plain.com/docs/graphql-reference/types/SidekickCreditBalance
The workspace's current Sidekick (Plain) credit balance.
# SidekickCreditBalanceStatus
Source: https://www.plain.com/docs/graphql-reference/types/SidekickCreditBalanceStatus
Whether metered Sidekick credits apply to the workspace.
# SidekickCreditTopupBalance
Source: https://www.plain.com/docs/graphql-reference/types/SidekickCreditTopupBalance
The purchased top-up credit bucket.
# SidekickCreditUsageByDay
Source: https://www.plain.com/docs/graphql-reference/types/SidekickCreditUsageByDay
Credit usage broken down by UTC day.
# SidekickCreditUsageByDayInput
Source: https://www.plain.com/docs/graphql-reference/types/SidekickCreditUsageByDayInput
# SidekickCreditUsageDay
Source: https://www.plain.com/docs/graphql-reference/types/SidekickCreditUsageDay
Credit usage for a single UTC day.
# SidekickCustomSkill
Source: https://www.plain.com/docs/graphql-reference/types/SidekickCustomSkill
A workspace-authored Sidekick skill, including its instructions body.
# SidekickGithubRepo
Source: https://www.plain.com/docs/graphql-reference/types/SidekickGithubRepo
A GitHub repository the workspace's Sidekick GitHub integration has access to.
# SidekickGithubRepoInput
Source: https://www.plain.com/docs/graphql-reference/types/SidekickGithubRepoInput
A repository to select for the workspace's Sidekick GitHub integration,
with optional per-repo operating instructions.
# SidekickGithubSelectedRepo
Source: https://www.plain.com/docs/graphql-reference/types/SidekickGithubSelectedRepo
A repository selected for the workspace's Sidekick GitHub integration.
# SidekickGithubServiceConfig
Source: https://www.plain.com/docs/graphql-reference/types/SidekickGithubServiceConfig
The Sidekick GitHub configuration for a workspace: selected repositories
and operating instructions.
# SidekickMcpApiKeyAuthInput
Source: https://www.plain.com/docs/graphql-reference/types/SidekickMcpApiKeyAuthInput
API-key auth config for a custom MCP server. Providing this on create registers the server as API-key rather than OAuth.
# SidekickMcpAuthParam
Source: https://www.plain.com/docs/graphql-reference/types/SidekickMcpAuthParam
One configured API-key auth attachment: the non-secret name and where it is attached. The secret value is never exposed.
# SidekickMcpAuthParamInput
Source: https://www.plain.com/docs/graphql-reference/types/SidekickMcpAuthParamInput
One API-key auth attachment to configure on create: the non-secret name plus where the secret is attached.
# SidekickMcpAuthParamPlacement
Source: https://www.plain.com/docs/graphql-reference/types/SidekickMcpAuthParamPlacement
Where an API-key secret is attached on each outbound request: an HTTP header or a URL query param.
# SidekickMcpIcon
Source: https://www.plain.com/docs/graphql-reference/types/SidekickMcpIcon
An icon a custom MCP server declares in its serverInfo, captured at discovery. src is an https or data: URI.
# SidekickMcpServer
Source: https://www.plain.com/docs/graphql-reference/types/SidekickMcpServer
A customer-owned MCP server registered for Sidekick. Sidekick reaches it only via the trusted proxy (the customer credential never enters the sandbox).
# SidekickMcpServerAuthType
Source: https://www.plain.com/docs/graphql-reference/types/SidekickMcpServerAuthType
How a custom MCP server authenticates: brokered OAuth, or a static API key attached in configurable headers and/or URL query params.
# SidekickMcpTool
Source: https://www.plain.com/docs/graphql-reference/types/SidekickMcpTool
One tool discovered on a custom MCP server, served from the cached discovery snapshot.
# SidekickPosthogServiceConfig
Source: https://www.plain.com/docs/graphql-reference/types/SidekickPosthogServiceConfig
The Sidekick PostHog configuration for a workspace: operating
instructions plus the default project the agent queries.
# SidekickServiceConfig
Source: https://www.plain.com/docs/graphql-reference/types/SidekickServiceConfig
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.
# SidekickSettings
Source: https://www.plain.com/docs/graphql-reference/types/SidekickSettings
Workspace-level settings for Sidekick. Wrapper type so additional settings
can be added without breaking existing clients.
# SidekickSkill
Source: https://www.plain.com/docs/graphql-reference/types/SidekickSkill
A skill available to Sidekick in this workspace, with its effective enabled
state. The concrete type (SystemSidekickSkill or CustomSidekickSkill)
indicates how the skill is provided. Use the sidekickCustomSkill query to
load a custom skill's instructions.
# SingleValueMetricName
Source: https://www.plain.com/docs/graphql-reference/types/SingleValueMetricName
# SlackAutoJoinRule
Source: https://www.plain.com/docs/graphql-reference/types/SlackAutoJoinRule
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.
# SlackAutoJoinRuleChannelMode
Source: https://www.plain.com/docs/graphql-reference/types/SlackAutoJoinRuleChannelMode
The default channel mode an auto-join rule applies to newly joined channels.
# SlackAutoJoinRuleIngestionMode
Source: https://www.plain.com/docs/graphql-reference/types/SlackAutoJoinRuleIngestionMode
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.
# SlackAutoJoinRuleIngestionModeInput
Source: https://www.plain.com/docs/graphql-reference/types/SlackAutoJoinRuleIngestionModeInput
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.
# SlackAutoJoinRuleInput
Source: https://www.plain.com/docs/graphql-reference/types/SlackAutoJoinRuleInput
# SlackAutoJoinRuleManualIngestion
Source: https://www.plain.com/docs/graphql-reference/types/SlackAutoJoinRuleManualIngestion
Manual (emoji-reaction) ingestion configuration. Each field is null when the
team default for that option applies.
# SlackAutoJoinRuleManualIngestionInput
Source: https://www.plain.com/docs/graphql-reference/types/SlackAutoJoinRuleManualIngestionInput
Manual (emoji-reaction) ingestion configuration provided to setSlackAutoJoinRules.
# SlackBroadcastSendDeliveryRecipient
Source: https://www.plain.com/docs/graphql-reference/types/SlackBroadcastSendDeliveryRecipient
# SlackBroadcastSender
Source: https://www.plain.com/docs/graphql-reference/types/SlackBroadcastSender
# SlackChannelMembership
Source: https://www.plain.com/docs/graphql-reference/types/SlackChannelMembership
# SlackCustomerIdentity
Source: https://www.plain.com/docs/graphql-reference/types/SlackCustomerIdentity
# SlackIngestionMode
Source: https://www.plain.com/docs/graphql-reference/types/SlackIngestionMode
How threads are created from messages in a connected slack channel.
# SlackMessageEntry
Source: https://www.plain.com/docs/graphql-reference/types/SlackMessageEntry
# SlackMessageEntryRelatedThread
Source: https://www.plain.com/docs/graphql-reference/types/SlackMessageEntryRelatedThread
# SlackReaction
Source: https://www.plain.com/docs/graphql-reference/types/SlackReaction
# SlackReplyEntry
Source: https://www.plain.com/docs/graphql-reference/types/SlackReplyEntry
# SlackThreadChannelAssociation
Source: https://www.plain.com/docs/graphql-reference/types/SlackThreadChannelAssociation
A thread channel association backed by a connected Slack channel.
# SlackThreadChannelDetails
Source: https://www.plain.com/docs/graphql-reference/types/SlackThreadChannelDetails
# SlackThreadChannelDetailsInput
Source: https://www.plain.com/docs/graphql-reference/types/SlackThreadChannelDetailsInput
# SlackUser
Source: https://www.plain.com/docs/graphql-reference/types/SlackUser
# SlackUserConnection
Source: https://www.plain.com/docs/graphql-reference/types/SlackUserConnection
# SlackUserEdge
Source: https://www.plain.com/docs/graphql-reference/types/SlackUserEdge
# SlackUserIdentity
Source: https://www.plain.com/docs/graphql-reference/types/SlackUserIdentity
# Snippet
Source: https://www.plain.com/docs/graphql-reference/types/Snippet
# SnippetConnection
Source: https://www.plain.com/docs/graphql-reference/types/SnippetConnection
# SnippetEdge
Source: https://www.plain.com/docs/graphql-reference/types/SnippetEdge
# SnoozeStatusDetail
Source: https://www.plain.com/docs/graphql-reference/types/SnoozeStatusDetail
# SnoozeThreadInput
Source: https://www.plain.com/docs/graphql-reference/types/SnoozeThreadInput
# SnoozeThreadOutput
Source: https://www.plain.com/docs/graphql-reference/types/SnoozeThreadOutput
# SortDirection
Source: https://www.plain.com/docs/graphql-reference/types/SortDirection
# StartServiceAuthorizationInput
Source: https://www.plain.com/docs/graphql-reference/types/StartServiceAuthorizationInput
# StartServiceAuthorizationOutput
Source: https://www.plain.com/docs/graphql-reference/types/StartServiceAuthorizationOutput
# StatusDetailType
Source: https://www.plain.com/docs/graphql-reference/types/StatusDetailType
# StringArrayInput
Source: https://www.plain.com/docs/graphql-reference/types/StringArrayInput
# StringArraySetting
Source: https://www.plain.com/docs/graphql-reference/types/StringArraySetting
A string array setting
# StringInput
Source: https://www.plain.com/docs/graphql-reference/types/StringInput
# StringSearchExpression
Source: https://www.plain.com/docs/graphql-reference/types/StringSearchExpression
The different ways in which a string is matched.
Exactly one of these must be provided in a single search expression.
# StringSetting
Source: https://www.plain.com/docs/graphql-reference/types/StringSetting
A string setting
# SubscriptionEventType
Source: https://www.plain.com/docs/graphql-reference/types/SubscriptionEventType
# SuggestedLabelType
Source: https://www.plain.com/docs/graphql-reference/types/SuggestedLabelType
An AI-proposed label type awaiting human review. Once accepted, a real LabelType is created and this record moves to ACCEPTED; dismissing it moves it to DISMISSED without creating a label type.
# SuggestedLabelTypeSource
Source: https://www.plain.com/docs/graphql-reference/types/SuggestedLabelTypeSource
How a suggested label type was produced.
# SuggestedLabelTypeStatus
Source: https://www.plain.com/docs/graphql-reference/types/SuggestedLabelTypeStatus
# SuggestedSlackTeammate
Source: https://www.plain.com/docs/graphql-reference/types/SuggestedSlackTeammate
A teammate from the workspace's connected Slack workspace who can be invited
to Plain. Distinct from SlackUser, which is used in customer-facing channel
contexts and does not expose email.
# SupportEmailAddressEmailActor
Source: https://www.plain.com/docs/graphql-reference/types/SupportEmailAddressEmailActor
# SurveyResponse
Source: https://www.plain.com/docs/graphql-reference/types/SurveyResponse
# SurveyResponseFilter
Source: https://www.plain.com/docs/graphql-reference/types/SurveyResponseFilter
# SurveyResponseFilterOutput
Source: https://www.plain.com/docs/graphql-reference/types/SurveyResponseFilterOutput
# SyncBusinessHoursSlotsInput
Source: https://www.plain.com/docs/graphql-reference/types/SyncBusinessHoursSlotsInput
# SyncBusinessHoursSlotsOutput
Source: https://www.plain.com/docs/graphql-reference/types/SyncBusinessHoursSlotsOutput
# SyncUserWorkingHoursInput
Source: https://www.plain.com/docs/graphql-reference/types/SyncUserWorkingHoursInput
# SyncUserWorkingHoursOutput
Source: https://www.plain.com/docs/graphql-reference/types/SyncUserWorkingHoursOutput
# System
Source: https://www.plain.com/docs/graphql-reference/types/System
# SystemActor
Source: https://www.plain.com/docs/graphql-reference/types/SystemActor
# SystemSidekickSkill
Source: https://www.plain.com/docs/graphql-reference/types/SystemSidekickSkill
A Plain-managed skill defined in code, available to every workspace.
# Task
Source: https://www.plain.com/docs/graphql-reference/types/Task
# TaskAssignee
Source: https://www.plain.com/docs/graphql-reference/types/TaskAssignee
# TaskConnection
Source: https://www.plain.com/docs/graphql-reference/types/TaskConnection
# TaskEdge
Source: https://www.plain.com/docs/graphql-reference/types/TaskEdge
# TaskLink
Source: https://www.plain.com/docs/graphql-reference/types/TaskLink
A link from a task back to the entity that caused it to be raised.
# TaskLinkConnection
Source: https://www.plain.com/docs/graphql-reference/types/TaskLinkConnection
# TaskLinkEdge
Source: https://www.plain.com/docs/graphql-reference/types/TaskLinkEdge
# TaskStatus
Source: https://www.plain.com/docs/graphql-reference/types/TaskStatus
The lifecycle status of a task.
# TasksFilter
Source: https://www.plain.com/docs/graphql-reference/types/TasksFilter
# TasksSort
Source: https://www.plain.com/docs/graphql-reference/types/TasksSort
# TasksSortField
Source: https://www.plain.com/docs/graphql-reference/types/TasksSortField
# TeamSettings
Source: https://www.plain.com/docs/graphql-reference/types/TeamSettings
# Tenant
Source: https://www.plain.com/docs/graphql-reference/types/Tenant
# TenantConnection
Source: https://www.plain.com/docs/graphql-reference/types/TenantConnection
# TenantEdge
Source: https://www.plain.com/docs/graphql-reference/types/TenantEdge
# TenantField
Source: https://www.plain.com/docs/graphql-reference/types/TenantField
# TenantFieldBooleanValue
Source: https://www.plain.com/docs/graphql-reference/types/TenantFieldBooleanValue
# TenantFieldDateTimeValue
Source: https://www.plain.com/docs/graphql-reference/types/TenantFieldDateTimeValue
# TenantFieldFilter
Source: https://www.plain.com/docs/graphql-reference/types/TenantFieldFilter
# TenantFieldIdentifier
Source: https://www.plain.com/docs/graphql-reference/types/TenantFieldIdentifier
# TenantFieldMappingConcept
Source: https://www.plain.com/docs/graphql-reference/types/TenantFieldMappingConcept
# TenantFieldNumberValue
Source: https://www.plain.com/docs/graphql-reference/types/TenantFieldNumberValue
# TenantFieldSchema
Source: https://www.plain.com/docs/graphql-reference/types/TenantFieldSchema
# TenantFieldSchemaConnection
Source: https://www.plain.com/docs/graphql-reference/types/TenantFieldSchemaConnection
# TenantFieldSchemaEdge
Source: https://www.plain.com/docs/graphql-reference/types/TenantFieldSchemaEdge
# TenantFieldSchemaInput
Source: https://www.plain.com/docs/graphql-reference/types/TenantFieldSchemaInput
# TenantFieldSchemasFilter
Source: https://www.plain.com/docs/graphql-reference/types/TenantFieldSchemasFilter
# TenantFieldStringArrayValue
Source: https://www.plain.com/docs/graphql-reference/types/TenantFieldStringArrayValue
# TenantFieldStringValue
Source: https://www.plain.com/docs/graphql-reference/types/TenantFieldStringValue
# TenantFieldType
Source: https://www.plain.com/docs/graphql-reference/types/TenantFieldType
The data type of a tenant field schema, determining which value field to use when upserting a tenant field.
# TenantFieldUserReferenceValue
Source: https://www.plain.com/docs/graphql-reference/types/TenantFieldUserReferenceValue
# TenantFieldValue
Source: https://www.plain.com/docs/graphql-reference/types/TenantFieldValue
# TenantIdentifierInput
Source: https://www.plain.com/docs/graphql-reference/types/TenantIdentifierInput
# TenantMembershipPolicy
Source: https://www.plain.com/docs/graphql-reference/types/TenantMembershipPolicy
Controls how tenants are assigned when a message from an associated channel is received.
# TenantSearchResult
Source: https://www.plain.com/docs/graphql-reference/types/TenantSearchResult
# TenantSearchResultConnection
Source: https://www.plain.com/docs/graphql-reference/types/TenantSearchResultConnection
# TenantSearchResultEdge
Source: https://www.plain.com/docs/graphql-reference/types/TenantSearchResultEdge
# TenantSource
Source: https://www.plain.com/docs/graphql-reference/types/TenantSource
# TenantTierMembership
Source: https://www.plain.com/docs/graphql-reference/types/TenantTierMembership
# TenantsFilter
Source: https://www.plain.com/docs/graphql-reference/types/TenantsFilter
# TenantsSearchQuery
Source: https://www.plain.com/docs/graphql-reference/types/TenantsSearchQuery
Query to search for tenants.
# Thread
Source: https://www.plain.com/docs/graphql-reference/types/Thread
A thread represents a conversation with a customer, around a specific topic.
# ThreadAdditionalAssigneesTransitionedEntry
Source: https://www.plain.com/docs/graphql-reference/types/ThreadAdditionalAssigneesTransitionedEntry
# ThreadAssignee
Source: https://www.plain.com/docs/graphql-reference/types/ThreadAssignee
The entity a thread is assigned to: a human user, a machine user (bot), or the Plain system itself.
# ThreadAssigneeInput
Source: https://www.plain.com/docs/graphql-reference/types/ThreadAssigneeInput
# ThreadAssignmentNotificationDetail
Source: https://www.plain.com/docs/graphql-reference/types/ThreadAssignmentNotificationDetail
# ThreadAssignmentTransitionedEntry
Source: https://www.plain.com/docs/graphql-reference/types/ThreadAssignmentTransitionedEntry
# ThreadCatchupCustomerEvent
Source: https://www.plain.com/docs/graphql-reference/types/ThreadCatchupCustomerEvent
# ThreadCatchupDetail
Source: https://www.plain.com/docs/graphql-reference/types/ThreadCatchupDetail
# ThreadCatchupFeedbackDetails
Source: https://www.plain.com/docs/graphql-reference/types/ThreadCatchupFeedbackDetails
# ThreadCatchupSuggestedAction
Source: https://www.plain.com/docs/graphql-reference/types/ThreadCatchupSuggestedAction
# ThreadCatchupSuggestedActionStatus
Source: https://www.plain.com/docs/graphql-reference/types/ThreadCatchupSuggestedActionStatus
# ThreadCatchupSuggestedCustomerAction
Source: https://www.plain.com/docs/graphql-reference/types/ThreadCatchupSuggestedCustomerAction
# ThreadCatchupSuggestedInternalAction
Source: https://www.plain.com/docs/graphql-reference/types/ThreadCatchupSuggestedInternalAction
# ThreadCatchupSuggestedSidekickPrompt
Source: https://www.plain.com/docs/graphql-reference/types/ThreadCatchupSuggestedSidekickPrompt
# ThreadCatchupUserEvent
Source: https://www.plain.com/docs/graphql-reference/types/ThreadCatchupUserEvent
# ThreadChannel
Source: https://www.plain.com/docs/graphql-reference/types/ThreadChannel
# ThreadChannelAssociation
Source: https://www.plain.com/docs/graphql-reference/types/ThreadChannelAssociation
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.
# ThreadChannelDetails
Source: https://www.plain.com/docs/graphql-reference/types/ThreadChannelDetails
# ThreadChannelDetailsInput
Source: https://www.plain.com/docs/graphql-reference/types/ThreadChannelDetailsInput
# ThreadCluster
Source: https://www.plain.com/docs/graphql-reference/types/ThreadCluster
# ThreadClusterConnection
Source: https://www.plain.com/docs/graphql-reference/types/ThreadClusterConnection
# ThreadClusterEdge
Source: https://www.plain.com/docs/graphql-reference/types/ThreadClusterEdge
# ThreadClustersFeedbackDetails
Source: https://www.plain.com/docs/graphql-reference/types/ThreadClustersFeedbackDetails
# ThreadClustersFilter
Source: https://www.plain.com/docs/graphql-reference/types/ThreadClustersFilter
# ThreadConnection
Source: https://www.plain.com/docs/graphql-reference/types/ThreadConnection
# ThreadDiscussion
Source: https://www.plain.com/docs/graphql-reference/types/ThreadDiscussion
# ThreadDiscussionAgentSessionChannelDetails
Source: https://www.plain.com/docs/graphql-reference/types/ThreadDiscussionAgentSessionChannelDetails
# ThreadDiscussionAgentStatus
Source: https://www.plain.com/docs/graphql-reference/types/ThreadDiscussionAgentStatus
The state of the agent working on a discussion, independent of whether the discussion itself is open or resolved.
# ThreadDiscussionApprovalRequestEntryPayload
Source: https://www.plain.com/docs/graphql-reference/types/ThreadDiscussionApprovalRequestEntryPayload
# ThreadDiscussionChannelDetails
Source: https://www.plain.com/docs/graphql-reference/types/ThreadDiscussionChannelDetails
# ThreadDiscussionConnectRequestEntryPayload
Source: https://www.plain.com/docs/graphql-reference/types/ThreadDiscussionConnectRequestEntryPayload
Sidekick asked the workspace to connect one or more of its tools. Rendered as a
card in the Sidekick transcript. Only produced for in-app sessions.
# ThreadDiscussionConnection
Source: https://www.plain.com/docs/graphql-reference/types/ThreadDiscussionConnection
# ThreadDiscussionCursorDetailsInput
Source: https://www.plain.com/docs/graphql-reference/types/ThreadDiscussionCursorDetailsInput
# ThreadDiscussionCursorWorkspaceBackgroundAgentChannelDetails
Source: https://www.plain.com/docs/graphql-reference/types/ThreadDiscussionCursorWorkspaceBackgroundAgentChannelDetails
# ThreadDiscussionEdge
Source: https://www.plain.com/docs/graphql-reference/types/ThreadDiscussionEdge
# ThreadDiscussionEmailChannelDetails
Source: https://www.plain.com/docs/graphql-reference/types/ThreadDiscussionEmailChannelDetails
# ThreadDiscussionEmailDetailsInput
Source: https://www.plain.com/docs/graphql-reference/types/ThreadDiscussionEmailDetailsInput
# ThreadDiscussionEntry
Source: https://www.plain.com/docs/graphql-reference/types/ThreadDiscussionEntry
# ThreadDiscussionEntryPayload
Source: https://www.plain.com/docs/graphql-reference/types/ThreadDiscussionEntryPayload
# ThreadDiscussionMessage
Source: https://www.plain.com/docs/graphql-reference/types/ThreadDiscussionMessage
# ThreadDiscussionMessageConnection
Source: https://www.plain.com/docs/graphql-reference/types/ThreadDiscussionMessageConnection
# ThreadDiscussionMessageEdge
Source: https://www.plain.com/docs/graphql-reference/types/ThreadDiscussionMessageEdge
# ThreadDiscussionMessageEntry
Source: https://www.plain.com/docs/graphql-reference/types/ThreadDiscussionMessageEntry
# ThreadDiscussionMessageEntryPayload
Source: https://www.plain.com/docs/graphql-reference/types/ThreadDiscussionMessageEntryPayload
# ThreadDiscussionMessageReaction
Source: https://www.plain.com/docs/graphql-reference/types/ThreadDiscussionMessageReaction
# ThreadDiscussionMessageType
Source: https://www.plain.com/docs/graphql-reference/types/ThreadDiscussionMessageType
# ThreadDiscussionResolvedEntry
Source: https://www.plain.com/docs/graphql-reference/types/ThreadDiscussionResolvedEntry
# ThreadDiscussionSlackChannelDetails
Source: https://www.plain.com/docs/graphql-reference/types/ThreadDiscussionSlackChannelDetails
# ThreadDiscussionSlackDetailsInput
Source: https://www.plain.com/docs/graphql-reference/types/ThreadDiscussionSlackDetailsInput
# ThreadDiscussionStatus
Source: https://www.plain.com/docs/graphql-reference/types/ThreadDiscussionStatus
# ThreadDiscussionToolCallApprovalEntryPayload
Source: https://www.plain.com/docs/graphql-reference/types/ThreadDiscussionToolCallApprovalEntryPayload
A custom agent's approval on one tool call it has already reported. Sidekick's approvals are ThreadDiscussionApprovalRequestEntryPayload.
# ThreadDiscussionToolCallEntryPayload
Source: https://www.plain.com/docs/graphql-reference/types/ThreadDiscussionToolCallEntryPayload
# ThreadDiscussionTurnStoppedEntryPayload
Source: https://www.plain.com/docs/graphql-reference/types/ThreadDiscussionTurnStoppedEntryPayload
A person stopped Sidekick mid-turn. Rendered as a plain line in the transcript so a
reader can see why the agent stopped talking. Who stopped it and when are the
message's own createdBy and createdAt.
# ThreadDiscussionType
Source: https://www.plain.com/docs/graphql-reference/types/ThreadDiscussionType
# ThreadDiscussionVisibility
Source: https://www.plain.com/docs/graphql-reference/types/ThreadDiscussionVisibility
# ThreadEdge
Source: https://www.plain.com/docs/graphql-reference/types/ThreadEdge
# ThreadEscalationDetails
Source: https://www.plain.com/docs/graphql-reference/types/ThreadEscalationDetails
# ThreadEvent
Source: https://www.plain.com/docs/graphql-reference/types/ThreadEvent
# ThreadEventEntry
Source: https://www.plain.com/docs/graphql-reference/types/ThreadEventEntry
# ThreadField
Source: https://www.plain.com/docs/graphql-reference/types/ThreadField
A stored value for a custom thread field on a specific thread.
# ThreadFieldDateFilter
Source: https://www.plain.com/docs/graphql-reference/types/ThreadFieldDateFilter
# ThreadFieldFilter
Source: https://www.plain.com/docs/graphql-reference/types/ThreadFieldFilter
# ThreadFieldNumberFilter
Source: https://www.plain.com/docs/graphql-reference/types/ThreadFieldNumberFilter
# ThreadFieldSchema
Source: https://www.plain.com/docs/graphql-reference/types/ThreadFieldSchema
Defines the shape and behaviour of a custom field that can be attached to threads.
# ThreadFieldSchemaConnection
Source: https://www.plain.com/docs/graphql-reference/types/ThreadFieldSchemaConnection
# ThreadFieldSchemaEdge
Source: https://www.plain.com/docs/graphql-reference/types/ThreadFieldSchemaEdge
# ThreadFieldSchemaOrderInput
Source: https://www.plain.com/docs/graphql-reference/types/ThreadFieldSchemaOrderInput
# ThreadFieldSchemaType
Source: https://www.plain.com/docs/graphql-reference/types/ThreadFieldSchemaType
The data type of a thread field schema, which determines which value field is used when reading or writing thread field values.
# ThreadHeatmapHour
Source: https://www.plain.com/docs/graphql-reference/types/ThreadHeatmapHour
One cell of a thread heatmap grid, representing activity for a specific day-of-week and hour-of-day combination.
# ThreadHeatmapMetric
Source: https://www.plain.com/docs/graphql-reference/types/ThreadHeatmapMetric
Result of a `threadHeatmapMetric` query. Activity distributed across a 7 × 24 grid (Monday–Sunday, hour 0–23 UTC).
# ThreadHeatmapMetricInput
Source: https://www.plain.com/docs/graphql-reference/types/ThreadHeatmapMetricInput
# ThreadHeatmapThreadIdsArgs
Source: https://www.plain.com/docs/graphql-reference/types/ThreadHeatmapThreadIdsArgs
Identifies the heatmap slice to retrieve thread ids from. Supply both fields for a single cell (one day-of-week/hour-of-day pair across the range), only `hourOfDay` for a column (that hour across all days), or only `dayOfWeek` for a row (that day across all hours). At least one is required in threadIds mode.
# ThreadLabelsChangedEntry
Source: https://www.plain.com/docs/graphql-reference/types/ThreadLabelsChangedEntry
# ThreadLink
Source: https://www.plain.com/docs/graphql-reference/types/ThreadLink
# ThreadLinkCandidate
Source: https://www.plain.com/docs/graphql-reference/types/ThreadLinkCandidate
# ThreadLinkCandidateConnection
Source: https://www.plain.com/docs/graphql-reference/types/ThreadLinkCandidateConnection
# ThreadLinkCandidateEdge
Source: https://www.plain.com/docs/graphql-reference/types/ThreadLinkCandidateEdge
# ThreadLinkCandidateFilter
Source: https://www.plain.com/docs/graphql-reference/types/ThreadLinkCandidateFilter
# ThreadLinkConnection
Source: https://www.plain.com/docs/graphql-reference/types/ThreadLinkConnection
# ThreadLinkCreatedEntry
Source: https://www.plain.com/docs/graphql-reference/types/ThreadLinkCreatedEntry
# ThreadLinkDeletedEntry
Source: https://www.plain.com/docs/graphql-reference/types/ThreadLinkDeletedEntry
# ThreadLinkEdge
Source: https://www.plain.com/docs/graphql-reference/types/ThreadLinkEdge
# ThreadLinkGroup
Source: https://www.plain.com/docs/graphql-reference/types/ThreadLinkGroup
# ThreadLinkGroupAggregateMetrics
Source: https://www.plain.com/docs/graphql-reference/types/ThreadLinkGroupAggregateMetrics
# ThreadLinkGroupCompanyMetrics
Source: https://www.plain.com/docs/graphql-reference/types/ThreadLinkGroupCompanyMetrics
# ThreadLinkGroupConnection
Source: https://www.plain.com/docs/graphql-reference/types/ThreadLinkGroupConnection
# ThreadLinkGroupEdge
Source: https://www.plain.com/docs/graphql-reference/types/ThreadLinkGroupEdge
# ThreadLinkGroupFilter
Source: https://www.plain.com/docs/graphql-reference/types/ThreadLinkGroupFilter
# ThreadLinkGroupSingleCompanyMetrics
Source: https://www.plain.com/docs/graphql-reference/types/ThreadLinkGroupSingleCompanyMetrics
# ThreadLinkGroupSingleTenantMetrics
Source: https://www.plain.com/docs/graphql-reference/types/ThreadLinkGroupSingleTenantMetrics
# ThreadLinkGroupSingleTierMetrics
Source: https://www.plain.com/docs/graphql-reference/types/ThreadLinkGroupSingleTierMetrics
# ThreadLinkGroupTenantMetrics
Source: https://www.plain.com/docs/graphql-reference/types/ThreadLinkGroupTenantMetrics
# ThreadLinkGroupTierMetrics
Source: https://www.plain.com/docs/graphql-reference/types/ThreadLinkGroupTierMetrics
# ThreadLinkLinkType
Source: https://www.plain.com/docs/graphql-reference/types/ThreadLinkLinkType
# ThreadLinkSourceFilter
Source: https://www.plain.com/docs/graphql-reference/types/ThreadLinkSourceFilter
# ThreadLinkSourceStatus
Source: https://www.plain.com/docs/graphql-reference/types/ThreadLinkSourceStatus
# ThreadLinkStatus
Source: https://www.plain.com/docs/graphql-reference/types/ThreadLinkStatus
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.
# ThreadLinkTargetCreatedEntry
Source: https://www.plain.com/docs/graphql-reference/types/ThreadLinkTargetCreatedEntry
# ThreadLinkTargetDeletedEntry
Source: https://www.plain.com/docs/graphql-reference/types/ThreadLinkTargetDeletedEntry
# ThreadLinkUpdatedEntry
Source: https://www.plain.com/docs/graphql-reference/types/ThreadLinkUpdatedEntry
# ThreadMessageInfo
Source: https://www.plain.com/docs/graphql-reference/types/ThreadMessageInfo
# ThreadMetricFilters
Source: https://www.plain.com/docs/graphql-reference/types/ThreadMetricFilters
# ThreadMetricGroup
Source: https://www.plain.com/docs/graphql-reference/types/ThreadMetricGroup
Identifies which group bucket a series or value belongs to in a grouped thread metric result.
# ThreadMetricGroupBy
Source: https://www.plain.com/docs/graphql-reference/types/ThreadMetricGroupBy
Dimension by which thread metric results can be grouped.
# ThreadMetricGroupByInput
Source: https://www.plain.com/docs/graphql-reference/types/ThreadMetricGroupByInput
Sub-key required when dimension is keyed (THREAD_FIELD key, TENANT_FIELD externalFieldId).
# ThreadMetricInterval
Source: https://www.plain.com/docs/graphql-reference/types/ThreadMetricInterval
# ThreadMetricIntervalUnit
Source: https://www.plain.com/docs/graphql-reference/types/ThreadMetricIntervalUnit
# ThreadMetricMode
Source: https://www.plain.com/docs/graphql-reference/types/ThreadMetricMode
What a thread metric query returns: the chart data or the thread ids behind a slice.
# ThreadPriorityChangedEntry
Source: https://www.plain.com/docs/graphql-reference/types/ThreadPriorityChangedEntry
# ThreadScopePrimitiveType
Source: https://www.plain.com/docs/graphql-reference/types/ThreadScopePrimitiveType
The dimension of a thread used to filter visibility for a custom role scope.
# ThreadSearchResult
Source: https://www.plain.com/docs/graphql-reference/types/ThreadSearchResult
# ThreadSearchResultConnection
Source: https://www.plain.com/docs/graphql-reference/types/ThreadSearchResultConnection
# ThreadSearchResultEdge
Source: https://www.plain.com/docs/graphql-reference/types/ThreadSearchResultEdge
# ThreadServiceLevelAgreementPolicyChangedEntry
Source: https://www.plain.com/docs/graphql-reference/types/ThreadServiceLevelAgreementPolicyChangedEntry
A timeline entry recording a change to the SLA policy applied to a thread.
# ThreadSingleValueMetric
Source: https://www.plain.com/docs/graphql-reference/types/ThreadSingleValueMetric
Result of a `threadSingleValueMetric` query. Contains one aggregate value per group bucket.
# ThreadSingleValueMetricInput
Source: https://www.plain.com/docs/graphql-reference/types/ThreadSingleValueMetricInput
# ThreadSingleValueMetricValue
Source: https://www.plain.com/docs/graphql-reference/types/ThreadSingleValueMetricValue
# ThreadStatus
Source: https://www.plain.com/docs/graphql-reference/types/ThreadStatus
The lifecycle status of a thread.
# ThreadStatusDetail
Source: https://www.plain.com/docs/graphql-reference/types/ThreadStatusDetail
# ThreadStatusDetailCreated
Source: https://www.plain.com/docs/graphql-reference/types/ThreadStatusDetailCreated
# ThreadStatusDetailDoneAutomaticallySet
Source: https://www.plain.com/docs/graphql-reference/types/ThreadStatusDetailDoneAutomaticallySet
# ThreadStatusDetailDoneManuallySet
Source: https://www.plain.com/docs/graphql-reference/types/ThreadStatusDetailDoneManuallySet
# ThreadStatusDetailIgnored
Source: https://www.plain.com/docs/graphql-reference/types/ThreadStatusDetailIgnored
# ThreadStatusDetailInProgress
Source: https://www.plain.com/docs/graphql-reference/types/ThreadStatusDetailInProgress
# ThreadStatusDetailLinearUpdated
Source: https://www.plain.com/docs/graphql-reference/types/ThreadStatusDetailLinearUpdated
# ThreadStatusDetailNewReply
Source: https://www.plain.com/docs/graphql-reference/types/ThreadStatusDetailNewReply
# ThreadStatusDetailReplied
Source: https://www.plain.com/docs/graphql-reference/types/ThreadStatusDetailReplied
# ThreadStatusDetailSnoozed
Source: https://www.plain.com/docs/graphql-reference/types/ThreadStatusDetailSnoozed
# ThreadStatusDetailThreadDiscussionResolved
Source: https://www.plain.com/docs/graphql-reference/types/ThreadStatusDetailThreadDiscussionResolved
# ThreadStatusDetailThreadLinkUpdated
Source: https://www.plain.com/docs/graphql-reference/types/ThreadStatusDetailThreadLinkUpdated
# ThreadStatusDetailUnsnoozed
Source: https://www.plain.com/docs/graphql-reference/types/ThreadStatusDetailUnsnoozed
# ThreadStatusDetailWaitingForCustomer
Source: https://www.plain.com/docs/graphql-reference/types/ThreadStatusDetailWaitingForCustomer
# ThreadStatusDetailWaitingForDuration
Source: https://www.plain.com/docs/graphql-reference/types/ThreadStatusDetailWaitingForDuration
# ThreadStatusDetailWaitingIndefinitely
Source: https://www.plain.com/docs/graphql-reference/types/ThreadStatusDetailWaitingIndefinitely
# ThreadStatusTransitionedEntry
Source: https://www.plain.com/docs/graphql-reference/types/ThreadStatusTransitionedEntry
# ThreadTimeSeriesMetric
Source: https://www.plain.com/docs/graphql-reference/types/ThreadTimeSeriesMetric
Result of a `threadTimeSeriesMetric` query. Each element in `timestamps` aligns with the same index across all series.
# ThreadTimeSeriesMetricInput
Source: https://www.plain.com/docs/graphql-reference/types/ThreadTimeSeriesMetricInput
# ThreadTimeSeriesSeries
Source: https://www.plain.com/docs/graphql-reference/types/ThreadTimeSeriesSeries
# ThreadTimelineEntriesFilter
Source: https://www.plain.com/docs/graphql-reference/types/ThreadTimelineEntriesFilter
# ThreadWithDistance
Source: https://www.plain.com/docs/graphql-reference/types/ThreadWithDistance
A thread paired with a semantic similarity score, returned by `relatedThreads`.
# ThreadsByExternalIdConnection
Source: https://www.plain.com/docs/graphql-reference/types/ThreadsByExternalIdConnection
# ThreadsDisplayOptions
Source: https://www.plain.com/docs/graphql-reference/types/ThreadsDisplayOptions
# ThreadsDisplayOptionsInput
Source: https://www.plain.com/docs/graphql-reference/types/ThreadsDisplayOptionsInput
# ThreadsFilter
Source: https://www.plain.com/docs/graphql-reference/types/ThreadsFilter
# ThreadsGroupBy
Source: https://www.plain.com/docs/graphql-reference/types/ThreadsGroupBy
# ThreadsLayout
Source: https://www.plain.com/docs/graphql-reference/types/ThreadsLayout
# ThreadsSearchQuery
Source: https://www.plain.com/docs/graphql-reference/types/ThreadsSearchQuery
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
# ThreadsSort
Source: https://www.plain.com/docs/graphql-reference/types/ThreadsSort
# ThreadsSortField
Source: https://www.plain.com/docs/graphql-reference/types/ThreadsSortField
# Tier
Source: https://www.plain.com/docs/graphql-reference/types/Tier
# TierConnection
Source: https://www.plain.com/docs/graphql-reference/types/TierConnection
# TierEdge
Source: https://www.plain.com/docs/graphql-reference/types/TierEdge
# TierIdentifierInput
Source: https://www.plain.com/docs/graphql-reference/types/TierIdentifierInput
# TierMemberIdentifierInput
Source: https://www.plain.com/docs/graphql-reference/types/TierMemberIdentifierInput
# TierMembership
Source: https://www.plain.com/docs/graphql-reference/types/TierMembership
# TierMembershipConnection
Source: https://www.plain.com/docs/graphql-reference/types/TierMembershipConnection
# TierMembershipEdge
Source: https://www.plain.com/docs/graphql-reference/types/TierMembershipEdge
# TieredRecurringPrice
Source: https://www.plain.com/docs/graphql-reference/types/TieredRecurringPrice
# TimeSeriesMetricName
Source: https://www.plain.com/docs/graphql-reference/types/TimeSeriesMetricName
# TimelineEntry
Source: https://www.plain.com/docs/graphql-reference/types/TimelineEntry
# TimelineEntryConnection
Source: https://www.plain.com/docs/graphql-reference/types/TimelineEntryConnection
# TimelineEntryEdge
Source: https://www.plain.com/docs/graphql-reference/types/TimelineEntryEdge
# TimelineEntryType
Source: https://www.plain.com/docs/graphql-reference/types/TimelineEntryType
Discriminates the type of a timeline entry, used when filtering the thread timeline.
# TimelineEventEntry
Source: https://www.plain.com/docs/graphql-reference/types/TimelineEventEntry
# Timezone
Source: https://www.plain.com/docs/graphql-reference/types/Timezone
# TodoStatusDetail
Source: https://www.plain.com/docs/graphql-reference/types/TodoStatusDetail
# ToggleFeatureEntitlement
Source: https://www.plain.com/docs/graphql-reference/types/ToggleFeatureEntitlement
# ToggleSlackMessageReactionInput
Source: https://www.plain.com/docs/graphql-reference/types/ToggleSlackMessageReactionInput
# ToggleSlackMessageReactionOutput
Source: https://www.plain.com/docs/graphql-reference/types/ToggleSlackMessageReactionOutput
# ToggleWorkflowRulePublishedInput
Source: https://www.plain.com/docs/graphql-reference/types/ToggleWorkflowRulePublishedInput
# ToggleWorkflowRulePublishedOutput
Source: https://www.plain.com/docs/graphql-reference/types/ToggleWorkflowRulePublishedOutput
# ToneRuleFeedbackDetails
Source: https://www.plain.com/docs/graphql-reference/types/ToneRuleFeedbackDetails
# TotalResolutionTimeServiceLevelAgreement
Source: https://www.plain.com/docs/graphql-reference/types/TotalResolutionTimeServiceLevelAgreement
An SLA that tracks the time from thread creation until the thread is marked Done, including after it is reopened. Reopening a very old thread can immediately breach this SLA.
# TriggerWorkflowInput
Source: https://www.plain.com/docs/graphql-reference/types/TriggerWorkflowInput
# TriggerWorkflowOutput
Source: https://www.plain.com/docs/graphql-reference/types/TriggerWorkflowOutput
# TriggerWorkflowRuleInput
Source: https://www.plain.com/docs/graphql-reference/types/TriggerWorkflowRuleInput
# TriggerWorkflowRuleOutput
Source: https://www.plain.com/docs/graphql-reference/types/TriggerWorkflowRuleOutput
# UnarchiveLabelTypeInput
Source: https://www.plain.com/docs/graphql-reference/types/UnarchiveLabelTypeInput
# UnarchiveLabelTypeOutput
Source: https://www.plain.com/docs/graphql-reference/types/UnarchiveLabelTypeOutput
# UnassignThreadInput
Source: https://www.plain.com/docs/graphql-reference/types/UnassignThreadInput
# UnassignThreadOutput
Source: https://www.plain.com/docs/graphql-reference/types/UnassignThreadOutput
# UnmarkCustomerAsSpamInput
Source: https://www.plain.com/docs/graphql-reference/types/UnmarkCustomerAsSpamInput
# UnmarkCustomerAsSpamOutput
Source: https://www.plain.com/docs/graphql-reference/types/UnmarkCustomerAsSpamOutput
# UpdateActiveBillingRotaInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateActiveBillingRotaInput
# UpdateActiveBillingRotaOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateActiveBillingRotaOutput
# UpdateAgentSandboxToolPolicyInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateAgentSandboxToolPolicyInput
# UpdateAgentSandboxToolPolicyOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateAgentSandboxToolPolicyOutput
# UpdateAiToneRulesInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateAiToneRulesInput
# UpdateAiToneRulesOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateAiToneRulesOutput
# UpdateApiKeyInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateApiKeyInput
# UpdateApiKeyOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateApiKeyOutput
# UpdateAutoresponderInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateAutoresponderInput
# UpdateAutoresponderOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateAutoresponderOutput
# UpdateBroadcastAudienceInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateBroadcastAudienceInput
# UpdateBroadcastAudienceOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateBroadcastAudienceOutput
# UpdateBroadcastInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateBroadcastInput
# UpdateBroadcastOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateBroadcastOutput
# UpdateBusinessHoursScheduleInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateBusinessHoursScheduleInput
# UpdateBusinessHoursScheduleOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateBusinessHoursScheduleOutput
# UpdateChatAppInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateChatAppInput
# UpdateChatAppOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateChatAppOutput
# UpdateCompanyTierInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateCompanyTierInput
# UpdateCompanyTierOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateCompanyTierOutput
# UpdateConnectedDiscordChannelInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateConnectedDiscordChannelInput
# UpdateConnectedDiscordChannelOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateConnectedDiscordChannelOutput
# UpdateConnectedSlackChannelInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateConnectedSlackChannelInput
# UpdateConnectedSlackChannelOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateConnectedSlackChannelOutput
# UpdateCustomRoleInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateCustomRoleInput
# UpdateCustomRoleOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateCustomRoleOutput
# UpdateCustomerCardConfigInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateCustomerCardConfigInput
For constraints and details on the fields see the `CustomerCardConfig` type.
# UpdateCustomerCardConfigOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateCustomerCardConfigOutput
# UpdateCustomerCompanyInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateCustomerCompanyInput
# UpdateCustomerCompanyOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateCustomerCompanyOutput
# UpdateCustomerGroupInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateCustomerGroupInput
# UpdateCustomerGroupOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateCustomerGroupOutput
# UpdateCustomerSurveyInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateCustomerSurveyInput
# UpdateCustomerSurveyOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateCustomerSurveyOutput
# UpdateEscalationPathInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateEscalationPathInput
# UpdateEscalationPathOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateEscalationPathOutput
# UpdateGeneratedReplyInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateGeneratedReplyInput
# UpdateGeneratedReplyOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateGeneratedReplyOutput
# UpdateHelpCenterArticleCopyOptionSettingsInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateHelpCenterArticleCopyOptionSettingsInput
# UpdateHelpCenterArticleCopyOptionsInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateHelpCenterArticleCopyOptionsInput
# UpdateHelpCenterArticleGroupInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateHelpCenterArticleGroupInput
# UpdateHelpCenterArticleGroupOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateHelpCenterArticleGroupOutput
# UpdateHelpCenterCustomDomainNameInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateHelpCenterCustomDomainNameInput
# UpdateHelpCenterCustomDomainNameOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateHelpCenterCustomDomainNameOutput
# UpdateHelpCenterIndexInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateHelpCenterIndexInput
# UpdateHelpCenterIndexOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateHelpCenterIndexOutput
# UpdateHelpCenterInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateHelpCenterInput
# UpdateHelpCenterOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateHelpCenterOutput
# UpdateImportJobDefinitionInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateImportJobDefinitionInput
# UpdateImportJobDefinitionOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateImportJobDefinitionOutput
# UpdateInternalNotificationsInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateInternalNotificationsInput
# UpdateInternalNotificationsOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateInternalNotificationsOutput
# UpdateLabelTypeInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateLabelTypeInput
# UpdateLabelTypeOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateLabelTypeOutput
# UpdateMachineUserInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateMachineUserInput
# UpdateMachineUserOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateMachineUserOutput
# UpdateMyUserInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateMyUserInput
# UpdateMyUserOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateMyUserOutput
# UpdateNoteInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateNoteInput
# UpdateNoteOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateNoteOutput
# UpdateSavedThreadsViewInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateSavedThreadsViewInput
# UpdateSavedThreadsViewOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateSavedThreadsViewOutput
# UpdateServiceLevelAgreementInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateServiceLevelAgreementInput
# UpdateServiceLevelAgreementOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateServiceLevelAgreementOutput
# UpdateServiceLevelAgreementPolicyInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateServiceLevelAgreementPolicyInput
# UpdateServiceLevelAgreementPolicyOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateServiceLevelAgreementPolicyOutput
# UpdateSettingInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateSettingInput
An input provided to the `updateSetting` mutation.
# UpdateSettingOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateSettingOutput
An output type provided by the `updateSetting` mutation.
Returns the updated setting or an error.
# UpdateSidekickCustomSkillInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateSidekickCustomSkillInput
# UpdateSidekickCustomSkillOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateSidekickCustomSkillOutput
# UpdateSidekickGithubConfigInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateSidekickGithubConfigInput
# UpdateSidekickGithubConfigOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateSidekickGithubConfigOutput
# UpdateSidekickMcpServerInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateSidekickMcpServerInput
# UpdateSidekickMcpServerOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateSidekickMcpServerOutput
# UpdateSidekickPosthogConfigInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateSidekickPosthogConfigInput
# UpdateSidekickPosthogConfigOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateSidekickPosthogConfigOutput
# UpdateSidekickServiceConfigInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateSidekickServiceConfigInput
# UpdateSidekickServiceConfigOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateSidekickServiceConfigOutput
# UpdateSidekickSettingsInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateSidekickSettingsInput
# UpdateSidekickSettingsOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateSidekickSettingsOutput
# UpdateSidekickSlackConfigInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateSidekickSlackConfigInput
# UpdateSidekickSlackConfigOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateSidekickSlackConfigOutput
# UpdateSnippetInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateSnippetInput
# UpdateSnippetOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateSnippetOutput
# UpdateTaskAssignedToInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateTaskAssignedToInput
Only one of the fields can be set.
# UpdateTaskInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateTaskInput
# UpdateTaskOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateTaskOutput
# UpdateTenantTierInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateTenantTierInput
# UpdateTenantTierOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateTenantTierOutput
# UpdateThreadAgentStatusInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateThreadAgentStatusInput
# UpdateThreadAgentStatusOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateThreadAgentStatusOutput
# UpdateThreadChannelAssociationTenantMembershipPolicyInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateThreadChannelAssociationTenantMembershipPolicyInput
# UpdateThreadChannelAssociationTenantMembershipPolicyOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateThreadChannelAssociationTenantMembershipPolicyOutput
# UpdateThreadEscalationPathInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateThreadEscalationPathInput
# UpdateThreadEscalationPathOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateThreadEscalationPathOutput
# UpdateThreadExternalIdInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateThreadExternalIdInput
# UpdateThreadExternalIdOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateThreadExternalIdOutput
# UpdateThreadFieldSchemaInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateThreadFieldSchemaInput
# UpdateThreadFieldSchemaOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateThreadFieldSchemaOutput
# UpdateThreadServiceLevelAgreementPolicyInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateThreadServiceLevelAgreementPolicyInput
# UpdateThreadServiceLevelAgreementPolicyOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateThreadServiceLevelAgreementPolicyOutput
# UpdateThreadSuggestedActionStatusInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateThreadSuggestedActionStatusInput
# UpdateThreadSuggestedActionStatusOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateThreadSuggestedActionStatusOutput
# UpdateThreadTenantInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateThreadTenantInput
# UpdateThreadTenantOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateThreadTenantOutput
# UpdateThreadTierInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateThreadTierInput
# UpdateThreadTierOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateThreadTierOutput
# UpdateThreadTitleInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateThreadTitleInput
# UpdateThreadTitleOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateThreadTitleOutput
# UpdateTierInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateTierInput
# UpdateTierOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateTierOutput
# UpdateUserDefaultSavedThreadsViewInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateUserDefaultSavedThreadsViewInput
# UpdateUserDefaultSavedThreadsViewOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateUserDefaultSavedThreadsViewOutput
# UpdateWebhookTargetInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateWebhookTargetInput
# UpdateWebhookTargetOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateWebhookTargetOutput
# UpdateWorkflowInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateWorkflowInput
# UpdateWorkflowOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateWorkflowOutput
# UpdateWorkflowRuleInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateWorkflowRuleInput
# UpdateWorkflowRuleOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateWorkflowRuleOutput
# UpdateWorkflowStepInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateWorkflowStepInput
# UpdateWorkflowStepOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateWorkflowStepOutput
# UpdateWorkspaceEmailSettingsInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateWorkspaceEmailSettingsInput
# UpdateWorkspaceEmailSettingsOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateWorkspaceEmailSettingsOutput
# UpdateWorkspaceInput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateWorkspaceInput
# UpdateWorkspaceOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpdateWorkspaceOutput
# UploadFormData
Source: https://www.plain.com/docs/graphql-reference/types/UploadFormData
# UpsertCompanyInput
Source: https://www.plain.com/docs/graphql-reference/types/UpsertCompanyInput
# UpsertCompanyOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpsertCompanyOutput
# UpsertCustomerGroupInput
Source: https://www.plain.com/docs/graphql-reference/types/UpsertCustomerGroupInput
# UpsertCustomerGroupOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpsertCustomerGroupOutput
# UpsertCustomerIdentifierInput
Source: https://www.plain.com/docs/graphql-reference/types/UpsertCustomerIdentifierInput
# UpsertCustomerInput
Source: https://www.plain.com/docs/graphql-reference/types/UpsertCustomerInput
# UpsertCustomerOnCreateInput
Source: https://www.plain.com/docs/graphql-reference/types/UpsertCustomerOnCreateInput
# UpsertCustomerOnUpdateInput
Source: https://www.plain.com/docs/graphql-reference/types/UpsertCustomerOnUpdateInput
# UpsertCustomerOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpsertCustomerOutput
# UpsertHelpCenterArticleInput
Source: https://www.plain.com/docs/graphql-reference/types/UpsertHelpCenterArticleInput
# UpsertHelpCenterArticleOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpsertHelpCenterArticleOutput
# UpsertMyEmailSignatureInput
Source: https://www.plain.com/docs/graphql-reference/types/UpsertMyEmailSignatureInput
# UpsertMyEmailSignatureOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpsertMyEmailSignatureOutput
# UpsertResult
Source: https://www.plain.com/docs/graphql-reference/types/UpsertResult
# UpsertRoleScopesInput
Source: https://www.plain.com/docs/graphql-reference/types/UpsertRoleScopesInput
# UpsertRoleScopesOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpsertRoleScopesOutput
# UpsertTeamSettingsInput
Source: https://www.plain.com/docs/graphql-reference/types/UpsertTeamSettingsInput
# UpsertTeamSettingsOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpsertTeamSettingsOutput
# UpsertTenantFieldInput
Source: https://www.plain.com/docs/graphql-reference/types/UpsertTenantFieldInput
# UpsertTenantFieldOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpsertTenantFieldOutput
# UpsertTenantFieldSchemaInput
Source: https://www.plain.com/docs/graphql-reference/types/UpsertTenantFieldSchemaInput
# UpsertTenantFieldSchemaOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpsertTenantFieldSchemaOutput
# UpsertTenantInput
Source: https://www.plain.com/docs/graphql-reference/types/UpsertTenantInput
# UpsertTenantOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpsertTenantOutput
# UpsertThreadFieldIdentifier
Source: https://www.plain.com/docs/graphql-reference/types/UpsertThreadFieldIdentifier
# UpsertThreadFieldInput
Source: https://www.plain.com/docs/graphql-reference/types/UpsertThreadFieldInput
# UpsertThreadFieldOutput
Source: https://www.plain.com/docs/graphql-reference/types/UpsertThreadFieldOutput
# User
Source: https://www.plain.com/docs/graphql-reference/types/User
# UserAccount
Source: https://www.plain.com/docs/graphql-reference/types/UserAccount
A Plain user's core account record, representing their identity independent of any workspace membership.
# UserActor
Source: https://www.plain.com/docs/graphql-reference/types/UserActor
# UserAuthDiscordChannelInstallationInfo
Source: https://www.plain.com/docs/graphql-reference/types/UserAuthDiscordChannelInstallationInfo
# UserAuthDiscordChannelIntegration
Source: https://www.plain.com/docs/graphql-reference/types/UserAuthDiscordChannelIntegration
# UserAuthDiscordChannelIntegrationConnection
Source: https://www.plain.com/docs/graphql-reference/types/UserAuthDiscordChannelIntegrationConnection
# UserAuthDiscordChannelIntegrationEdge
Source: https://www.plain.com/docs/graphql-reference/types/UserAuthDiscordChannelIntegrationEdge
# UserAuthSlackInstallationInfo
Source: https://www.plain.com/docs/graphql-reference/types/UserAuthSlackInstallationInfo
# UserAuthSlackIntegration
Source: https://www.plain.com/docs/graphql-reference/types/UserAuthSlackIntegration
# UserConnection
Source: https://www.plain.com/docs/graphql-reference/types/UserConnection
# UserEdge
Source: https://www.plain.com/docs/graphql-reference/types/UserEdge
# UserEmailActor
Source: https://www.plain.com/docs/graphql-reference/types/UserEmailActor
# UserIdentifierInput
Source: https://www.plain.com/docs/graphql-reference/types/UserIdentifierInput
Identifies a Plain user or machine user. Exactly one field must be set.
# UserImpersonationInput
Source: https://www.plain.com/docs/graphql-reference/types/UserImpersonationInput
# UserLinearInstallationInfo
Source: https://www.plain.com/docs/graphql-reference/types/UserLinearInstallationInfo
# UserLinearIntegration
Source: https://www.plain.com/docs/graphql-reference/types/UserLinearIntegration
# UserMSTeamsInstallationInfo
Source: https://www.plain.com/docs/graphql-reference/types/UserMSTeamsInstallationInfo
# UserMSTeamsIntegration
Source: https://www.plain.com/docs/graphql-reference/types/UserMSTeamsIntegration
# UserSlackInstallationInfo
Source: https://www.plain.com/docs/graphql-reference/types/UserSlackInstallationInfo
# UserSlackIntegration
Source: https://www.plain.com/docs/graphql-reference/types/UserSlackIntegration
# UserStatus
Source: https://www.plain.com/docs/graphql-reference/types/UserStatus
# UserWorkingHours
Source: https://www.plain.com/docs/graphql-reference/types/UserWorkingHours
Configuration for automatic status switching based on a user's working hours.
# UserWorkingHoursSlot
Source: https://www.plain.com/docs/graphql-reference/types/UserWorkingHoursSlot
A time slot representing when a user is available during a specific day.
# UserWorkingHoursSlotInput
Source: https://www.plain.com/docs/graphql-reference/types/UserWorkingHoursSlotInput
# UsersFilter
Source: https://www.plain.com/docs/graphql-reference/types/UsersFilter
# VerifyHelpCenterCustomDomainNameInput
Source: https://www.plain.com/docs/graphql-reference/types/VerifyHelpCenterCustomDomainNameInput
# VerifyHelpCenterCustomDomainNameOutput
Source: https://www.plain.com/docs/graphql-reference/types/VerifyHelpCenterCustomDomainNameOutput
# VerifyWorkspaceEmailDnsSettingsOutput
Source: https://www.plain.com/docs/graphql-reference/types/VerifyWorkspaceEmailDnsSettingsOutput
# VerifyWorkspaceEmailForwardingSettingsInput
Source: https://www.plain.com/docs/graphql-reference/types/VerifyWorkspaceEmailForwardingSettingsInput
# VerifyWorkspaceEmailForwardingSettingsOutput
Source: https://www.plain.com/docs/graphql-reference/types/VerifyWorkspaceEmailForwardingSettingsOutput
# WebhookDeliveryAttempt
Source: https://www.plain.com/docs/graphql-reference/types/WebhookDeliveryAttempt
# WebhookDeliveryAttemptConnection
Source: https://www.plain.com/docs/graphql-reference/types/WebhookDeliveryAttemptConnection
# WebhookDeliveryAttemptEdge
Source: https://www.plain.com/docs/graphql-reference/types/WebhookDeliveryAttemptEdge
# WebhookDeliveryAttemptErrorResult
Source: https://www.plain.com/docs/graphql-reference/types/WebhookDeliveryAttemptErrorResult
# WebhookDeliveryAttemptFailedResult
Source: https://www.plain.com/docs/graphql-reference/types/WebhookDeliveryAttemptFailedResult
# WebhookDeliveryAttemptFilter
Source: https://www.plain.com/docs/graphql-reference/types/WebhookDeliveryAttemptFilter
# WebhookDeliveryAttemptRejectedResult
Source: https://www.plain.com/docs/graphql-reference/types/WebhookDeliveryAttemptRejectedResult
# WebhookDeliveryAttemptResult
Source: https://www.plain.com/docs/graphql-reference/types/WebhookDeliveryAttemptResult
# WebhookDeliveryAttemptResultStatus
Source: https://www.plain.com/docs/graphql-reference/types/WebhookDeliveryAttemptResultStatus
# WebhookDeliveryAttemptSchemaValidationFailedResult
Source: https://www.plain.com/docs/graphql-reference/types/WebhookDeliveryAttemptSchemaValidationFailedResult
# WebhookDeliveryAttemptSuccessfulResult
Source: https://www.plain.com/docs/graphql-reference/types/WebhookDeliveryAttemptSuccessfulResult
# WebhookTarget
Source: https://www.plain.com/docs/graphql-reference/types/WebhookTarget
# WebhookTargetConnection
Source: https://www.plain.com/docs/graphql-reference/types/WebhookTargetConnection
# WebhookTargetEdge
Source: https://www.plain.com/docs/graphql-reference/types/WebhookTargetEdge
# WebhookTargetEventSubscription
Source: https://www.plain.com/docs/graphql-reference/types/WebhookTargetEventSubscription
# WebhookTargetEventSubscriptionInput
Source: https://www.plain.com/docs/graphql-reference/types/WebhookTargetEventSubscriptionInput
# WebhookTargetHeader
Source: https://www.plain.com/docs/graphql-reference/types/WebhookTargetHeader
A custom HTTP header sent with every webhook delivery. Only the name is exposed — values can be auth tokens and are never returned; supply a value via an input to set or change it.
# WebhookTargetHeaderInput
Source: https://www.plain.com/docs/graphql-reference/types/WebhookTargetHeaderInput
A custom HTTP header sent with every webhook delivery.
# WebhookVersion
Source: https://www.plain.com/docs/graphql-reference/types/WebhookVersion
# WebhookVersionConnection
Source: https://www.plain.com/docs/graphql-reference/types/WebhookVersionConnection
# WebhookVersionEdge
Source: https://www.plain.com/docs/graphql-reference/types/WebhookVersionEdge
# WeekDay
Source: https://www.plain.com/docs/graphql-reference/types/WeekDay
# WorkOSConfiguration
Source: https://www.plain.com/docs/graphql-reference/types/WorkOSConfiguration
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.
# Workflow
Source: https://www.plain.com/docs/graphql-reference/types/Workflow
# WorkflowCapabilities
Source: https://www.plain.com/docs/graphql-reference/types/WorkflowCapabilities
Which blocks a workflow of a given trigger type may contain. Used to drive the builder palette.
# WorkflowConnection
Source: https://www.plain.com/docs/graphql-reference/types/WorkflowConnection
# WorkflowEdge
Source: https://www.plain.com/docs/graphql-reference/types/WorkflowEdge
# WorkflowExecution
Source: https://www.plain.com/docs/graphql-reference/types/WorkflowExecution
# WorkflowExecutionByEntityFilter
Source: https://www.plain.com/docs/graphql-reference/types/WorkflowExecutionByEntityFilter
# WorkflowExecutionConnection
Source: https://www.plain.com/docs/graphql-reference/types/WorkflowExecutionConnection
# WorkflowExecutionEdge
Source: https://www.plain.com/docs/graphql-reference/types/WorkflowExecutionEdge
# WorkflowExecutionEntityType
Source: https://www.plain.com/docs/graphql-reference/types/WorkflowExecutionEntityType
# WorkflowExecutionStatus
Source: https://www.plain.com/docs/graphql-reference/types/WorkflowExecutionStatus
The lifecycle state of a workflow execution.
# WorkflowExecutionsFilter
Source: https://www.plain.com/docs/graphql-reference/types/WorkflowExecutionsFilter
Optional narrowing for `workflowExecutionsForWorkspace`. Every field is independent; omit them all to list the whole workspace.
# WorkflowRule
Source: https://www.plain.com/docs/graphql-reference/types/WorkflowRule
# WorkflowRuleConnection
Source: https://www.plain.com/docs/graphql-reference/types/WorkflowRuleConnection
# WorkflowRuleEdge
Source: https://www.plain.com/docs/graphql-reference/types/WorkflowRuleEdge
# WorkflowShareLink
Source: https://www.plain.com/docs/graphql-reference/types/WorkflowShareLink
# WorkflowShareLinkStep
Source: https://www.plain.com/docs/graphql-reference/types/WorkflowShareLinkStep
# WorkflowStep
Source: https://www.plain.com/docs/graphql-reference/types/WorkflowStep
# WorkflowStepEntityRef
Source: https://www.plain.com/docs/graphql-reference/types/WorkflowStepEntityRef
An entity an action step produced or affected (e.g. a Sidekick discussion it started), so the run can link to it.
# WorkflowStepEntityType
Source: https://www.plain.com/docs/graphql-reference/types/WorkflowStepEntityType
# WorkflowStepExecution
Source: https://www.plain.com/docs/graphql-reference/types/WorkflowStepExecution
# WorkflowStepExecutionStatus
Source: https://www.plain.com/docs/graphql-reference/types/WorkflowStepExecutionStatus
# WorkflowStepType
Source: https://www.plain.com/docs/graphql-reference/types/WorkflowStepType
The role a step plays in a workflow's execution graph.
# WorkflowSummary
Source: https://www.plain.com/docs/graphql-reference/types/WorkflowSummary
A summary of a workflow with only id and name fields.
# WorkflowTemplate
Source: https://www.plain.com/docs/graphql-reference/types/WorkflowTemplate
# WorkflowTemplateContentBlock
Source: https://www.plain.com/docs/graphql-reference/types/WorkflowTemplateContentBlock
# WorkflowTemplateDependencies
Source: https://www.plain.com/docs/graphql-reference/types/WorkflowTemplateDependencies
# WorkflowTemplateFile
Source: https://www.plain.com/docs/graphql-reference/types/WorkflowTemplateFile
# WorkflowTemplateGalleryItem
Source: https://www.plain.com/docs/graphql-reference/types/WorkflowTemplateGalleryItem
# WorkflowTemplateStep
Source: https://www.plain.com/docs/graphql-reference/types/WorkflowTemplateStep
# WorkflowTemplateWorkflow
Source: https://www.plain.com/docs/graphql-reference/types/WorkflowTemplateWorkflow
A workflow a template installs. Deliberately mirrors the shape the workflow editor parses.
# WorkflowTriggerType
Source: https://www.plain.com/docs/graphql-reference/types/WorkflowTriggerType
Controls how a workflow is triggered.
# WorkflowsFilter
Source: https://www.plain.com/docs/graphql-reference/types/WorkflowsFilter
# WorkflowsSort
Source: https://www.plain.com/docs/graphql-reference/types/WorkflowsSort
# WorkflowsSortField
Source: https://www.plain.com/docs/graphql-reference/types/WorkflowsSortField
How to order the `workflows` connection.
# WorkosConnectAuthMechanismInput
Source: https://www.plain.com/docs/graphql-reference/types/WorkosConnectAuthMechanismInput
# Workspace
Source: https://www.plain.com/docs/graphql-reference/types/Workspace
# WorkspaceChatSettings
Source: https://www.plain.com/docs/graphql-reference/types/WorkspaceChatSettings
Workspace-level settings controlling the chat channel.
# WorkspaceConnection
Source: https://www.plain.com/docs/graphql-reference/types/WorkspaceConnection
# WorkspaceCursorIntegration
Source: https://www.plain.com/docs/graphql-reference/types/WorkspaceCursorIntegration
# WorkspaceDiscordChannelInstallationInfo
Source: https://www.plain.com/docs/graphql-reference/types/WorkspaceDiscordChannelInstallationInfo
# WorkspaceDiscordChannelIntegration
Source: https://www.plain.com/docs/graphql-reference/types/WorkspaceDiscordChannelIntegration
# WorkspaceDiscordChannelIntegrationConnection
Source: https://www.plain.com/docs/graphql-reference/types/WorkspaceDiscordChannelIntegrationConnection
# WorkspaceDiscordChannelIntegrationEdge
Source: https://www.plain.com/docs/graphql-reference/types/WorkspaceDiscordChannelIntegrationEdge
# WorkspaceDiscordIntegration
Source: https://www.plain.com/docs/graphql-reference/types/WorkspaceDiscordIntegration
# WorkspaceDiscordIntegrationConnection
Source: https://www.plain.com/docs/graphql-reference/types/WorkspaceDiscordIntegrationConnection
# WorkspaceDiscordIntegrationEdge
Source: https://www.plain.com/docs/graphql-reference/types/WorkspaceDiscordIntegrationEdge
# WorkspaceEdge
Source: https://www.plain.com/docs/graphql-reference/types/WorkspaceEdge
# WorkspaceEmailDomainSettings
Source: https://www.plain.com/docs/graphql-reference/types/WorkspaceEmailDomainSettings
# WorkspaceEmailSettings
Source: https://www.plain.com/docs/graphql-reference/types/WorkspaceEmailSettings
# WorkspaceFile
Source: https://www.plain.com/docs/graphql-reference/types/WorkspaceFile
# WorkspaceFileDownloadUrl
Source: https://www.plain.com/docs/graphql-reference/types/WorkspaceFileDownloadUrl
# WorkspaceFileInput
Source: https://www.plain.com/docs/graphql-reference/types/WorkspaceFileInput
# WorkspaceFileUploadUrl
Source: https://www.plain.com/docs/graphql-reference/types/WorkspaceFileUploadUrl
# WorkspaceFileVisibility
Source: https://www.plain.com/docs/graphql-reference/types/WorkspaceFileVisibility
Controls where the file is stored and how it can be accessed.
# WorkspaceHmac
Source: https://www.plain.com/docs/graphql-reference/types/WorkspaceHmac
# WorkspaceInvite
Source: https://www.plain.com/docs/graphql-reference/types/WorkspaceInvite
# WorkspaceInviteConnection
Source: https://www.plain.com/docs/graphql-reference/types/WorkspaceInviteConnection
# WorkspaceInviteEdge
Source: https://www.plain.com/docs/graphql-reference/types/WorkspaceInviteEdge
# WorkspaceLinearInstallationInfo
Source: https://www.plain.com/docs/graphql-reference/types/WorkspaceLinearInstallationInfo
# WorkspaceLinearIntegration
Source: https://www.plain.com/docs/graphql-reference/types/WorkspaceLinearIntegration
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.
# WorkspaceMSTeamsInstallationInfo
Source: https://www.plain.com/docs/graphql-reference/types/WorkspaceMSTeamsInstallationInfo
# WorkspaceMSTeamsIntegration
Source: https://www.plain.com/docs/graphql-reference/types/WorkspaceMSTeamsIntegration
# WorkspaceSlackChannelInstallationInfo
Source: https://www.plain.com/docs/graphql-reference/types/WorkspaceSlackChannelInstallationInfo
# WorkspaceSlackChannelIntegration
Source: https://www.plain.com/docs/graphql-reference/types/WorkspaceSlackChannelIntegration
# WorkspaceSlackChannelIntegrationConnection
Source: https://www.plain.com/docs/graphql-reference/types/WorkspaceSlackChannelIntegrationConnection
# WorkspaceSlackChannelIntegrationEdge
Source: https://www.plain.com/docs/graphql-reference/types/WorkspaceSlackChannelIntegrationEdge
# WorkspaceSlackInstallationInfo
Source: https://www.plain.com/docs/graphql-reference/types/WorkspaceSlackInstallationInfo
# WorkspaceSlackIntegration
Source: https://www.plain.com/docs/graphql-reference/types/WorkspaceSlackIntegration
# WorkspaceSlackIntegrationConnection
Source: https://www.plain.com/docs/graphql-reference/types/WorkspaceSlackIntegrationConnection
# WorkspaceSlackIntegrationEdge
Source: https://www.plain.com/docs/graphql-reference/types/WorkspaceSlackIntegrationEdge
# WorkspaceSlackSidekickInstallationInfo
Source: https://www.plain.com/docs/graphql-reference/types/WorkspaceSlackSidekickInstallationInfo
# WorkspaceSlackSidekickIntegration
Source: https://www.plain.com/docs/graphql-reference/types/WorkspaceSlackSidekickIntegration
# AI feedback
Source: https://www.plain.com/docs/graphql/ai-feedback
List the feedback your team has left on Ari replies and other Plain AI features.
When a user rates an [Ari](/docs/product/agents/ari) reply or another Plain AI feature with a thumbs up or down, Plain stores the vote and any comment. `aiFeedback` lists that feedback, newest first.
The API is read-only. Feedback is submitted from the Plain app.
This operation requires the following permission:
* `aiFeatureFeedback:read`
## Get AI feedback
`details` depends on the `feature`, so select its fields with fragments:
* **`AI_AGENT`**: an Ari reply, with `threadId` and `timelineEntryId`
* **`THREAD_CATCHUP`**: a thread summary, with `threadId`
* **`THREAD_CLUSTERS`**: a cluster of related threads, with `clusterId`
* **`TONE_RULE`**: a [tone of voice](/docs/product/agents/tone-of-voice) rule, with `toneRuleId`
* **`KNOWLEDGE_GAP`**: a [knowledge gap](/docs/product/agents/knowledge-gaps), with `knowledgeGapId`
Every `details` type has a `sentiment`, a `reason`, and an optional `comment`.
Filter with `features`, `sentiments`, and a `createdAt` window. Feedback submitted before Plain captured sentiment has none, so `sentiments` excludes it.
# API explorer
Source: https://www.plain.com/docs/graphql/api-explorer
# Attachments
Source: https://www.plain.com/docs/graphql/attachments
How to upload attachments programmatically for messages and events in Plain.
This page outlines how to upload attachments programmatically.
At a high level to upload attachments you:
* Make an API call to get an upload url and some metadata
* You then upload your file, and metadata to that upload url.
* Use the ID of the attachment you uploaded in other API calls (e.g. create a thread or send an email).
## Step by step guide
To try this, you will need an [API key](/docs/graphql/authentication/) with the following permission:
* `attachment:create`
- `fileName` is the name under which the attachment will appear in the timeline
- `fileSizeBytes` is the exact size of the attachment in bytes
- `c_XXXXXXXXXXXXXXXXXXXXXXXXXX` is the customer id you are uploading the attachment for
The GraphQL mutation to create an attachment upload URL is the following:
In the `AttachmentUploadUrl` we created in the previous step we get back two fields needed to upload the attachment:
* `uploadFormUrl`: The URL to which to upload the file to
* `uploadFormData`: A list of key, value pairs that have to be included in the data we upload along with the actual file data.
With this information we can now upload our actual file to Plain. To do this we need to build a form (`multipart/form-data`) with the data contained in `uploadFormData` and submit it to the `uploadFormUrl`.
Here is some example code showing how you would do this in the Browser and from a Node server:
## Limitations
* A maximum of **100 attachments** can be added to a message
* The **combined** size of all attachments you add to a message cannot exceed the following limits based on attachment type:
* **Email attachments**: 6 MB
* **Chat attachments**: 100 MB
* **Slack attachments**: 50 MB
* **Microsoft Teams attachments**: 50 MB
* **Discord attachments**: 50 MB
* **Thread discussion attachments**: 50 MB
* **Note attachments**: 50 MB
* The following file extensions are not allowed as attachments:
`
bat, bin, chm, com, cpl, crt, exe, hlp, hta, inf, ins, isp, jse, lnk, mdb, msc, msi, msp, mst, pcd, pif, reg, scr, sct, shs, vba, vbe, vbs, wsf, wsh, wsl`
* Attachments uploaded, but never referenced by a message, will be
**deleted after 24 hours**.
* Upload URLs are only **valid for 2 hours** after which a new URL needs to be created.
# Authentication
Source: https://www.plain.com/docs/graphql/authentication
Create a machine user and an API key, and scope it with fine-grained permissions.
A machine user can have several API keys, so you can rotate them without downtime. Each API key carries its own fine-grained permissions.
Go to **Settings** → **Machine Users** and click **+ Add Machine User**
A machine user has two fields:
* **Name:** visible only to you and could indicate the usage e.g. "Autoresponder"
* **Public name:** This is the name visible to customers (if the machine user interacts with customers) e.g. "Mr Robot"
Click "Add API key" and select the permissions you need. When making API calls, if you have insufficient permissions, the error should tell you which permissions you need.
The relevant documentation will tell you which permissions are required for each feature.
Once you've made an API key you should copy it and put it somewhere safe, as you will not be able to see it again once you navigate away.
Now that you have an API key, pass it on any API call as a header:
```plaintext theme={null}
Authorization: Bearer plainApiKey_xxx
```
# Companies
Source: https://www.plain.com/docs/graphql/companies
Companies group customers by email domain. Plain infers them automatically, and you can set them yourself.
Within Plain every customer can belong to one company. The company is inferred automatically using the customer's email address. For example if their email address ends with "@nike.com" then their company will be automatically set to "Nike".
Companies allow you to prioritize and filter your threads.
Additionally [tiers and SLAs](/docs/product/platform/tiers) can be associated with a company.
# Delete a company
Source: https://www.plain.com/docs/graphql/companies/delete
Delete a company and unlink it from its customers, without deleting the customers themselves.
Deleting a company unlinks it from all of its customers. The customers themselves are not deleted, they no longer belong to a company.
A company is identified by either its Plain `companyId` or its `companyDomainName`.
This operation requires the following permissions:
* `company:delete`
# Fetch companies
Source: https://www.plain.com/docs/graphql/companies/get-companies
Fetch companies by ID, by domain, or as a paginated collection.
There are three ways to fetch companies:
* [Get companies](#get-companies) (To fetch more than one company at a time)
* [Get company by ID](#get-company-by-id)
* [Search for companies](#search-for-companies)
These operations require the following permissions:
* `company:read`
## Get companies
You can get all companies you've interacted with in your workspace using the `companies` query. This endpoint supports [Pagination](/docs/graphql/pagination).
## Get company by ID
If you already have the ID of a company you can fetch it directly using the `company` query.
## Search for companies
The `searchCompanies` query performs a case-insensitive partial match across a company's name and domain. The search term must be at least 2 characters long.
# Update customer company
Source: https://www.plain.com/docs/graphql/companies/update-customer-company
Override the company Plain inferred for a customer, or clear it.
Plain automatically derives a customer's company for you, but you can also update it manually.
The customer in question is identified by their id (ie `c_...`).
With regards to the company, you can either specify an existing company using the ID we've generated (ie `co_...`), or pass the company domain, which we'll use to derive the rest of the company's info.
If you wish to only remove the customer's associated company, then you can pass `null` as the `companyIdentifier`.
This operation requires the following permissions:
* `customer:edit`
# Upsert a company
Source: https://www.plain.com/docs/graphql/companies/upsert
Create or update a company by domain, so you control its name and details rather than the inferred ones.
Plain auto-creates companies from customer email domains, but you can also upsert them directly via the API. This is useful when you want to set details like the company name, logo or account owner ahead of any customers being created.
`upsertCompany` will create a new company if one with the given identifier doesn't exist, or update it in place if it does. The mutation returns a `result` field of either `CREATED` or `UPDATED` so you can tell which happened.
A company is identified by either its Plain `companyId` or its `companyDomainName`. When upserting by domain, you can pass either a bare domain (e.g. `plain.com`) or a full URL (e.g. `https://www.plain.com`) and we'll extract the domain for you.
This operation requires the following permissions:
* `company:create`
* `company:edit`
# Building your own ticket importer
Source: https://www.plain.com/docs/graphql/custom-ticket-importer
Write your own importer to bring support history into Plain from a tool with no built-in importer.
If your support history lives somewhere Plain doesn't have a one-click importer for, you can bring it across yourself with a short script built on the Plain API.
## When to use this
Plain has one-click importers for [Zendesk](/docs/product/integrations/zendesk), [Freshdesk](/docs/product/integrations/freshdesk), [Intercom](/docs/product/integrations/intercom), and [Help Scout](/docs/product/integrations/help-scout). If you're coming from one of those, use the importer and skip this page.
A custom import is the route for everything else: a less common support tool, a shared inbox, an internal ticketing system, or a spreadsheet of past conversations you still want your team to be able to search.
## What comes across
A custom import gives you the same foundations as the one-click importers:
* **Your conversation history**, as threads with their messages, replies, and internal notes
* **Original timestamps**, on every thread and every message, so your archive reflects when each thing happened
* **The right authors**, with inbound messages attributed to the customer and replies attributed to the user who sent them
* **Attachments**, uploaded to Plain and attached to the messages they belong to
* **Thread details** you decide to map across, including title, status, priority, labels, assignee, and a link back to the original ticket
Imported threads don't trigger [SLAs](/docs/product/platform/slas), [auto-responses](/docs/product/platform/auto-responses), or [workflows](/docs/product/workflows). Bringing in years of history is quiet, and nothing goes out to your customers.
## What's involved
You'll write a script that reads conversations out of your current tool and creates them in Plain. In outline:
1. Create an API key with permission to import threads
2. Make sure the customers you're importing for exist in Plain
3. For each ticket, create the thread with its original date and details
4. Add that thread's messages, in batches, with attachments where you have them
5. Check the result for each item so you can retry anything that didn't land
Two things make this safer than it sounds. Each thread is imported against the reference it had in your old tool, so running the script twice skips anything already in Plain instead of creating duplicates. And you can start with a handful of tickets, check how they look in Plain, then run the rest.
## Before you start
Read [Importing threads](/docs/graphql/threads/import) in the developer docs. It has the full technical detail: the API reference, the permissions you'll need, how messages and attachments are structured, and examples of each call.
It's worth planning the import as part of your wider move. [Migrating from another tool](https://help.plain.com/article/migration) covers the order most teams work in and what you'll rebuild in Plain rather than import.
If you'd like a hand scoping the script or a review before you run it at full volume, email [help@plain.com](mailto:help@plain.com).
# Customers
Source: https://www.plain.com/docs/graphql/customers
Customers are created automatically when someone contacts you, and you can also upsert them yourself.
Customers that reach out to you will automatically be created in Plain without requiring any API integration.
However, using our API to manage customers proactively can be helpful when you are optimizing your support workflow.
For example:
* You can [**put customers into groups**](/docs/graphql/customers/customer-groups/) to better organize your support queue. For example, you could group customers by pricing tier (e.g. Free Tier, Teams, Enterprise)
* You can [**create customers**](/docs/graphql/customers/upsert/) in Plain when they sign-up on your own site so that you can reach out to them proactively without waiting for them to get in touch.
* You can [**save your own customer's ID**](/docs/graphql/customers/upsert) for use with [**customer cards**](/docs/customer-cards/).
# Customer groups
Source: https://www.plain.com/docs/graphql/customers/customer-groups
Segment customers into named groups you define, separate from companies and tenants.
Customer groups can be used to group and segment your customers. For example you could organize your customers by their tier "Free", "Growth", "Enterprise" or make use of groups to keep track of customers trialing beta features.
Customers can belong to one or many groups. You can filter customer threads by group, so you can focus on a subset of them.
You can add customers to groups through the API, or in the Plain app.
This guide assumes you've already created some customer groups in **Settings** → **Customer Groups**.
## Add a customer to groups
A customer can be added to a customer group using the `addCustomerToCustomerGroup` mutation.
When to call this API depends on what your customer groups represent. If you group customers by pricing tier, call it every time their tier changes.
This operation requires the following permissions:
* `customer:create`
* `customer:edit`
If you prefer you can also use the customer group id instead of the key. You can do this like so:
## Remove a customer from groups
A customer can be removed from a customer group by using the `removeCustomerFromGroup` mutation.
If you prefer you can also use the customer group id instead of the key. You can do this like so:
# Delete customers
Source: https://www.plain.com/docs/graphql/customers/delete
Permanently delete a customer and their threads with the deleteCustomer mutation.
You can delete customers with the `deleteCustomer` API.
To delete a customer you will need the customer's ID from within Plain. You can get this ID in the UI by going to a thread from that customer and pressing the 'Copy ID' button from the customer details panel on the right, or via our [fetch API](/docs/graphql/customers/get).
Deleting a customer will trigger an asynchronous process which causes all data (such as threads) associated with that customer to be deleted.
This operation requires the following permissions:
* `customer:delete`
# Fetch customers
Source: https://www.plain.com/docs/graphql/customers/get
Fetch customers by ID, email, or external ID, or as a paginated collection.
There are five ways to fetch customers:
* [Get customers](#get-customers) (To fetch more than one customer at a time)
* [Get customer by ID](#get-customer-by-id)
* [Get customer by email](#get-customer-by-email)
* [Get customer by external ID](#get-customer-by-external-id)
* [Search for customers](#search-for-customers)
These operations require the following permissions:
* `customer:read`
## Get customers
Our API allows you to fetch customers as a collection using the `customers` query in GraphQL. This endpoint supports [Pagination](/docs/graphql/pagination).
This endpoint supports a range of filtering and sorting options, for full details try our [API explorer](https://app.plain.com/developer/api-explorer/).
## Get customer by ID
If you already have the ID of a customer from within Plain or one of our other endpoints you can fetch more details about them using the `customer` query in GraphQL.
## Get customer by email
To fetch a customer by email you can use the `customerByEmail` query in GraphQL.
## Get customer by external ID
If you store a stable identifier from your own system on Plain customers (for example your internal user ID), you can fetch the customer back by that value using the `customerByExternalId` query.
External IDs are unique within a workspace.
## Search for customers
The `searchCustomers` query lets you do a case-insensitive partial match across a customer's name, email, short name and external ID. This is the same search behavior as the customer picker in the Plain app and is best suited for human-driven lookups rather than precise programmatic resolution.
For exact lookups by email or external ID, prefer [`customerByEmail`](#get-customer-by-email) or [`customerByExternalId`](#get-customer-by-external-id).
# Mark customer as spam
Source: https://www.plain.com/docs/graphql/customers/spam
Flag a customer as spam to hide their threads and keep them out of your metrics.
You can flag a customer as spam to hide their threads from the main inbox and stop them being included in metrics. This is useful for closing the loop on automated handling of throwaway accounts, bot traffic or known abusers.
When a customer is marked as spam their `markedAsSpamAt` timestamp is set. The mutation is idempotent: calling it on an already-spam customer leaves the timestamp unchanged.
This operation requires the following permissions:
* `customer:edit`
## Mark a customer as spam
## Unmark a customer as spam
To reverse the above, use `unmarkCustomerAsSpam`. The customer's `markedAsSpamAt` timestamp is cleared and their threads start appearing in the inbox again.
# Upserting customers
Source: https://www.plain.com/docs/graphql/customers/upsert
Learn how to create and update customers programmatically.
Creating and updating customers is handled via a single API called `upsertCustomer`.
When you upsert a customer, you define:
1. The identifier: This is the field you'd like to use to select the customer and is one of
* `emailAddress`: This is the customer's email address. Within Plain email addresses are unique to customers.
* `customerId`: This is Plain's customer ID. Implicitly if you use this as an identifier you will only be updating the customer since the customer can't have an id unless it already exists.
* `externalId`: This is the customer's id in your systems. If you set this, you can use it to sync customer details from your backend with Plain.
2. The customer details you'd like to use if creating the customer.
3. The customer details you'd like to update if the customer already exists.
When upserting a customer you will always get back a customer or an error.
## Upserting a customer
This operation requires the following permissions:
* `customer:create`
* `customer:edit`
This will:
* Find a customer with the email '[donald@example.com](mailto:donald@example.com)' (the identifier).
* If a customer with that email exists will update it (see `onUpdate` below)
* Otherwise, it will create the customer (see `onCreate` below)
The GraphQL mutation is the following:
The value of the `result` type will be:
* `CREATED`: if a customer didn't exist and was created
* `UPDATED`: if a customer already existed AND the values being updated **were different**.
* `NOOP`: if a customer already existed AND the values being updated **were the same**
# Discussions
Source: https://www.plain.com/docs/graphql/discussions
Discussions are side-conversations about a thread, held in Slack or over email, away from the customer.
A **discussion** is a side-conversation about a thread, held in a Slack thread or an email chain where users pull in expertise from elsewhere in the company. Discussions show up in the thread timeline so context isn't lost.
Discussions are mostly created from within the Plain app, but the API is useful if you want to programmatically loop a specific Slack channel into certain types of threads (for example, automatically open a Slack discussion in `#billing-support` for every thread tagged `Billing`).
# Start a discussion
Source: https://www.plain.com/docs/graphql/discussions/create
Open a Slack or email side-conversation attached to an existing thread.
Opening a discussion attaches a new side-conversation to an existing thread. The `type` determines where messages are exchanged:
* `SLACK`: posts a new Slack thread in a [connected Slack channel](/docs/product/channels/slack). Requires `slackDetails.connectedSlackChannelId`.
* `EMAIL`: sends a new email to one or more recipients. Requires `emailDetails.toAddresses`.
The `markdownContent` is the first message of the discussion. For Slack discussions you can also pass `slackBlocks` (a JSON-encoded [Slack Block Kit](https://api.slack.com/block-kit) array) to render rich content in Slack; `markdownContent` remains the fallback rendered in Plain.
# Fetch discussions
Source: https://www.plain.com/docs/graphql/discussions/get
Fetch discussions, filtered by status, thread, creator, or last activity.
You can fetch discussions as a paginated collection with filters, or fetch a single discussion by ID.
## List discussions
The `discussions` query supports filtering by status, thread, creator, last-activity timestamps and more.
## Get a discussion by ID
# Send a discussion message
Source: https://www.plain.com/docs/graphql/discussions/send-message
Post a new message into an existing discussion, in its original channel.
Adds a new message to an existing discussion. The message is posted in the original channel (Slack thread reply or email reply) and recorded against the discussion in Plain.
# Error codes
Source: https://www.plain.com/docs/graphql/error-codes
Look up what an error code from the Plain API means and how to handle it.
Every error the Plain API returns carries a code. This page lists each one, what causes it, and what to do about it.
## `input_validation`
The provided input failed validation. See field errors for details.
## `forbidden`
Permission denied.
## `internal`
An internal server error. The request should be retried. If the error persists, please get in touch at [help@plain.com](mailto:help@plain.com)
## `not_found`
An entity referenced in the request is not found. For example trying to create an issue for a customer that doesn't exist.
## `not_yet_implemented`
The API is not yet implemented. If you think it should already be implemented please get in touch at [help@plain.com](mailto:help@plain.com)
## `action_not_allowed_in_demo_workspace`
The performed action is not allowed for a demo workspace.
## `attachment_file_size_too_large`
The attachment being uploaded exceeds the limit (6 MB)
## `attachment_file_type_not_allowed`
The file type is not allowed. Banned file types: `bat`, `bin`, `chm`, `com`, `cpl`, `crt`, `exe`, `hlp`, `hta`, `inf`, `ins`, `isp`, `jse`, `lnk`, `mdb`, `msc`, `msi`, `msp`, `mst`, `pcd`, `pif`, `reg`, `scr`, `sct`, `shs`, `vba`, `vbe`, `vbs`, `wsf`, `wsh`, `wsl`
## `attachment_not_uploaded`
The attachment ID being referenced was created, but not uploaded. Upload the attachment and try again.
## `cannot_assign_customer_to_user`
The user that the customer is being assigned to doesn't have a role that is capable of helping the customer. Assign the "Help customers" role to the user and try again.
## `cannot_remove_only_admin_user`
Can't remove the last user with an admin role. Assign another user the admin role as well and try again.
## `cannot_reply_to_unsent_email`
The email being replied to has yet to be sent. Wait until the email is sent and try again.
## `cannot_create_thread_from_slack_message`
The Slack message cannot be ingested. The channel is not connected, is disabled, or is not a customer channel, or the message is a reply rather than a top-level message.
## `cannot_update_field`
Some Custom Timeline Entry fields can't be updated but only created (such as `timestamp`). Delete the Custom Timeline Entry and recreate it if you want to update these fields.
## `customer_already_exists_with_email`
A customer with this email already exists in the workspace and can't be created again.
## `customer_already_exists_with_external_id`
A customer with this external id already exists in the workspace and can't be created again.
## `customer_already_is_status`
An attempt to change a customer to a status that the customer already is was made.
## `customer_already_marked_as_spam`
The customer has already been marked as spam and can't be marked as spam again.
## `customer_card_config_key_already_exists`
A Customer Card config with this key already exists in the workspace and can't be created again.
## `customer_is_marked_as_spam`
An action was attempted but cannot be performed as the customer is marked as spam.
## `customer_is_not_marked_as_spam`
An action was attempted but cannot be performed as it requires the customer to be marked as spam.
## `customer_status_cannot_be_changed_to_idle`
The customer's status cannot be changed to idle, see error for reason.
## `customer_jwt_expired`
The customer's JWT has expired. Recreate the JWT and try again.
## `customer_jwt_invalid`
The customer's JWT is in an invalid format. Fix the contents of the JWT and try again.
## `customer_group_has_memberships`
The customer group has memberships and can't be deleted.
## `customer_group_key_already_exists`
A customer group with this key already exists in the workspace and can't be created again.
## `customer_session_challenge_invalid`
The provided customer challenge digits are invalid.
## `domain_already_taken`
The domain in the support email address is already taken by a different workspace. Currently only one workspace can use a domain. If this is an issue, please contact [help@plain.com](mailto:help@plain.com).
## `domain_cannot_be_public`
The domain in the support email address is considered public and cannot be used.
## `insufficient_permissions`
The user doesn't have the required permissions to create an API key that has more permissions than the user itself.
## `issue_already_open`
Issue is already in an open state and can't be opened.
## `issue_already_resolved`
Issue is already in a resolved state and can't be resolved.
## `issue_already_this_issue_type`
Issue is already the provided issue type and can't be changed to it.
## `issue_already_this_priority`
Issue is already the provided priority and can't be changed to it.
## `linear_issue_already_linked_to_issue`
Issue is already linked to the provided Linear issue and cannot be linked again.
## `linear_organisation_cannot_be_authorised`
The Linear OAuth flow failed. Detailed reasoning is included in the error.
## `mark_as_read_user_must_be_assigned_to_customer`
The user trying to mark the timeline as read isn't the user that is assigned to the customer. Assign the user to the customer and try again.
## `roles_at_least_one_admin_required`
Can't remove the role for the user as it would leave no users with the admin role in the workspace. Assign another user the admin role as well and try again.
## `too_many_customer_card_configs`
The maximum number of Customer Card configs has been reached for this workspace.
## `too_many_webhook_targets`
The maximum number of webhook targets has been reached for this webhook.
## `user_account_already_exists`
A User Account already exists for the current user.
## `user_already_this_status`
User is already in this status and can't be changed to it.
## `user_linear_integration_not_found`
A User does not have a Linear integration setup.
## `workspace_app_key_not_found`
The workspace app key can't be found.
## `workspace_app_public_key_required`
A workspace app key must be provided.
## `workspace_app_required`
A workspace app must be provided.
## `workspace_chat_not_enabled`
Chat is disabled so chat messages can't be sent.
## `workspace_email_domain_not_configured`
Email domain settings aren't fully configured yet. Double check the email settings page.
## `workspace_email_domain_not_set`
A support email is not configured in the email settings. Double check the email settings page.
## `workspace_email_forwarding_not_configured`
Email forwarding settings aren't fully configured yet. Double check the email settings page.
## `workspace_email_not_enabled`
Email is not enabled so emails can't be sent.
## `workspace_invite_already_accepted`
The invite has already been accepted by the user.
## `workspace_invite_email_already_invited`
The email trying to be invited already has an outstanding invite.
## `workspace_invite_email_already_member_of_workspace`
The email trying to be invited is already a member of the workspace.
## `workspace_invite_email_doesnt_match`
The user trying to accept the invite has a different email than the invite is for.
## `workspace_support_email_address_conflict`
The entered support email address is already taken by a different workspace.
## `workspace_user_email_already_used_as_support_email`
The provided email is already used as a support email.
## `you_shall_not_pass`
🧙 User account signup is currently blocked.
# Error handling
Source: https://www.plain.com/docs/graphql/error-handling
GraphQL queries and mutations require different error handling.
Plain models query errors and mutation errors differently, because the two fail for different reasons:
* **Queries** fail for only three common reasons: unauthenticated, forbidden, or an internal server error. Unauthenticated and forbidden mean the API keys are invalid; internal server errors should be retried.
* **Mutations** return errors regularly as part of the normal business flow, due to invalid inputs. Errors include enough detail to display to whoever is using your product.
## Query errors
Query errors aren't modeled in the GraphQL schema, but rather use [GraphQL's error extensions](https://www.apollographql.com/docs/apollo-server/data/errors/).
If the query returns the value `null`, the entity was not found (equivalent to an HTTP 404 in a REST API).
The list of error extensions that can be returned by queries:
* `GRAPHQL_PARSE_FAILED`: The GraphQL operation string contains a syntax error. The request should not be retried.
* `GRAPHQL_VALIDATION_FAILED`: The GraphQL operation is not valid against the schema. The request should not be retried.
* `BAD_USER_INPUT`: The GraphQL operation includes an invalid value for a field argument. The request should not be retried.
* `UNAUTHENTICATED`: The API key is invalid. The request should not be retried.
* `FORBIDDEN`: The API key is unauthorized to access the entity being queried. The request should not be retried.
* `INTERNAL_SERVER_ERROR`: An internal error occurred. The request should be retried. If this error persists, please get in touch at [help@plain.com](mailto:help@plain.com) and report the issue.
## Mutation errors
All mutations return with an `Output` type that follow a consistent pattern of having two optional fields,
one for the result and one for the error. If the error is returned then the mutation failed.
```tsx theme={null}
type Example {
data: String!
}
type ExampleOutput {
# example is the result of the mutation, is only returned if the mutation succeeded
example: Example
# if error is returned then the mutation failed
error: MutationError
}
```
Every `MutationError` has the following fields (assuming you included all these fields in your query):
* **message:** written for a developer, not for whoever is using your product.
* **type:** one of `VALIDATION`, `FORBIDDEN`, `INTERNAL`.
* Where `VALIDATION` means input validation failed. See the fields for details on why the input was invalid.
* Where `FORBIDDEN` means the user is not authorized to do this mutation. See `message` for details on which permissions are missing.
* Where `INTERNAL` means an unknown internal server error occurred. Retry in this scenario and contact [help@plain.com](mailto:help@plain.com) if the error persists.
* **code:** a unique error code for each type of error returned. This code can be used to provide a localized or user-friendly error message. You can find the [list of error codes](/docs/graphql/error-codes) documented.
* **fields:** an array containing all the fields that errored
* **field:** the name of the input field the error is for.
* **message:** an English technical description of the error, written for a developer rather than for whoever is using your product.
* **type:** one of `VALIDATION`, `REQUIRED`, `NOT_FOUND`.
* Where `VALIDATION` means the field was provided, but didn't pass the requirements of the field. See the `message` on the field for details on why.
* Where `REQUIRED` means the field is required. String inputs may be trimmed and checked for emptiness.
* Where `NOT_FOUND` means the input field referenced an entity that wasn't found. For example, you tried to resolve an issue that doesn't exist/was deleted.
# Events
Source: https://www.plain.com/docs/graphql/events
Log important events to have the full picture of what happened in Plain.
When helping a customer it can be useful to have context about their recent activity in your product. For example, if someone is getting in touch about a 401 error, it could be important to know that they recently deleted an API key in their settings.
Events are created via the Plain API and you have full control of what they look like using Plain's UI components.
There are two types of events
* **[Customer events](/docs/graphql/events/create-customer-event)**: these are created in every existing thread for a customer. When a new thread is created (e.g. by an inbound communication, or by calling the [createThread](/docs/graphql/threads/create) endpoint) the **25** most recent events are shown.
* **[Thread events](/docs/graphql/events/create-thread-event)**: these events belong to a single thread, and only appear in a single thread's timeline.
## UI components
To define what each event should look like, you use the Plain UI components. All the components are documented in the [Plain UI Components](/docs/ui-components/) section.
### Playground
The UI Components Playground lets you build and preview the component JSON used to create an event. Use this to prototype an event before starting to build your integration.
[**UI Components Playground →**](https://app.plain.com/developer/ui-components-playground/)
# Create a customer event
Source: https://www.plain.com/docs/graphql/events/create-customer-event
Add a custom event to every thread belonging to a customer.
Customer events let you record something that happened in your own product against a customer, so it appears in the timeline of every thread that customer has.
A customer event will be created in all threads that belong to the provided customer ID. If you
want an event to appear in a specific thread use a [thread
event](/docs/graphql/events/create-thread-event).
To create an event you need a customer ID.
You can get this by [upserting a customer](/docs/graphql/customers/upsert) in Plain, from data in webhooks or other API calls you made. If you want to test this, press ⌘ + K on any thread and then "Copy customer ID" to get an ID you can experiment with.
In this example we'll be creating the following event:
For this you'll need an API key with the following permissions:
* `customerEvent:create`
# Create a thread event
Source: https://www.plain.com/docs/graphql/events/create-thread-event
Add a custom event to a single thread's timeline.
Thread events let you record something that happened in your own product against one specific thread, so it appears in that thread's timeline only.
A thread event will only be created in the thread ID provided. If you want an event to appear in
all threads for a customer please use a [customer
event](/docs/graphql/events/create-customer-event).
To create a thread event you need a thread ID.
You can get this by [creating a thread](/docs/graphql/threads/create) in Plain, from data in webhooks or other API calls you made. If you want to test this, press ⌘ + K on any thread and then "Copy thread ID" to get an ID you can experiment with.
In this example we'll be creating the following event:
For this you'll need an API key with the following permissions:
* `threadEvent:create`
* `threadEvent:read`
# Help Center
Source: https://www.plain.com/docs/graphql/help-center
Publish and manage Help Center articles, groups, and navigation through the API.
[Plain's Help Center](/docs/product/help-center) lets you publish self-serve articles for your customers, hosted either on a Plain subdomain or on your own custom domain.
The Help Center API covers managing the *content* of a Help Center programmatically:
* **Articles**: individual help pages with HTML content
* **Article groups**: folders that organize articles in the navigation
* **The navigation index**: the ordered tree of groups and articles that drives the Help Center sidebar
Use the API when you want to keep articles in sync with a docs-as-code workflow in your own git repo, or to bulk-author content from another source.
Help centers themselves (name, subdomain, branding, access, custom domains) are created and configured in the Plain app under **Settings → Help Center**. The API only covers managing content within an existing Help Center.
These operations require the following permissions:
* `helpCenter:edit`
The read queries require:
* `helpCenter:read`
# Access & authentication
Source: https://www.plain.com/docs/graphql/help-center/authentication
Control who can read your Help Center, from fully public to signed-in customers only.
Plain gives you full control over who can access your Help Center. You can configure this under **Help Center → General.**
## Access levels
### Everyone
Anyone with the URL can access your Help Center, no login required.
* Best for public knowledge bases
* Search engines can index your content
### Authenticated customers
Visitors must log in using their email address before accessing your Help Center. Login is via **magic links**, so there's no need for a password.
* Anyone with an email address can authenticate, including people who are not yet users of your product
* Once logged in, customers can create and track threads without a second sign-in
* Their email is visible in Plain, so you can attribute usage and AI conversations to them
To restrict access further, gate the Help Center by **tier**, **tenant**, **company**, or specific **customers**. Rules are OR'd: a visitor who matches any one of them gets in. Adding a tier rule and a company rule widens access rather than narrowing it.
An example use-case could be that you only want to provide access to your Help Center to Premium customers. To do this, you can select your "Premium" option in the "Tiers" select menu:
### Your team
Only members of your Plain workspace can access the Help Center. Ideal for internal knowledge bases or documentation you're not ready to publish publicly.
A visitor who authenticates but matches no access rule sees an "Access restricted" page. It reads: *"This Help Center is either internal or you do not have the necessary permissions to access it."* They can sign out and try another account.
## Plain AI access
Independently of your human access settings, you can control whether Plain AI features (Ari, Suggested responses, and Sidekick) can read your Help Center articles.
* This is useful if your Help Center is set to "Your team only" but you still want AI features to reference your articles when responding to customers
* Enable this under **Help Center → General → AI access**
# Fetch help centers and articles
Source: https://www.plain.com/docs/graphql/help-center/get
Fetch help centers, and fetch articles by slug or ID.
You can fetch help centers as a collection or by ID, and fetch an individual article by either its ID or its slug.
## List help centers
## Get a Help Center by ID
## Get a Help Center article by ID
## Get a Help Center article by slug
`slug` is unique within a Help Center.
# Manage article groups
Source: https://www.plain.com/docs/graphql/help-center/manage-article-groups
Create, update, and delete the nested groups that organize Help Center articles.
Article groups are folders that organize articles in the Help Center navigation. Groups can be nested by setting `parentHelpCenterArticleGroupId` when creating a sub-group.
## Create an article group
## Update an article group
## Delete an article group
Deleting an article group leaves its articles intact. They are left ungrouped. To remove an article entirely use [`deleteHelpCenterArticle`](./manage-articles).
# Manage articles
Source: https://www.plain.com/docs/graphql/help-center/manage-articles
Create, update, publish, and delete individual Help Center articles.
Articles are the individual help pages in a Help Center. Each article belongs to one Help Center and optionally to one article group.
`contentHtml` is the article body as HTML. Plain renders this directly in the Help Center. `status` is either `DRAFT` (only visible in the Plain app) or `PUBLISHED` (visible on the Help Center).
## Create or update an article
`upsertHelpCenterArticle` creates a new article when no `helpCenterArticleId` is provided, or updates an existing article in place when one is. `slug` is normalized to lowercase.
## Delete an article
# Manage the navigation
Source: https://www.plain.com/docs/graphql/help-center/navigation
Set the order of groups and articles in your Help Center navigation.
The Help Center index defines the order in which article groups and articles appear in the Help Center navigation, as well as the parent-child relationships between them.
`updateHelpCenterIndex` replaces the entire navigation tree in a single call. To avoid clobbering concurrent edits, you must include the `hash` returned by the previous `helpCenterIndex` query: if the Help Center has been re-indexed since you read it, the call will fail and you should re-fetch the latest index and re-apply your changes.
Each entry references an existing article or article group by ID, and optionally a parent group ID to nest it within a group.
The `type` field accepts:
* `ARTICLE`: references an existing Help Center article by its `entityId`. The displayed title is taken from the article itself.
* `ARTICLE_GROUP`: references an existing article group by its `entityId`. The displayed title is taken from the group itself.
* `HEADING`: an inline section title that groups the items beneath it in the sidebar. Headings have no backing entity, so you supply your own unique `entityId` (any opaque string, conventionally prefixed `hch_`) and a non-empty `title`. The `title` field is ignored for `ARTICLE` and `ARTICLE_GROUP` items.
# Introduction
Source: https://www.plain.com/docs/graphql/introduction
How Plain's GraphQL API is structured, and the data model behind it.
Plain itself is built on this same GraphQL API. This means that there are **no limitations** in what can be done via the API vs the UI.
These docs cover the most used operations. For anything not documented here, read the [schema](/docs/graphql/schema) or [get in touch](mailto:help@plain.com).
If you're looking to access our GraphQL API from an Agent, try our [MCP Server](/docs/integrations/mcp-server) instead.
## Key details
Our API is compatible with all common GraphQL clients with the following details:
* **API URL:** `https://core-api.uk.plain.com/graphql/v1`
* **Allowed method**: POST
* **Required headers:**
* `Content-Type: application/json`
* `Authorization: Bearer YOUR_TOKEN` where the token is your API key. See [authentication](/docs/graphql/authentication/) for more details.
* **JSON body:**
* `query`: the GraphQL query string
* `variables`: a JSON object of variables used in the GraphQL query
* `operationName`: the name of your GraphQL operation (used for tracking only; it does not affect the call or its result)
If you'd like to use the **GraphQL schema to generate types** for your client code you can fetch the schema
from: `https://core-api.uk.plain.com/graphql/v1/schema.graphql`
## Your first API call
In this example, we're going to get a customer in your workspace by their email address. You can find a customer's email on the right-hand side when looking at one of their threads in Plain.
You will need an API key with the `customer:read` permission. See [authentication](/docs/graphql/authentication/) for details on how to get an API key
You'll need to set two shell variables:
* `PLAIN_TOKEN`: The API key
* `PLAIN_CUSTOMER_EMAIL`: The email of the customer you want to fetch
```bash theme={null}
PLAIN_TOKEN=XXX
PLAIN_CUSTOMER_EMAIL=XXX
curl -X POST https://core-api.uk.plain.com/graphql/v1 \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $PLAIN_TOKEN" \
-d '{"query":"query customerByEmail($email: String!) { customerByEmail(email: $email) { id fullName updatedAt { iso8601 } } }","variables":{"email":"'"$PLAIN_CUSTOMER_EMAIL"'"},"operationName":"customerByEmail"}'
```
# Knowledge gaps
Source: https://www.plain.com/docs/graphql/knowledge-gaps
Knowledge gaps are questions customers keep asking that your knowledge sources do not answer, detected by Ari and tracked as tasks.
A knowledge gap is a question customers keep asking that your knowledge sources don't answer. When [Ari](/docs/product/agents/ari) can't answer a customer and hands the thread to your team, Plain records a signal. Related signals are grouped into one gap with an AI-generated title and description, and Plain raises a [task](/docs/graphql/tasks) for your team to fill it. For how gaps are detected, reviewed, and filled in the app, see [Knowledge gaps](/docs/product/agents/knowledge-gaps).
Knowledge gaps are in beta. The queries and fields on these pages may change without a deprecation period.
The API is read-only. Plain creates gaps; you can't create, edit, or delete one. A gap's status is its task's status. To change it, [update the task](/docs/graphql/tasks/update).
Use the API to list the gaps in your workspace, sorted by signal count, or to read the gap behind a task your team is working on. To fill a gap, write the answer into your [Help Center](/docs/graphql/help-center) or wherever Ari reads your knowledge from.
# Fetch knowledge gaps
Source: https://www.plain.com/docs/graphql/knowledge-gaps/get
List the knowledge gaps in your workspace or fetch one by ID.
There are two ways to fetch knowledge gaps:
* [Get knowledge gaps](#get-knowledge-gaps): paginated collection, filtered by status and sorted by signal count or recency
* [Get knowledge gap by ID](#get-knowledge-gap-by-id)
Knowledge gaps are in beta. The queries and fields on this page may change without a deprecation period.
These operations require the following permissions:
* `knowledgeGap:read`
* `task:read` to read the linked `task`
## Get knowledge gaps
Filter with `statuses`, which takes any of `TODO`, `DONE`, and `CANCELLED`. Sort with `sortBy`: `SIGNAL_COUNT` orders by how many customer conversations hit the gap, `LAST_SEEN_AT` by when the most recent one did. The default is `SIGNAL_COUNT` descending.
## Get knowledge gap by ID
# Labels
Source: https://www.plain.com/docs/graphql/labels
Labels categorize threads. Each one references a label type that defines its name, icon, and color.
Labels are a lightweight way to categorize threads, consisting of label text coupled with an icon. Each thread can have multiple labels.
They can be added manually or programmatically. For example when a contact form is submitted, you could automatically add a label to the corresponding thread with the issue category they selected, so that you know upfront why they are getting in touch.
The available labels you can apply are defined by your label types. Label types can be created and managed in your settings (⌘ + K and then search for "Manage labels").
When you want to stop a label being available you can archive a label type. Archived label types are kept on existing threads in order to avoid losing valuable historic data.
Label changes can also be a starting point for integrations [via our webhooks](/docs/webhooks/thread-labels-changed). This lets you build workflows triggered by the addition of a label.
# Add labels
Source: https://www.plain.com/docs/graphql/labels/add
Add one or more labels to a thread by label type ID.
You can add multiple labels to a thread with a call to `addLabels`. Label type IDs passed to this endpoint should not be archived, we return a validation error with code `cannot_add_label_using_archived_label_type` for any which are submitted.
If a label type you provide is already added to the thread we will return a validation error with code `label_with_given_type_already_added_to_thread`.
You can retrieve label type IDs in the Plain UI settings by hovering over a label type and selecting 'Copy label ID' from the overflow menu.
This operation requires the following permissions:
* `label:create`
# Manage label types
Source: https://www.plain.com/docs/graphql/labels/label-types
Create, update, archive, and fetch the label types available in your workspace.
Label types define the labels available in your workspace. Each label type has a name, icon, color, and optionally a description, parent label type and external ID.
When you want to stop a label being available, archive its label type rather than deleting it: this preserves the historical record on existing threads.
These operations require the following permissions:
* `labelType:create`
* `labelType:edit`
The read queries require:
* `labelType:read`
## Get label types
Use `labelTypes` to fetch the full list of label types in your workspace. By default this includes archived label types: pass `filters: { isArchived: false }` to exclude them.
## Get a label type by ID
## Get a label type by external ID
If you store your own identifier on label types via `externalId`, you can look them up by that value. External IDs are unique within a workspace.
## Create a label type
## Update a label type
Updates use field-level wrapper inputs: to change a field pass `{ "value": ... }`; to leave a field untouched omit it entirely.
## Archive a label type
Archived label types stay attached to existing threads but cannot be added to new threads. Attempting to call `addLabels` with an archived label type returns the error code `cannot_add_label_using_archived_label_type`.
## Unarchive a label type
# Remove labels
Source: https://www.plain.com/docs/graphql/labels/remove
Remove labels from a thread by label ID.
You can remove labels from a thread with a call to `removeLabels`. Label IDs for this call can be retrieved by fetching a thread with the API.
This operation requires the following permissions:
* `label:delete`
# Messaging
Source: https://www.plain.com/docs/graphql/messaging
Send and reply to customer messages over email and chat from the API.
We provide various methods to message your customers with the Plain API. You can use this to reach out proactively, build an autoresponder or even to handle things like waiting list access.
Send a new email in a thread ignoring previous communications.
Use this to reply to an existing inbound email in a thread.
Reply to a thread automatically using the best channel.
# Reply to emails
Source: https://www.plain.com/docs/graphql/messaging/reply-email
Reply to a specific inbound email with the replyToEmail mutation.
You can reply to an inbound email with the `replyToEmail` API.
This operation requires the following permissions:
* `email:create`
* `email:read`
# Reply to threads
Source: https://www.plain.com/docs/graphql/messaging/reply-to-thread
Reply on a thread over whichever channel it already uses.
You can reply to a thread using the `replyToThread` mutation, as long as the thread's communication channel is either `API`, `CHAT`, `EMAIL`, `SLACK` or 'MS\_TEAMS'. This information is available in the thread as the `channel` field.
If it is not possible to reply to a thread, you will get the mutation error code [`cannot_reply_to_thread`](/docs/graphql/error-codes#cannot_reply_to_thread) and a message indicating why.
This operation requires the following permissions:
* `thread:reply`
## Impersonation
Impersonation is exclusively available in our `Frontier` plan. You can see all available plans in our [pricing page](https://www.plain.com/pricing).
### Reply as a customer
This feature allows you to bring native messaging between your customers and Plain, [straight into your own product](/docs/headless-portal).
With impersonation, you can reply to a thread on behalf of one of your customers: impersonated messages will show up as if they were sent by the customers themselves.
In order to impersonate a customer, provide the `impersonation` parameter in the `replyToThread` mutation, specifying the identifier of the customer you want to impersonate. You can pick any of the available customer identifiers (`emailAddress`, `customerId` or `externalId`)
```graphql theme={null}
{
"impersonation": {
"asCustomer": {
"customerIdentifier": {
"emailAddress": "blanca@example.com"
}
}
}
}
```
Impersonation is only possible for `API`, `CHAT`, `EMAIL` and `SLACK` threads (based on the thread's `channel` field).
The customer message will be processed differently based on the thread's channel:
* `SLACK`: the message will appear in Slack as a new message from the impersonated customer, including their name and any other customer details
* `API` and `EMAIL`: the message will be sent as an email with the impersonated customer's email address as the "From" address, making it appear as if they sent the email directly
* `CHAT`: the message will appear in the thread as coming directly from the impersonated customer, with their name and avatar displayed
When replying to an `EMAIL` or `API` thread, you can optionally add 'Cc' and 'Bcc' recipients by using the `channelSpecificOptions` parameter:
```graphql theme={null}
{
"channelSpecificOptions": {
"email": {
// For CC'd recipients
"additionalRecipients": [
{
"email": "peter@example.com",
"name": "Peter"
},
],
// For BCC'd recipients
"hiddenRecipients": [
{
"email": "finance@example.com"
}
]
}
}
}
```
This operation requires the following permissions:
* `thread:reply`
* `customer:impersonate`
### Reply as a user
Replying as a user sends the message from a team member. The customer sees that team member's name instead of the machine user's. Use this when an agent or automation replies on a team member's behalf.
`impersonation` takes exactly one of `asCustomer` or `asUser`. Sending both, or neither, fails with `input_validation`. `MS_TEAMS` and `DISCORD` threads reject it with `cannot_reply_to_thread`.
Only an API key can reply as a user, and the key's [impersonation allow list](/docs/agents/discussions#reply-to-customers-as-a-team-member) must include that user. Set `impersonation.asUser.userIdentifier` to the user's `userId` or `emailAddress`:
```json theme={null}
{
"impersonation": {
"asUser": {
"userIdentifier": {
"emailAddress": "sam@example.com"
}
}
}
}
```
Each channel delivers the message differently:
* `API` and `EMAIL`: Plain sends the email from the user's public name, the same as a reply the user sends from Plain
* `CHAT`: the message appears in the thread as sent by the user, with their name and avatar
* `SLACK`: Plain posts the message and any attachments through the user's own Slack connection, as if the user sent it from Slack. If the user hasn't connected Slack in Plain, the reply fails with `missing_user_auth_slack_integration_for_team`
In the thread's timeline in Plain, the message shows the team member as its author, with a tag beside their name naming the machine user that sent it. In the API, the entry's `actor` is the team member and its `createdBy` is the machine user.
The reply fails with one of these errors:
* `cannot_reply_to_thread`: the user isn't on the API key's allow list, the user's role can't reply to threads, or the caller isn't a machine user with an API key
* `not_found`: the user doesn't exist or has been removed from the workspace
* `input_validation`: the identifier names a machine user, or `impersonation` sets both targets or neither
This operation requires the following permissions:
* `thread:reply`
# Send new emails
Source: https://www.plain.com/docs/graphql/messaging/send-email
Start a new outbound email to a customer, creating a thread for it.
As well as creating outbound emails in the UI you can also send them with the `sendNewEmail` API. This is useful for proactively reaching out about issues.
# Suggested replies
Source: https://www.plain.com/docs/graphql/messaging/suggested-replies
Add a suggested reply to a thread for a user to review, edit, and send.
Suggested replies (also called generated replies) allow you to programmatically add AI-generated or pre-composed reply suggestions to a thread. These suggestions are shown to the user in Plain so they can review and send them to the customer.
This is useful if you are building your own AI integration or want to suggest replies based on your own logic, external knowledge bases, or automation workflows.
## Adding a suggested reply
To add a suggested reply you need to provide the `threadId`, the `timelineEntryId` of the message you are responding to, and the `markdown` content of the reply.
The timeline entry must be a message, from either the customer or a machine user. Use the most recent inbound message on the thread. Notes, user replies and timeline events such as label or status changes are not valid targets.
A `timelineEntryId` identifies a single entry on a customer's timeline (e.g. an email or chat message). You can get one by querying a thread's `timelineEntries` connection, where each entry has an `id` you can use here.
The `markdown` field supports a maximum of 5,000 characters.
Your replies show even when suggested responses are turned off, and take precedence over Ari's for the same message.
To add a suggested reply, you need an API key with the following permissions:
* `generatedReply:create`
### Threads created through the API
A thread that your integration opens with `createThread` has its first message authored by your machine user, not by the customer. That message is a valid target, so an agent can suggest a reply on a thread it opened itself. Do not filter the timeline down to customer entries before you choose a `timelineEntryId`, or you will find no target on these threads.
# Notes
Source: https://www.plain.com/docs/graphql/notes
Notes are internal comments on a thread. They appear in the timeline but never reach the customer.
Notes are internal comments that show up in the thread timeline alongside customer messages but are only visible to your team. They're useful for context: like a heads-up from another user, a reminder, or annotations from an automation.
Notes can be attached either to a customer (visible across all of that customer's threads) or specifically to a thread (visible only in that thread's timeline).
These operations require the following permissions:
* `note:create`
* `note:edit`
* `note:delete`
## Create a note
`text` is the plain-text body. If you also pass `markdown`, that version is preferred where rich text is supported.
## Update a note
## Delete a note
# Pagination
Source: https://www.plain.com/docs/graphql/pagination
Every collection in the API follows the Relay cursor pagination spec.
Our GraphQL API follows the [Relay pagination spec](https://relay.dev/graphql/connections.htm).
When fetching collections from our API you can control how much data is returned. We will return 25 records per request by default and the maximum page size is 100 records.
We support two forms of page control arguments:
1. Forward pagination with `after` (cursor) & `first` (numeric count)
2. Reverse pagination with `before` (cursor) & `last` (numeric count)
Note that these must not be mixed, e.g performing a query with values for first & before will result in a validation error.
Endpoints which return paginated results will return a `pageInfo` object along with a `totalCount` field which allows you to make subsequent calls with page controls. Using the `getCustomers` API as an example this would look as follows:
This will fetch a subsequent page of 50 entries by passing in the `endCursor` from an initial query.
# Schema
Source: https://www.plain.com/docs/graphql/schema
Download the GraphQL schema for code generation, or read it in the API explorer.
If you need the schema programmatically for code generation or want to read it directly, view the [raw GraphQL schema](https://core-api.uk.plain.com/graphql/v1/schema.graphql).
You can also use the [API Explorer](https://app.plain.com/developer/api-explorer/) to learn about our API schema. That is the fastest way to see everything the GraphQL API can do.
[**View API Explorer →**](https://app.plain.com/developer/api-explorer/)
# GraphQL SDK
Source: https://www.plain.com/docs/graphql/sdk
A typed SDK for Plain's GraphQL API, auto-generated from the schema.
The `@team-plain/graphql` package provides a fully typed client for Plain's GraphQL API. It is auto-generated from the [GraphQL schema](/docs/graphql/schema), which means every query and mutation available in the API is available in the SDK.
You can use any GraphQL client to interact with Plain's API, but this SDK adds type safety, automatic pagination, and structured error handling.
## Installation
```bash theme={null}
npm install @team-plain/graphql
```
Supports both ESM and CJS.
## Setup
```ts theme={null}
import { PlainClient } from "@team-plain/graphql";
const client = new PlainClient({ apiKey: "plainApiKey_xxx" });
```
You will need an API key. See [authentication](/docs/graphql/authentication) for how to create one.
## Queries
Queries are available under `client.query`. Relations on returned models are lazy-loaded: accessing them triggers a separate API call automatically.
```ts theme={null}
const customer = await client.query.customer({ customerId: "c_123" });
console.log(customer.fullName);
// Relations are lazy-loaded — accessing them makes a separate API call
const company = await customer.company;
console.log(company.name);
```
## Mutations
Mutations are available under `client.mutation`. Mutation errors are returned as typed data, not thrown as exceptions. This matches Plain's API where all mutations return `*Output` types with an optional `error` field.
```ts theme={null}
const result = await client.mutation.upsertCustomer({
input: {
identifier: { emailAddress: "alice@example.com" },
onCreate: {
fullName: "Alice",
email: { email: "alice@example.com", isVerified: false },
},
onUpdate: {},
},
});
if (result.error) {
// Typed MutationError with message, type, code, and field-level errors
console.error(result.error.message);
result.error.fields?.forEach((f) => {
console.error(` ${f.field}: ${f.message}`);
});
} else {
console.log(result.customer?.id);
}
```
## Pagination
```ts theme={null}
const customers = await client.query.customers({ first: 10 });
for (const customer of customers.nodes) {
console.log(customer.fullName);
}
// Fetch the next page
const nextPage = await customers.fetchNext();
```
## Union types
GraphQL union and interface fields are exposed as discriminated unions of model classes. Each union member has a `__typename` property for type narrowing and supports the same lazy-loading as any other model.
```ts theme={null}
const thread = await client.query.thread({ threadId: "t_123" });
// Narrow with __typename
if (thread.createdBy.__typename === "UserActor") {
console.log(thread.createdBy.userId);
// Lazy-load a relation on the union member
const user = await thread.createdBy.user;
console.log(user?.fullName);
}
// Or narrow with instanceof
import { UserActorModel } from "@team-plain/graphql";
if (thread.createdBy instanceof UserActorModel) {
const user = await thread.createdBy.user;
}
```
Models also support querying sub-connections directly:
```ts theme={null}
const thread = await client.query.thread({ threadId: "t_123" });
// Fetch timeline entries directly from the thread model
const timelineEntries = await thread.timelineEntries({ first: 25 });
for (const entry of timelineEntries.nodes) {
console.log(entry.entry.__typename);
}
```
## Error handling
* **Queries**: network, auth (401), forbidden (403), and rate limit (429) errors throw typed exceptions (`AuthenticationError`, `ForbiddenError`, `RateLimitError`, `NetworkError`, `PlainGraphQLError`).
* **Mutations**: return the full `*Output` type. Check `result.error` for a typed `MutationError` with `message`, `type`, `code`, and `fields[]`. This is intentional: Plain's API treats mutation errors as data.
For more details on error handling patterns, see [error handling](/docs/graphql/error-handling).
## Migrating from `@team-plain/typescript-sdk`
If you're upgrading from the old `@team-plain/typescript-sdk` package, see the [migration guide](https://github.com/team-plain/sdk/blob/main/packages/graphql/MIGRATION.md) for a full breakdown of breaking changes.
## Resources
* [GraphQL schema](https://core-api.uk.plain.com/graphql/v1/schema.graphql): the full schema this SDK is generated from
* [API explorer](https://app.plain.com/developer/api-explorer/): browse and test queries interactively
* [GitHub repository](https://github.com/team-plain/sdk/tree/main/packages/graphql)
# SLA policies
Source: https://www.plain.com/docs/graphql/sla-policies
SLA policies are named sets of SLA targets that you create once and apply to threads with a workflow or the API.
A Service Level Agreement (SLA) policy is a named set of SLA targets, such as a first response time and a next response time. A thread starts tracking those targets when the policy is applied to it, and a thread with no policy has no SLA. For how policies work in the app, see [SLA policies](/docs/product/platform/slas).
SLA policies replace the legacy SLAs configured on tiers, which are being deprecated. Workspaces that still use tier SLAs manage them with the [tier SLA operations](/docs/graphql/tiers/service-level-agreements).
Use the API to create and manage policies, list the policies in your workspace, and apply a policy to a thread. In the app, a [workflow](/docs/product/workflows) applies the policy. The API lets you do the same from your own systems, for example when your integration creates a thread.
Each target has a type, a time in minutes, a warning period, and optionally the business hours schedules it counts against. A policy holds at most one target of each type.
# Apply SLA policies
Source: https://www.plain.com/docs/graphql/sla-policies/apply-to-thread
Set or remove the SLA policy on a thread.
Applying a policy is what starts the clock. The policy's targets become the thread's SLAs. Pass `serviceLevelAgreementPolicyId: null` to remove the policy, which cancels any target still counting down.
If your workspace still has legacy tier SLAs, a policy takes precedence over them on the thread, and removing the policy returns the thread to its tier's SLAs.
This operation requires the following permissions:
* `thread:edit`
## Update the SLA policy on a thread
The returned thread's `serviceLevelAgreementStatusSummary` shows the state of each target after the change. For what happens to targets when a thread moves between policies, see [Change or remove a policy on a thread](/docs/product/platform/slas#change-or-remove-a-policy-on-a-thread).
# Get SLA policies
Source: https://www.plain.com/docs/graphql/sla-policies/get
List the SLA policies in your workspace or fetch one by ID.
There are two ways to fetch SLA policies:
* [Get SLA policies](#get-sla-policies): paginated collection, filtered by name or target type
* [Get SLA policy by ID](#get-sla-policy-by-id)
These operations require the following permissions:
* `serviceLevelAgreement:read`
## Get SLA policies
Policies are sorted by name. Narrow the list with `filters`: `searchQuery` matches part of the name, and `serviceLevelAgreementTypes` keeps only policies that hold a target of at least one of the given types. This endpoint supports [Pagination](/docs/graphql/pagination).
## Get SLA policy by ID
Returns `null` when no policy has the ID.
# Manage SLA policies
Source: https://www.plain.com/docs/graphql/sla-policies/manage
Create, update, and delete SLA policies and their targets.
A policy is created with its targets in one call, and updated by passing the full set of targets it should have afterwards. For what each field means, see [What a policy contains](/docs/product/platform/slas#what-a-policy-contains).
These operations require the following permissions:
* `serviceLevelAgreement:create`
* `serviceLevelAgreement:edit`
* `serviceLevelAgreement:delete`
## Create an SLA policy
Each target needs a `type`, a `minutes` value, and a `warnBefore` period that is shorter than the target. Pass `businessHoursScheduleIds` for a target to count only while one of those schedules is open. Leave it out or pass an empty array for the target to count around the clock. A policy only starts tracking once it's applied to a thread.
## Update an SLA policy
`targets` is the full set the policy should have after the update. A target whose type already exists is updated in place, so threads tracking it keep their deadlines. A new type is added, and a type you leave out is removed along with its trackers. Leave `businessHoursScheduleIds` out of a target for it to keep the schedules it already has.
Field-level wrapper inputs apply to `name` and `description`: pass `{ "value": ... }` for the fields you want to change and omit the rest.
## Delete an SLA policy
Deleting a policy deletes its targets and their trackers, and threads it was applied to stop tracking SLAs. If your workspace still has legacy tier SLAs, those threads return to their tier's SLAs.
# Snippets
Source: https://www.plain.com/docs/graphql/snippets
Snippets are reusable pieces of text that users insert into a reply with a few keystrokes.
Snippets are reusable pieces of text, also known as canned responses, that users can drop into a reply with a few keystrokes. They're useful for boilerplate openers, common troubleshooting steps, or any phrasing your team repeats.
Each snippet has a name (used to search and identify it), a plain-text body, and an optional markdown body that's preferred when sending into rich-text channels. Snippets can also be grouped under a `path` (an alphanumeric string). This is what's used to organize snippets in folders in the Plain app.
You can create, update and delete snippets in the Plain app, but for teams that maintain a shared library of canned responses in another system, the API is useful for keeping them in sync.
# Create a snippet
Source: https://www.plain.com/docs/graphql/snippets/create
Create a snippet, with a name to find it by and optional dynamic variables.
Snippet `name` is used to find the snippet when inserting it during a reply. The `text` field is the plain-text body that will be used in environments that don't render markdown. If you also pass `markdown`, that version is preferred wherever rich text is supported.
The optional `path` groups snippets in the Plain app. Only alphanumeric characters are allowed.
This operation requires the following permissions:
* `snippet:create`
# Delete a snippet
Source: https://www.plain.com/docs/graphql/snippets/delete
Soft-delete a snippet, hiding it from the picker while keeping reply history intact.
Deleted snippets are soft-deleted. They're hidden from the snippet picker but remain queryable by ID with `isDeleted: true`. This preserves the history of any replies that referenced the snippet.
This operation requires the following permissions:
* `snippet:delete`
# Fetch snippets
Source: https://www.plain.com/docs/graphql/snippets/get
Fetch snippets by ID or as a paginated collection.
You can fetch snippets either as a paginated collection or by ID.
These operations require the following permissions:
* `snippet:read`
## Get snippets
## Get a snippet by ID
# Update a snippet
Source: https://www.plain.com/docs/graphql/snippets/update
Change a snippet's name, text, or group using field-level wrapper inputs.
Updates use field-level wrapper inputs: to change a field pass `{ "value": ... }`; to leave a field untouched omit it entirely. To clear the `path` (un-group a snippet) pass `{ "value": null }`.
This operation requires the following permissions:
* `snippet:edit`
# Tasks
Source: https://www.plain.com/docs/graphql/tasks
Tasks are internal reminders and follow-ups, tracked alongside threads rather than on them.
Tasks are reminders or follow-ups that your team needs to action. Each task has a title, description, status, priority and can be assigned to a user or a machine user. Tasks can also be linked to a company or a tenant to give them context.
Tasks live alongside threads in the Plain app but are not threads themselves. They are a lighter-weight to-do, useful for things like "send onboarding follow-up next Friday" or "check in on this customer after their renewal".
Use the API when you want tasks to be created or updated as a side-effect of something happening in your own systems (for example, automatically creating a follow-up task when a deal closes in your CRM).
Plain also raises tasks itself. When [Ari](/docs/product/agents/ari) can't answer a customer from your knowledge sources, it records a [knowledge gap](/docs/graphql/knowledge-gaps) and creates a task to fill it. Read the gap from the task's `sourceLinks`; see [Get the knowledge gap behind a task](/docs/graphql/tasks/get#get-the-knowledge-gap-behind-a-task).
Subscribe to [task webhook events](/docs/webhooks/task-created) to react to tasks created, updated, or deleted in Plain, including tasks raised for knowledge gaps.
# Create a task
Source: https://www.plain.com/docs/graphql/tasks/create
Create a task with a title, and optionally a description, priority, assignee, and due date.
`title` is the only required input. You can optionally set a description, status, priority, assignee (a user or a machine user) and a parent company or tenant.
A task can be linked to either a company or a tenant, not both.
This operation requires the following permissions:
* `task:create`
# Delete a task
Source: https://www.plain.com/docs/graphql/tasks/delete
Soft-delete a task, removing it from the active list but keeping it queryable by ID.
Deleting a task soft-deletes it. It is removed from the active task list but remains queryable by ID with `isDeleted: true`.
This operation requires the following permissions:
* `task:delete`
# Fetch tasks
Source: https://www.plain.com/docs/graphql/tasks/get
Fetch tasks by ID, by their short reference, or as a paginated collection.
There are three ways to fetch tasks:
* [Get tasks](#get-tasks): paginated collection
* [Get task by ID](#get-task-by-id)
* [Get task by ref](#get-task-by-ref). `ref` is the short human-readable identifier shown in the Plain app (e.g. `T-123`)
These operations require the following permissions:
* `task:read`
## Get tasks
## Get task by ID
## Get task by ref
## Get the knowledge gap behind a task
When [Ari](/docs/product/agents/ari) raises a task for a [knowledge gap](/docs/graphql/knowledge-gaps), the task's `sourceLinks` holds one entry with `sourceType: "knowledge_gap"`, and on that entry `knowledgeGap` carries the gap's title and description. The task's `status` is the gap's status; to change it, [update the task](/docs/graphql/tasks/update).
Knowledge gaps are in beta. `sourceLinks` and the `KnowledgeGapTaskLink` type may change without a deprecation period.
Reading `knowledgeGap` also requires the `knowledgeGap:read` permission. A task you created yourself has an empty `sourceLinks`.
# Update a task
Source: https://www.plain.com/docs/graphql/tasks/update
Change individual fields on a task, including who and what it is linked to.
Pass only the fields you want to change. Setting `companyId` clears any existing `tenantId` on the task and vice versa. A task can be linked to either, but not both.
This operation requires the following permissions:
* `task:edit`
# Tenants
Source: https://www.plain.com/docs/graphql/tenants
Tenants mirror how your product groups users, so threads can be scoped to a whole account.
Tenants allow you to structure your customers in Plain in the same way as they are structured in your product.
For example if within your product customers are organized in a 'team' then you would create one tenant per team in your product. A tenant has an `externalId` so that you can map it back to an entity in your database.
Customers can belong to multiple tenants.
For advanced integrations with Plain you can specify a tenant when creating a thread. This is useful when building a support portal in your product as it allows you to fetch threads specific to a team in your product.
Additionally [tiers and SLAs](/docs/product/platform/tiers) can be associated with a tenant.
# Add customers to tenants
Source: https://www.plain.com/docs/graphql/tenants/add-customers
Add a customer to one or more tenants in a single call.
You can add a customer to multiple tenants.
When selecting the customer you can chose how to identify them. You can use the customer's email, externalId or id.
This operation requires the following permissions:
* `customer:edit`
* `customerTenantMembership:create`
# Delete a tenant
Source: https://www.plain.com/docs/graphql/tenants/delete
Delete a tenant, unlinking its customers and removing its tenant fields.
Deleting a tenant unlinks it from all of its customers and removes any associated tenant fields. Threads previously linked to the tenant retain a reference to the deletion record but are no longer routed via it.
A tenant is identified by either its Plain `tenantId` or its `externalId`.
This operation requires the following permissions:
* `tenant:delete`
# Get tenants
Source: https://www.plain.com/docs/graphql/tenants/get
Fetch tenants by ID or external ID, or as a paginated collection.
There are three ways to fetch tenants:
* [Get tenants](#get-tenants) to fetch more than one tenant at a time.
* [Get tenant by ID](#get-tenant-by-id)
* [Search for tenants](#search-for-tenants)
These operations require the following permissions:
* `tenant:read`
## Get tenants
Our API allows you to fetch tenants as a collection using the `tenants` query in GraphQL. This endpoint supports [Pagination](/docs/graphql/pagination).
## Get tenant by ID
If you know the tenant's ID in Plain you can use this method to fetch the tenant. Prefer [upsert](./upsert) when you have the full details of the tenant.
## Search for tenants
The `searchTenants` query lets you do a case-insensitive partial match on a tenant's name as well as an exact match on its external ID. The search term must be at least 2 characters long.
# Remove customers from tenants
Source: https://www.plain.com/docs/graphql/tenants/remove-customers
Remove a customer from one or more tenants in a single call.
You can remove customers from multiple tenants in one API call.
When selecting the customer you can chose how to identify them. You can use the customer's email, externalId or id.
This operation requires the following permissions:
* `customer:edit`
* `customerTenantMembership:delete`
# Set customer tenants
Source: https://www.plain.com/docs/graphql/tenants/set-customer-tenants
Replace all of a customer's tenants at once, rather than adding or removing individually.
You can also set all tenants for a customer. Unlike the more specific add or remove mutations this is useful if you are sycing tenants and customers with Plain.
This operation requires the following permissions:
* `customer:edit`
* `customerTenantMembership:create`
* `customerTenantMembership:delete`
# Tenant fields
Source: https://www.plain.com/docs/graphql/tenants/tenant-fields
Attach custom fields to a tenant, such as plan, MRR, or account owner.
Tenant fields are custom fields you can attach to a tenant. For example a plan name, MRR, account owner or any other attribute that is meaningful at the tenant level rather than per-customer.
Working with tenant fields involves two layers:
1. **Tenant field schemas** define what fields exist (type, label, options). You create the schema once.
2. **Tenant field values** are the per-tenant values stored against a given schema.
This is the same model as thread fields: see [thread fields](/docs/graphql/threads/thread-fields) for the equivalent on threads.
These operations require the following permissions:
* `tenant:edit`
* `tenantFieldSchema:create` / `tenantFieldSchema:edit` / `tenantFieldSchema:delete` (for schema operations)
## Create or update tenant field schemas
`upsertTenantFieldSchema` accepts an array of schemas to create or update in one call. Each schema is identified by the combination of `source` and `externalFieldId`.
Supported field types are `STRING_TYPE`, `NUMBER_TYPE`, `BOOLEAN_TYPE`, `STRING_ARRAY`, `DATETIME_TYPE` and `ENUM_TYPE` (use `options` to define the allowed values).
## Delete a tenant field schema
Deleting a schema also removes any tenant field values stored against it.
## Set a tenant field value
Once a schema exists, use `upsertTenantField` to set or update the value for a specific tenant. Pass exactly one of `stringValue`, `numberValue`, `booleanValue`, `arrayValue` or `dateValue` matching the schema's `type`.
## Delete a tenant field value
To clear a tenant's value for a specific field without removing the schema itself, call `deleteTenantField`.
# Upserting tenants
Source: https://www.plain.com/docs/graphql/tenants/upsert
Create or update a tenant, keyed on the external ID it has in your own product.
When upserting a tenant you need to specify an `externalId` which matches the id of the tenant in your own backend.
For example if your product is structured in teams, then when creating a tenant for a team you'd use the team's id as the `externalId`.
To upsert a tenant you need the following permissions:
* `tenant:read`
* `tenant:create`
# Threads
Source: https://www.plain.com/docs/graphql/threads
Threads are the core of Plain's data model. Every customer conversation becomes one.
Threads are the core of Plain's data model and equivalent to tickets or conversations in other support platforms. When you use Plain to help a customer you assign yourself to a thread and then mark the thread as `Done` once you're done helping.
Threads are automatically created when a new email is received but can also be [created via the API](/docs/graphql/threads/create) (when a customer submits a contact form for example). If you're migrating from another support provider, you can [import historic threads](/docs/graphql/threads/import) with their original timestamps and conversation history.
Threads have [a status](/docs/product/platform/threads/statuses) and can be assigned to multiple users.
Threads belong to one customer but can contain multiple email threads and customers.
An example thread looks like this:
The below is only showing a subset fields a thread has. Since our API is a GraphQL API you decide
which fields you need when you make API requests. Use our [API
explorer](https://app.plain.com/developer/api-explorer) to discover the full schema of threads.
# Assignment
Source: https://www.plain.com/docs/graphql/threads/assignment
Assign a thread to a user or a machine user, add co-assignees, or unassign it.
Threads can be assigned to users or machine users. The latter is useful if you want a bot to handle or are building a complex automation of some kind.
## Assigning a thread
To assign threads you need an API key with the following permissions:
* `thread:assign`
* `thread:read`
## Unassigning threads
To unassign threads you need an API key with the following permissions:
* `thread:unassign`
* `thread:read`
## Additional assignees
In addition to the primary assignee, threads can have **additional assignees**. Users who are also looped in on the thread but who aren't the main person responsible for it. This is useful for collaborative cases or when escalating to a specialist.
You can add or remove either users or machine users in a single call.
### Add additional assignees
### Remove additional assignees
# Autoresponders
Source: https://www.plain.com/docs/graphql/threads/autoresponders
Build an autoresponder that decides what to send, rather than using a fixed workspace reply.
Plain has native [workspace level auto-responses](/docs/product/platform/auto-responses). For cases those don't cover, implement your own custom autoresponder.
To do this, set up endpoint(s) to be notified of one or more [webhooks](/docs/webhooks) from Plain. Listen for the [thread created](/docs/webhooks/thread-created) webhook, which lets you respond to any thread whether it was created via email, Slack or a contact form.
If you want to only reply to emails, you can use the [email received](/docs/webhooks/thread-email-received) webhook. This triggers for every email, not only the first one in a thread, so check the `isStartOfThread` field provided in the webhook payload to ensure you only reply to the first message.
Note that if you subscribe to both `thread.thread_created` and `thread.email_received` you may
receive two events for the same email, since we create a new thread for emails which don't belong
to an existing thread. In order to avoid replying to the same message twice please check the
`isStartOfThread` field in the `thread.email_received` payload.
Once you have received an event and decided how to respond you can use the `replyToThread` mutation to send a reply back to the customer. See our [API explorer](https://app.plain.com/developer/api-explorer/) for more details.
# Create threads
Source: https://www.plain.com/docs/graphql/threads/create
Create a thread from the API, for contact forms or proactive outreach.
Creating a thread is useful in scenarios where you want to programmatically start a support interaction.
The two most common cases are a submitted contact form, and proactive outreach after an event or error in your product.
A thread is created with an initial 'message' composed out of [UI components](/docs/ui-components). You have full control over the structure and appearance of the message in Plain.
To create a thread you need a `customerId`. You can get a customer id by [creating the customer](/docs/graphql/customers/upsert) in Plain first.
If you're migrating historic threads from another support provider, use [`importThread`](/docs/graphql/threads/import) instead. It preserves original timestamps and won't trigger SLAs or autoresponders.
To create a thread, you need an API key with the following permissions:
* `thread:create`
* `thread:read`
# Delete a thread
Source: https://www.plain.com/docs/graphql/threads/delete
Permanently delete a thread and all its associated data from Plain.
Permanently delete a thread and all its associated data from Plain. This action is irreversible.
This operation requires the following permissions:
* `thread:delete`
# Escalating threads
Source: https://www.plain.com/docs/graphql/threads/escalation
Move a thread up an escalation path, or set which path it follows.
An [escalation path](/docs/product/platform/escalation-paths) is a configured sequence of users or label-type owners that a thread escalates through when no-one is responding in time. Escalation paths themselves are configured in the Plain app under **Settings → Escalation paths**.
The API lets you trigger an escalation programmatically and change which escalation path a thread is on.
These operations require the following permissions:
* `thread:edit`
## Escalate a thread
`escalateThread` advances the thread to the next step in its current escalation path. If the thread doesn't have an escalation path attached, the call returns an error.
## Change the escalation path
Attach a thread to a specific escalation path (or pass `escalationPathId: null` to detach it).
# Fetch threads
Source: https://www.plain.com/docs/graphql/threads/get
Fetch threads by ID or external ID, list them with filters, or search their contents.
There are four ways to fetch threads:
* [Get a thread by ID](#get-a-thread-by-id)
* [Get a thread by external ID](#get-a-thread-by-external-id)
* [List threads](#list-threads): paginated, with filters
* [Search threads](#search-threads): full-text search on title, description and message contents
These operations require the following permissions:
* `thread:read`
## Get a thread by ID
## Get a thread by external ID
A thread's `externalId` is unique within a customer, which is why the customer ID is required when looking up by external ID.
## List threads
The `threads` query supports filtering on status, status detail, assignee, customer, labels, priority, date ranges) and sorting. This is the right query for building inbox-style views.
## Search threads
`searchThreads` performs a full-text search across thread title, description and message contents. For exact lookups prefer [`thread`](#get-a-thread-by-id) or [`threadByExternalId`](#get-a-thread-by-external-id).
# Importing threads
Source: https://www.plain.com/docs/graphql/threads/import
Import historical threads and their messages, preserving original timestamps.
Plain has [built-in importers](https://help.plain.com/article/migration) for common providers. If your source system is supported, use those first. They handle the mapping for you. These mutations are for when you need to build a custom import, for example from a less common provider or an internal tool.
The `importThread` and `importThreadMessages` mutations let you bring across historic threads while preserving original timestamps, authors, and attachments so your team has full context in Plain.
Unlike [`createThread`](/docs/graphql/threads/create), imported threads do not trigger SLAs or autoresponders and are marked with import provenance tracking.
## Overview
Importing a thread is a two-step process:
1. **Create the thread** with `importThread`: this sets up the thread with its metadata (title, status, priority, labels, etc.) and the original creation timestamp.
2. **Add messages** with `importThreadMessages`: this adds the conversation history (inbound messages, outbound replies, and internal notes) to the thread.
You must create the thread before importing its messages. Each mutation is idempotent: if you call it again with the same `externalId`, the duplicate is skipped (the result will be `NOOP`).
## Permissions
To import threads you need an API key with the following permissions:
* `thread:import`
* `attachment:create` (if importing messages with attachments)
## Import a thread
The `importThread` mutation creates a thread tied to an existing customer. You can identify the customer by their Plain customer ID, email address, or external ID.
The `statusDetail.type` must match the thread `status`:
* `TODO` allows `NEW_REPLY` or `IN_PROGRESS`
* `SNOOZED` allows `WAITING_FOR_CUSTOMER`
* `DONE` allows `DONE_MANUALLY_SET` or `IGNORED`
If you provide a `tenantId`, the customer must already be a [member of the tenant](/docs/graphql/tenants/add-customers) or the import will fail.
The mutation returns a `result` field which is one of:
* `CREATED`: the thread was imported successfully.
* `NOOP`: a thread with this `externalId` already exists, so the import was skipped.
## Import thread messages
Once you have a thread, use `importThreadMessages` to add conversation history. You can import up to **25 messages per call**.
Each message has a `type` that determines which `author` field to set:
* `INBOUND`: set `author.customerId` (the customer who sent the message)
* `OUTBOUND`: set `author.userId` (the support agent who replied)
* `NOTE`: set `author.userId` (the agent who wrote the internal note)
Exactly one of `customerId` or `userId` must be provided.
The mutation returns a `results` array with one entry per message in the same order as the input. Each result contains:
* `result`: `CREATED` or `NOOP` (if a message with that `externalId` already exists).
* `threadMessage`: the created `TimelineEntry` (for `INBOUND`/`OUTBOUND`) or `Note` (for `NOTE`). Null if the message failed.
* `error`: per-message error details, null on success.
If some messages fail while others succeed, the top-level `error` will have the code `bulk_partial_failure`.
## Importing messages with attachments
To import messages that have attachments, you need to upload the attachments first and then reference them by ID.
Use `createAttachmentUploadUrl` to get an upload URL, then upload the file. See the [attachments guide](/docs/graphql/attachments) for the full upload flow.
When creating the upload URL, use the attachment type `CUSTOM_TIMELINE_ENTRY` for `INBOUND` and `OUTBOUND` messages, or `NOTE` for `NOTE` messages.
Pass the attachment ID (returned by `createAttachmentUploadUrl`) in the `attachmentIds` array of the message:
```json theme={null}
{
"author": {
"customerId": "c_01H14DFQ4PDYBH398J1E99TWSS"
},
"text": "Here is a screenshot of the error.",
"createdAt": "2024-06-15T10:32:00Z",
"type": "INBOUND",
"externalId": "msg_004",
"attachmentIds": ["att_01HB924PME9C0YWKW1N4AK3BZA"]
}
```
Attachments that are uploaded but not referenced by any message are deleted after 24 hours. Make sure to call `importThreadMessages` promptly after uploading.
## Putting it all together
A typical migration script follows this order:
1. [Upsert customers](/docs/graphql/customers/upsert) so they exist in Plain.
2. Call `importThread` for each ticket in the source system.
3. Upload any attachments using [`createAttachmentUploadUrl`](/docs/graphql/attachments).
4. Call `importThreadMessages` with the thread ID and messages (in batches of up to 25).
5. Check the `result` and `error` fields to confirm each import succeeded.
Since both mutations are idempotent on `externalId`, you can safely re-run a migration script without creating duplicates.
# Changing status
Source: https://www.plain.com/docs/graphql/threads/status-changes
Move a thread between Todo, Snoozed, and Done, and read what each status means.
Threads can be in one of 3 statuses:
* `Todo`
* `Snoozed`
* `Done`
When you log into Plain you can filter threads by these statuses.
When threads are created they default to `Todo`.
To change a thread's status you need an API key with the following permissions:
* `thread:edit`
* `thread:read`
## Mark thread as `Done`
When any activity happens in a thread, it will move back to `Todo`.
Unlike traditional ticketing software, Plain expects a thread to move between `Todo` and `Done` several times over the course of helping a customer. This will not break or influence any metrics. `Done` in Plain means "I'm done for now, there is nothing left for me to do".
## Snooze thread
You can snooze threads for a duration of time defined in seconds.
When any activity happens in a thread, it will be automatically unsnoozed and move to `Todo`. Otherwise threads will be unsnoozed when the timer runs out.
## Mark thread as `Todo`
This is useful if you mistakenly marked a thread as `Done` or snoozed a thread and want to unsnooze it. Otherwise write a message and the thread moves back to **Todo** on its own.
# Thread fields
Source: https://www.plain.com/docs/graphql/threads/thread-fields
Extend the thread model with your own typed fields, defined by a schema.
Thread fields allow you to extend Plain's thread data model. The thread fields which you want to support have to conform to a schema configured in **Settings** → **Thread fields**.
Thread fields can be nested and be either a boolean, text or a string enum.
Thread fields can be required. When they are required, their value must be set in order for the thread to be marked as done.
For interacting with thread fields via the API, every field has a `key` defined in its schema. Keys let you refer to a thread field without having to know its ID in the schema. For example if you have a field called "Product Area" the key you might choose for the key to be `product_area`.
## Manage thread field schemas
Most teams configure thread field schemas in **Settings** → **Thread fields**, but if you want to provision them programmatically you can do so via the API.
### Get thread field schemas
### Create a thread field schema
`key` must be unique within your workspace. `type` is one of `STRING`, `BOOL`, `NUMBER`, `DATETIME`, or `ENUM` (for enum types, populate `enumValues`).
This operation requires the following permissions:
* `threadFieldSchema:create`
### Update a thread field schema
Field-level wrapper inputs apply: pass `{ "value": ... }` for the fields you want to change.
This operation requires the following permissions:
* `threadFieldSchema:edit`
### Delete a thread field schema
Deleting a schema also removes any values stored against it on threads.
This operation requires the following permissions:
* `threadFieldSchema:delete`
### Reorder thread field schemas
`reorderThreadFieldSchemas` updates the `order` of multiple schemas in a single call. You don't need to include every schema. Only the ones whose order is changing.
## Manage thread field values
### Upsert a thread field
To upsert a thread field you need an API key with the following permissions:
* `threadField:create`
* `threadField:update`
### Delete a thread field
To delete a thread field you need an API key with the following permissions:
* `threadField:delete`
# Update thread attributes
Source: https://www.plain.com/docs/graphql/threads/update
Change individual thread attributes such as title, priority, customer, or tenant.
The mutations below change individual attributes on an existing thread. Each one operates on a single field. There is no general-purpose `updateThread` mutation, so to change multiple attributes you call multiple mutations.
These operations require the following permissions:
* `thread:edit`
## Change thread title
## Change thread priority
Priority is an integer from `0` (urgent) to `3` (low).
## Change the thread's customer
Reassigns the thread to a different customer in your workspace. The original customer keeps any other threads they have.
## Change the thread's tenant
Move the thread to a different tenant (or pass `tenantIdentifier: null` to detach the thread from its current tenant).
## Change the thread's tier
Move the thread to a different tier (or pass `tierIdentifier: null` to detach).
# Tiers & SLAs
Source: https://www.plain.com/docs/graphql/tiers
Tiers group companies and tenants so you can attach different SLA targets to each.
Within Plain you can organize [companies](/docs/product/platform/companies) and [tenants](/docs/product/platform/tenants) into Tiers. Tiers should match your pricing tiers (e.g. "Enterprise", "Pro", "Free", etc.).
This allows you to prioritize and filter your threads by tier.
Tiers also add support for defining [SLAs](/docs/product/platform/slas/tier-slas) so you can enforce a first-response time for different support tiers within your product or pricing.
Tiers are created via the UI in Plain. Tenants and companies are then added and removed via the API when this happens in your product, so Plain stays in sync.
# Add companies and tenants to tiers
Source: https://www.plain.com/docs/graphql/tiers/add-members
Add companies and tenants to a tier in a single mutation.
You can add multiple tenants and companies to a tier in a single mutation.
Companies and tenants can only be in a single tier.
This operation requires the following permissions:
* `tierMembership:read`
* `tierMembership:create`
# Get tiers
Source: https://www.plain.com/docs/graphql/tiers/get
Fetch tiers by ID or as a paginated collection, with their SLA configuration.
These operations require the following permissions:
* `tier:read`
## Get tiers
This endpoint supports [Pagination](/docs/graphql/pagination).
### Get tier by ID
If you know the tiers's ID in Plain you can use this method to fetch the tier.
# Remove companies and tenants from tiers
Source: https://www.plain.com/docs/graphql/tiers/remove-members
Remove companies and tenants from the tiers they belong to.
You can remove companies and tenants from the tiers they are part of manually in the UI or via the API.
This operation requires the following permissions:
* `tierMembership:read`
* `tierMembership:delete`
# Service level agreements
Source: https://www.plain.com/docs/graphql/tiers/service-level-agreements
Set first-response and next-response SLA targets per tier and per priority.
[Service level agreements (SLAs)](/docs/product/platform/slas/tier-slas) commit you to responding to a customer within a certain time. SLAs are attached to a tier, which means every company or tenant belonging to that tier inherits them.
Two types of SLA exist:
* **First response time**: measured from when a thread is created until the first response is sent.
* **Next response time**: measured each time the customer sends a new message, until a user responds.
SLAs can be scoped to specific thread priorities or labels, and can either be tracked 24/7 or only during your workspace's business hours.
These operations require the following permissions:
* `serviceLevelAgreement:create`
* `serviceLevelAgreement:edit`
* `serviceLevelAgreement:delete`
## Create an SLA
Provide either `firstResponseTimeMinutes` or `nextResponseTimeMinutes` (but not both. That creates a first-response SLA on one tier and a next-response SLA on the same tier with separate calls).
## Update an SLA
Field-level wrapper inputs apply: pass `{ "value": ... }` for the fields you want to update and omit the rest.
## Delete an SLA
# Update company tier
Source: https://www.plain.com/docs/graphql/tiers/update-company-tier
Set the tier for a company explicitly, rather than inheriting it.
If you want to explicitly set the tier for a company you can do so using this mutation. If instead you want to add many companies to a tier at once, you can use the [add members mutation](./add-members).
This operation requires the following permissions:
* `tierMembership:read`
* `tierMembership:create`
# Update tenant tier
Source: https://www.plain.com/docs/graphql/tiers/update-tenant-tier
Set the tier for a tenant explicitly, rather than inheriting it.
If you want to explicitly set the tier for a tenant you can do so using this mutation. If instead you want to add many companies to a tier at once, you can use the [add members mutation](./add-members).
This operation requires the following permissions:
* `tierMembership:read`
* `tierMembership:create`
# Webhook targets
Source: https://www.plain.com/docs/graphql/webhook-targets
A webhook target is an HTTP endpoint Plain delivers events to, subscribed to the events you choose.
A **webhook target** is an HTTP endpoint that Plain delivers events to. Each target subscribes to one or more event types and is pinned to a specific webhook schema version.
This section of the GraphQL API covers the *management* of webhook targets. How to register a new endpoint, change which events it receives, disable it temporarily, or inspect delivery attempts when something goes wrong.
If you're looking for the format of the events themselves, see [Webhooks](/docs/webhooks).
These operations require the following permissions:
* `webhookTarget:create`
* `webhookTarget:edit`
* `webhookTarget:delete`
The read queries require:
* `webhookTarget:read`
Webhook targets can also be created and managed in the Plain app under **Settings → Webhooks**. Use the API when you want to provision targets as part of an infrastructure-as-code setup or to wire up environments programmatically.
# Create a webhook target
Source: https://www.plain.com/docs/graphql/webhook-targets/create
Register an HTTP endpoint to receive webhook events, and choose which events it gets.
Creating a webhook target registers a new HTTP endpoint that Plain will deliver events to. You must pass:
* `url`: the endpoint Plain should POST to
* `description`: a short human-readable label (shown in the Plain app)
* `isEnabled`: whether deliveries should start immediately
* `eventSubscriptions`: the list of event types this target should receive
* `version`: the webhook schema version to pin to (we recommend always pinning to a specific version)
The full list of subscribable event types is available via the `subscriptionEventTypes` query, or in the [webhooks reference](/docs/webhooks).
# Delete a webhook target
Source: https://www.plain.com/docs/graphql/webhook-targets/delete
Delete a webhook target and stop all deliveries to that endpoint.
Deleting a webhook target stops deliveries to that endpoint and removes the target from your workspace. Existing delivery attempt history is retained until normal retention expiry.
# Inspect delivery attempts
Source: https://www.plain.com/docs/graphql/webhook-targets/delivery-attempts
Inspect each delivery attempt to a webhook target, including failures and retries.
Each time Plain attempts to deliver an event to a webhook target, the result is recorded as a delivery attempt. This is useful for debugging failing webhooks programmatically: for example to surface a recent failure rate in your own observability tooling.
Each attempt records:
* the event ID and event type that was delivered
* when the attempt happened and how long it took
* the result, which is one of: a successful HTTP response, a failed HTTP response (4xx/5xx), an error (network failure), a rejection, or a schema validation failure
You can filter attempts by event types or by result status.
# Fetch webhook targets
Source: https://www.plain.com/docs/graphql/webhook-targets/get
Fetch webhook targets by ID or as a paginated collection.
You can fetch webhook targets as a paginated collection, or fetch a single target by ID.
## List webhook targets
## Get a webhook target by ID
# Update a webhook target
Source: https://www.plain.com/docs/graphql/webhook-targets/update
Change a target's URL, pause it, or move it to a newer schema version.
Update an existing webhook target. For example to change the URL, pause deliveries by setting `isEnabled` to false, change which events are delivered, or move to a newer schema version.
Field-level wrapper inputs apply for scalar fields. Pass `{ "value": ... }` for the fields you want to change. `eventSubscriptions` is the exception: it's a full replacement of the previous list, so include every event type the target should subscribe to.
# Headless portal
Source: https://www.plain.com/docs/headless-portal
Build a support portal inside your own product using the Plain API, so customers read and reply to their threads without leaving your UI.
A headless portal lets your customers read, create, and reply to their threads inside your own product. You build the interface; Plain stores the threads, routes them to your team, and gives you the API to read and write them.
Because you own the markup, the portal matches your product rather than looking like an embedded widget, and customers stay signed in with your existing session instead of a separate support login. Threads created this way arrive with the tenant, priority, and labels already set, so they land in your queue ready to triage.
Building one is four pieces of work:
1. Create a tenant for each of your customers.
2. Fetch that tenant's threads to list them.
3. Let customers open new threads.
4. Let customers reply to a thread.
Read the [data model](/docs/graphql/introduction) first, since the rest of this page assumes it.
## Create a tenant for each customer
When a customer signs up to your product, create a tenant for them. Each tenant in Plain should map 1:1 to the workspace, team, or organization concept in your own product. [Tenants](/docs/graphql/tenants) covers the model in full.
This is the step that scopes the portal. Without tenants, there is no reliable way to tell which threads belong to which of your customers' teams, so a portal cannot show one team its own threads and nothing else.
Create tenants by [upserting them](/docs/graphql/tenants/upsert), then [add individual customers to a tenant](/docs/graphql/tenants/add-customers).
You can skip tenants while prototyping and filter by customer instead. The trade-off is that if John and Lucy are on the same team in your product, John sees only his own threads and not Lucy's. For most B2B products that is the wrong experience, but it is fine for a first pass.
## Fetch a tenant's threads
Once threads carry a tenant, fetch them by filtering on it:
```ts theme={null}
const threads = await plainClient.getThreads({
filters: {
tenantIdentifiers: [{ externalId: tenantExternalId }],
statuses: [ThreadStatus.Todo, ThreadStatus.Snoozed],
},
});
```
If you skipped tenants, filter by `customerId` instead to get one customer's threads.
That returns the threads but not their contents. To read the messages on a thread, query its timeline entries:
```graphql theme={null}
query threadTimeline($threadId: ID!, $first: Int, $after: String, $last: Int, $before: String) {
thread(threadId: $threadId) {
title
description
priority
status
createdAt {
__typename
iso8601
}
customer {
fullName
}
updatedAt {
__typename
iso8601
}
timelineEntries(first: $first, after: $after, last: $last, before: $before) {
edges {
cursor
node {
id
timestamp {
__typename
iso8601
}
actor {
__typename
... on UserActor {
user {
fullName
}
}
... on CustomerActor {
customer {
fullName
}
}
... on MachineUserActor {
machineUser {
fullName
}
}
}
entry {
__typename
... on CustomEntry {
title
components {
__typename
... on ComponentText {
text
}
}
}
... on ChatEntry {
chatId
text
}
}
}
}
pageInfo {
__typename
hasPreviousPage
hasNextPage
startCursor
endCursor
}
}
}
}
```
## Let customers open new threads
Build a contact form in your portal that [creates a thread](/docs/graphql/threads/create). Pre-fill the customer's tenant ID on the thread, or it will not appear in the portal.
A form with only a title and a message works, but structured questions are worth the extra fields: what the request is about, how urgent it is, which product area it affects. Those answers become thread fields and labels, which is what makes the thread routable without a human reading it first.
## Let customers reply to a thread
Technically this is one call to the `replyToThread` mutation. The work is in the interface rather than the API.
What you build depends on the channels you support. If your portal does not handle email, you do not need `Cc` or `Bcc` fields.
## Example implementation
There is a working Next.js example that puts all four pieces together:
[Headless portal example on GitHub](https://github.com/team-plain/example-headless-portal)
## Security
A portal calls [Plain's GraphQL API with an API key](/docs/graphql/authentication). That key needs broad permissions, because it reads threads and customer details and performs actions such as sending email.
Never call the Plain API directly from the browser. Doing so exposes an API key that can read every thread and customer in your workspace.
Make every Plain API call from a backend you control. That is also where you enforce access control, so a customer can only reach their own threads.
If a key leaks, delete it in your workspace settings straight away, then contact Plain so we can help with mitigation and investigation.
## Getting help
A polished portal involves more than the four steps above: formatting, attachments, and file uploads all add work. Plain's engineering team can help you plan and scope it against your product and stack.
The headless portal is available on the Horizon and Frontier plans. To talk it through, contact us in Plain or at [help@plain.com](mailto:help@plain.com).
# Agent integrations
Source: https://www.plain.com/docs/integrations
Connect Plain to your editor or a local assistant.
These pages cover connecting Plain to tools you use rather than building an agent that works threads on its own. Point an assistant at your support data, give it a skill for a specific job, or read and reply to threads from your editor.
To build an agent that runs inside Plain, start with [building agents](/docs/agents).
Read and write support data from a local assistant over MCP.
Package a Plain job as a skill.
Read and reply to Plain threads from inside Cursor.
# Agent skill
Source: https://www.plain.com/docs/integrations/agent-skill
Connect Plain to AI coding agents like Claude Code, Codex, Amp, and OpenCode using the Plain Support Skill.
The [Plain Support Skill](https://github.com/team-plain/skills) gives AI tools direct access to your Plain workspace via the GraphQL API.
Once installed, your agent can read customers, threads, thread timelines, Help Center articles, and more.
The skill uses your Plain API key to authenticate, so the agent will only have access to what that API key permits.
## Compatible agents
The Plain Support Skill follows the open [Agent Skills specification](https://skills.sh) and works with any compatible agent, including:
* [Claude Code](https://code.claude.com)
* [OpenAI Codex](https://openai.com/codex/)
* [Amp](https://ampcode.com)
* [OpenCode](https://opencode.ai)
* [Cursor](https://cursor.com)
* And [many more](https://skills.sh/docs/faq)
## Installation
Install the skill using the skills CLI:
```bash theme={null}
npx skills add team-plain/plain-support
```
The installer will ask which agent(s) you want to install to. You can also specify the agent directly:
```bash theme={null}
npx skills add team-plain/plain-support -a claude-code
npx skills add team-plain/plain-support -a codex
npx skills add team-plain/plain-support -a opencode
```
Alternatively, you can clone the repository manually:
```bash theme={null}
git clone git@github.com:team-plain/plain-support.git
```
## Prerequisites
### API key
In Plain, go to **Settings → Machine users & API keys** and create an API key. Then set it as an environment variable:
```bash theme={null}
export PLAIN_API_KEY="plainApiKey_..."
```
Add this to your shell profile (`.bashrc`, `.zshrc`, etc.) so it persists across sessions.
### Dependencies
The skill requires `curl` and `jq`. On most systems `curl` is already installed. For `jq`:
```bash theme={null}
# macOS
brew install jq
# Ubuntu/Debian
sudo apt-get install jq
```
## What you can do
| Resource | Read | Write |
| ---------------- | ------------------------------------------- | --------------------------------- |
| **Customers** | List, get, search by name/email/external ID | None |
| **Threads** | List, get, search, read full timeline | Add internal notes |
| **Help Center** | List centers, articles, groups | Create/update articles and groups |
| **Companies** | List, get | None |
| **Tenants** | List, get | None |
| **Labels** | List | None |
| **Tiers & SLAs** | List, get with SLA configs | None |
| **Workspace** | Get info | None |
## Use case examples
* **Debug a customer issue end to end:** Ask your agent to look up a customer, read their thread timeline, and summarize the conversation. Then use that context to investigate the bug in your codebase and put up a fix, all in one session.
* **Research customer history before a call:** "Show me everything about customer [john@acme.com](mailto:john@acme.com)". The agent will pull their profile, open threads, and recent conversations so you're fully briefed.
* **Add investigation notes:** After debugging an issue, tell the agent to add your findings as an internal note on the thread so your team has context.
* **Draft Help Center content:** Ask the agent to create a new Help Center article based on patterns it sees in support threads. It can write the HTML content and publish it directly to your Plain Help Center.
* **Ad hoc analysis:** "Which customers are in the Enterprise tier?" or "Show me all open threads with priority 0". Query your support data conversationally.
## Example prompts
* "What are the open support threads for customer [john@example.com](mailto:john@example.com)?"
* "Read the full conversation on thread th\_01ABC and summarize it"
* "Add a note to thread th\_01ABC with my investigation findings"
* "Create a Help Center article about how to reset passwords"
* "Which customers are in the Enterprise tier?"
* "List my open threads"
# Cursor
Source: https://www.plain.com/docs/integrations/cursor
Ask Cursor about your product's code from inside a Plain thread.
Setting up a Cursor integration lets you can ask Cursor about your product's code directly in Plain. The Cursor integration lets you and Sidekick ask questions about your codebase(s) to help troubleshoot and resolve customer issues faster.
The Cursor integration requires their [Cloud Agents](https://cursor.com/blog/cloud-agents) feature, which is only available on Cursor Pro plans (and above) at this time.
## What it does
The Cursor integration connects Plain with your GitHub repositories through Cursor's AI-powered code analysis. You can ask questions like "How does the password reset flow work" or "What API endpoints handle user authentication?" and get answers based on your actual code.
This is especially useful when:
* A customer reports a bug and you need to understand what the code does
* You need to verify how a specific feature is implemented
* You want to suggest troubleshooting steps based on the actual code paths
* You're looking for specific implementation details to help answer technical questions
## Setting up your API key
You need to be a workspace admin to set up the Cursor integration.
The Cursor integration requires Plain AI. If it is not on already, go to **Plain AI** settings and click **Enable Plain AI**.
Log into [Cursor](https://cursor.com) and go to **Dashboard → Integrations**.
Connect **GitHub** to Cursor first. Whichever repositories you allow here are the only ones Cursor can look up from Plain.
Generate a new **User API key** and copy it. It starts with `key_`.
In Plain, go to **Plain AI → Configuration → Ask Cursor**, paste the key, and click **Create Integration**.
Anyone in your workspace can now ask questions from Cursor on any thread.
Note that while the Cursor API key is scoped to a single person's Cursor account, on Plain's side, it's shared by the entire workspace.
## How it works
### Asking questions from a thread
1. Click **Ask Cursor** button in the thread sidebar under **Actions** (or press Cmd + K and search "Ask Cursor")
2. Type your question. Note that Cursor won't automatically get the context of the thread you're on, so include any relevant details from the customer as needed.
3. Select which repository you want to search
When you ask a question, you select which GitHub repository to search. Make sure you pick the repo that contains the relevant code for your question.
If your product uses multiple repositories (like separate frontend and backend repos), you might need to ask separate questions for each one.
4. Click Ask
The Cursor agent runs in the background and searches your codebase. When it's done, you'll see the results in the thread timeline. The thread automatically moves to "Close the loop" status so you can come back to review the answer.
### What to ask
Good questions for Cursor are specific and focused on understanding how the code works:
* *How does the user authentication flow work?*
* *What happens when a payment fails in the checkout process?*
* *How do we validate email addresses in the signup form?*
* *What error handling do we have for API timeouts?*
Less effective questions:
* Broad questions like "How does the app work?
* Questions about user data (Cursor only reads code, not databases)
* Questions better suited for your documentation or past threads
The more specific your question, the more useful the answer will be.
### Reviewing agent responses
Questions asked from Cursor will appear in the thread sidebar under **Actions**. New messages from Cursor also appear in the thread timeline, so you can see responses at a glance without opening the Ask Cursor sidebar popover. Sidekick will also proactively make use of any existing Cursor agent responses when summarizing threads or making suggestions.
Note that all Cursor questions and responses are visible to the rest of your team.
At the moment, **Ask Cursor** is limited to a single response per question. You cannot ask follow-ups. When Cursor isn't able to answer your question well, ask a new one, providing more information as needed.
# MCP server
Source: https://www.plain.com/docs/integrations/mcp-server
Connect Claude Code, Cursor, or any MCP client to your Plain workspace over the Model Context Protocol.
MCP (Model Context Protocol) is an open standard that lets AI tools connect to external platforms. The Plain MCP server at `https://mcp.plain.com/mcp` lets tools like Claude, ChatGPT, and Cursor work with your support data directly.
You authenticate with your existing Plain account, so the MCP server has the same permissions as your user. No API keys needed.
## Setup
### ChatGPT
To use the Plain MCP in ChatGPT you will have to create an app, but first you may need to enable Developer Mode in **Settings** → **Apps** > **Advanced settings**.
To add Plain's MCP as an app:
1. Go to **Settings** → **Apps**
2. Click **Create app,** Name it "Plain" and enter the URL `https://mcp.plain.com/mcp`
3. Set authentication to **OAuth**, then click **Create** and complete the login flow.
If you don't see "Advanced Settings" in the "Apps" settings menu, you might need to reach out to your administrator to enable adding "Custom Apps" for your team.
### Cursor
You can add the Plain MCP to Cursor via your Cursor config at `~/.cursor/mcp.json`
```json theme={null}
{
"mcpServers": {
"plain": {
"type": "remote",
"url": "https://mcp.plain.com/mcp"
}
}
}
```
Go to **Cursor Settings** → **Tools & MCP** and click **Connect** next to Plain to authenticate.
### Claude.ai
1. Go to "Customize" → "Connectors"
2. Click the ➕ icon, then choose "Add custom connector"
3. Add a custom connector named "Plain" with the Remote MCP server URL `https://mcp.plain.com/mcp`
4. Click the "Connect" button next to the newly created Plain connector to authenticate.
If you're on a Claude Team plan, you may need to reach out to your administrator to add this custom connector for you.
### Claude Code
Adding the Plain MCP to claude-code can be done via the `mcp-remote` helper package which involves adding the following to your `~/.claude.json` configuration.
```json theme={null}
"mcpServers": {
"plain": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://mcp.plain.com/mcp"],
"enabled": true
}
}
```
Unfortunately Claude Code does not support refreshing OAuth tokens yet. A workaround until that is implemented is to use the `mcp-remote` npm package.
Next time you start Claude Code, it will launch a browser window asking you to authenticate with your Plain account and select the workspace you'd like to connect. Run the `/mcp` command and select the `plain` MCP to see more details.
### **FX**
[**fx**](https://fx.sh/ "https://fx.sh") is a terminal AI coding agent from Vercel Labs. Add the Plain MCP via the CLI:
```text theme={null}
fx mcp add --transport http plain https://mcp.plain.com/mcp
```
Or add it manually to `~/.fx/mcp.json`:
```text theme={null}
{
"mcp": {
"plain": {
"type": "http",
"url": "https://mcp.plain.com/mcp",
"enabled": true
}
}
}
```
fx will prompt you to complete the OAuth flow with your Plain account on the next session start.
### Other MCP clients
MCP clients that run locally (not in the browser) can connect to Plain if they support remote HTTP servers. Point it at `https://mcp.plain.com/mcp` and complete the OAuth flow when prompted.
## Authentication
Plain MCP uses OAuth to allow your MCP server connection to share the same context as your normal web app user. No additional managing of permissions required. This means replies to threads will appear as though they come from your user.
### Required OAuth scopes
The Plain MCP server requires every OAuth token to include the `openid `and`offline_access` scopes. If your authorization request omits these, the server returns `403 Forbidden` , even when OAuth completes successfully and returns tokens.
Set `scope` to `openid offline_access` (space-separated) in your authorization request. If you are using `@modelcontextprotocol/sdk`, some versions discover required scopes automatically via the [Protected Resource Metadata endpoint](https://mcp.plain.com/.well-known/oauth-protected-resource/mcp). If yours does not, set them explicitly in your client configuration.
After successfully authenticating, your client should receive both an `accessToken` as well as a `refreshToken`. It is the client's responsibility to do the actual refreshing. Most agentic coding applications support this now, but we have had issues with claude-code in particular. Use the '[mcp-remote](https://www.npmjs.com/package/mcp-remote)' package and the setup detailed above rather than passing only the URL as a "remote" mcp server in claude's configuration.
## Available tools
The MCP server includes 30 tools. Tools marked **write** take actions in your workspace. Most AI clients will ask for confirmation before executing them.
### Threads
| **Tool** | **Access** | **What it does** |
| ------------------------- | ---------- | -------------------------------------------------------------- |
| Fetch Threads | Read | List threads with filters and pagination |
| Fetch Thread Details | Read | Full thread details plus timeline entries |
| Fetch My Assigned Threads | Read | List threads assigned to you (active by default) |
| Search Threads | Read | Search threads by text with optional filters |
| Reply To Thread | Write | Send a reply to an email, Slack, or form thread |
| Assign Thread | Write | Assign a thread to a user, machine user, or yourself |
| Unassign Thread | Write | Remove the current assignee |
| Mark Thread As Done | Write | Mark a thread as resolved |
| Mark Thread As Todo | Write | Reopen or set a thread back to active |
| Snooze Thread | Write | Snooze a thread until later |
| Change Thread Priority | Write | Set priority: urgent, high, normal, or low |
| Add Labels | Write | Add labels to a thread |
| Create Note | Write | Add an internal note to a thread or customer |
| Fetch Citations | Read | List knowledge sources that Ari used when replying on a thread |
### Customers
| **Tool** | **Access** | **What it does** |
| ---------------------- | ---------- | ----------------------------------------- |
| Fetch Customers | Read | List customers (excludes spam by default) |
| Fetch Customer Details | Read | Full profile for a specific customer |
| Fetch Customer Threads | Read | List threads for a specific customer |
| Search Customers | Read | Search by name, email, or external ID |
### Tenants
| **Tool** | **Access** | **What it does** |
| -------------------- | ---------- | ------------------------------------ |
| Fetch Tenants | Read | List tenants with pagination |
| Fetch Tenant Details | Read | Full details including tenant fields |
| Search Tenants | Read | Search by name or external ID |
| Upsert Tenant | Write | Create or update a tenant |
### Help Center
| **Tool** | **Access** | **What it does** |
| ------------------------------- | ---------- | ------------------------------------ |
| Get Help Centers | Read | List your help centers |
| Get Help Center Article Groups | Read | List your Help Center article groups |
| Get Help Center Articles | Read | List articles in a Help Center |
| Get Help Center Article | Read | Fetch an article by ID |
| Get Help Center Article By Slug | Read | Fetch an article by slug |
| Upsert Help Center Article | Write | Create or update an article |
### User and workspace
| **Tool** | **Access** | **What it does** |
| ------------------- | ---------- | ---------------------------------- |
| Fetch My User | Read | Your authenticated user profile |
| Fetch My Workspace | Read | Your workspace details |
| Fetch User By Email | Read | Find a Plain user by email address |
| Fetch Labels | Read | List all labels in your workspace |
## Use case examples
### Triage your queue
> "Show me my assigned threads. Summarize the top 5 by urgency and suggest which ones I should handle first."
Read your active threads and get a prioritized summary without opening each one.
### Understand a customer's history
> "Look up the customer with email [alex@acme.com](mailto:alex@acme.com). Show me their recent threads and summarize the pattern. Are they hitting the same issue repeatedly?"
Pull together a customer's full context in seconds instead of clicking through timelines.
### Summarize what's waiting
> "Show me all my TODO threads. Group them by label and summarize what each one is about."
See what has gone quiet, so you can decide what needs attention.
### Draft a reply with full context
> "Show me thread th\_01ABC123. Read the full timeline, then draft a reply that addresses the customer's latest question. Use a helpful but concise tone."
The AI reads the full thread history and drafts a reply for you to review before sending.
### Audit your Help Center
> "List all articles in my Help Center. Flag any that haven't been updated in the last 90 days."
Spot outdated documentation without manually checking each article.
### Get a snapshot of a tenant's health
> "Look up the tenant 'Acme Corp'. Show me their details and all open threads. Are there any patterns or escalation risks?"
Prep for an account review by pulling tenant context and open issues together.
### Cross-reference with other tools
The Plain MCP works alongside other MCP servers. If you also have Slack, GitHub, or Linear MCPs connected, you can combine them:
> "Find threads about 'payment errors' from the last 7 days, check if there's a related GitHub issue, and post a summary to #incidents in Slack."
Each server handles its own domain, and the AI coordinates between them.
## If something isn't working
* **Can't authenticate:** Make sure you can log into Plain at [app.plain.com](http://app.plain.com) first. The MCP server uses the same auth.
* **403 after OAuth completes:** Your token is missing required scopes. Add `openid offline_access` to the `scope` parameter in your authorization request and complete a fresh OAuth flow. See [Authentication](#authentication) for details.
* **Tool not appearing:** Some AI clients cache available tools. Try disconnecting and reconnecting the MCP server.
* **Permission errors:** Check that your Plain user has the right role for the action you're trying to take.
If it's still not working, collect:
* Your workspace ID (visible in **Settings** → **Workspace**)
* The exact error message
* Which AI client you're using
Then reach out to us via [**help@plain.com**](mailto:help@plain.com) for assistance.
# mTLS
Source: https://www.plain.com/docs/mtls
Plain presents a client certificate on outbound requests, so you can verify they came from Plain.
All outbound requests made to your **webhook targets** and **customer card endpoints** include a client TLS certificate which you can verify to achieve mutual authentication.
This certificate is self-signed. In order to verify it, we provide our CA's certificate (in PEM format), which you need to add to your server/truststore:
```plaintext theme={null}
-----BEGIN CERTIFICATE-----
MIIDDzCCAfegAwIBAgIUPLCyLvion+WDNw0V8HAZEZL5VjswDQYJKoZIhvcNAQEL
BQAwFjEUMBIGA1UEAwwLUGxhaW5NdGxzQ0EwIBcNMjQxMDEwMDkwMzMzWhgPMjEy
NDA5MTYwOTAzMzNaMBYxFDASBgNVBAMMC1BsYWluTXRsc0NBMIIBIjANBgkqhkiG
9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvikyF2YpU4zEYUWVYMc5P07CPQgtP6Agoia9
mElydDTReTXW9Rle0apHKNS8OUk8S6qtA5raEh8VT2HOZBUTZb16A1vl54be+LK7
imm7csEsU+FbHbfx9rRbisESu6Mkvf5qklovgcg5UfI4IrmQK3POB6pMBCcmdjyZ
udbx6YSrV5LZLth7Gxq9lcPuwzzpv2DWZTr1GGAQ46UNLXNo4+4IQYtgjThRAl4m
IBbezmiXqpi9N/7ay+P9kb4TZDQohentJu/1+y6Bj8Mxk86kq0KLlYfrEbm86lGp
mJ8s3R5luh98muRT4NdKeoHGf96UAqUq21i00TDJ/PklqardWQIDAQABo1MwUTAd
BgNVHQ4EFgQUlYHkn4D7QBvBudbhtq2M+f8CzpAwHwYDVR0jBBgwFoAUlYHkn4D7
QBvBudbhtq2M+f8CzpAwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC
AQEAMMLZc8zu7AqP+c2Pms6kRkp9Wr/C6QmXMuhHC98RZL1VcmZhE2P0lg/t644o
prYX8yf7Z2SRZgNb2s8oekPpuI2U2WFC4eam1dK5kS4ux7IgaXZkuB8DyZVSo1WO
KeIb2IYmXZ6hflnFNsTRjhe/Bkb7uVVw5jMaPfxWqPmeHtgUIIoh7nYj+ZnqV5Jz
FQFDb+dZzZDol/Wa3XKm7w96MrX/tanAKTygIkXyjqCrjxTI26latBQV2OPADrRO
uagGFG2G0o56wC8LTJdmceZfWYmVBLawSibj75Av8fwHgXK+XAi05m2GuVOQAfLq
yuMQLHrNDReQDB1tylx13b6meg==
-----END CERTIFICATE-----
```
If you serve your API through AWS API Gateway, you can do this by [enabling mTLS and
uploading the
certificate](https://docs.aws.amazon.com/apigateway/latest/developerguide/rest-api-mutual-tls.html)
above as the truststore.
# Getting started with Ari
Source: https://www.plain.com/docs/product/agents/ari
Ari is Plain's customer-facing AI agent. Set it up, test it, and deploy it in five steps.
Ari is the **customer facing AI agent** built into Plain.
It answers what it can from your [Knowledge Sources](/docs/product/agents/knowledge-sources), and hands off to your team the moment it is unsure, before one of your customers has to ask twice.
There is no flow builder and no decision tree to configure. Turn Ari on, point it at your knowledge, and it starts answering.
1. Resolves the routine instantly, around the clock
2. Knows when to step back, and hands off to a human before a customer gets frustrated
3. Answers only from your knowledge, so its responses stay grounded in what you've told it
4. Answers from your knowledge with no flow to build
## Ari and Sidekick
Plain has two AI agents. Ari responds autonomously to your customers without a human-in-the-loop; Sidekick works for your team and helps you automate your support process.
| | Ari | Sidekick |
| --------- | ----------------------------------------------------------- | -------------------------------------------- |
| Talks to | Your customers | Your team |
| Where | On threads assigned to it | Inside Plain, and in your Slack |
| Knowledge | [Knowledge Sources](/docs/product/agents/knowledge-sources) | The same, plus past threads and integrations |
Set up, test, and deploy Ari in five steps.
## 1. Enable setting
Ari is part of Plain AI, which is enabled by default on new workspaces. Check [this page](https://app.plain.com/~/ai) if you've previously opted out.
## 2. Add knowledge
Ari automatically reads your customer facing Help Centers. Add anything outside Plain as a [Knowledge Sources](/docs/product/agents/knowledge-sources). Ari only answers from this knowledge; without it, it will have a high deflection rate.
When Ari can't answer a question from this knowledge, Plain records a [knowledge gap](/docs/product/agents/knowledge-gaps) and raises a task for your team, so you know what to document next.
## 3. Route threads
Ari works on threads assigned to it. There are two ways to do that.
### Assign via workflow
Go to [Workflows](https://app.plain.com/~/workflows), create a rule, and use the "Assign to user" action with Ari selected. The conditions on the rule decide which threads Ari picks up, so you can scope it by tier, label, channel, or business hours.
Example: "When a new thread is created on the chat channel with tier Free, assign to Ari."
Use this [Workflow Template](https://app.plain.com/~/workflows/gallery/route-new-chat-threads-to-ari-ai-agent) to get started.
### Manually
Assign Ari to any thread yourself, exactly like assigning a user. Useful for testing on a specific conversation.
## 4. Test
Enable Ari in [Shadow Mode](/docs/product/agents/ari/shadow-mode) to see how it behaves on threads without yet giving it the permission to send messages to your customers.
And try out Ari in the [Playground](/docs/product/agents/ari/playground), to test specific scenarios and build trust.
This is a great time to tweak Ari's behavior via [Custom Instructions](/docs/product/agents/ari/custom-instructions), to better suit your support process and tone.
## 5. Go live
When you're happy with Ari, and have built trust with the agent, then switch the mode from Shadow to Live on the [Ari → Preferences](https://app.plain.com/~/ai/ari/preferences) page.
Ari starts replying to customers on the threads your workflows assign to it.
## How do I know it's working?
Head over to either of the following pages to see which threads Ari is working on:
* [Ari → Activity](https://app.plain.com/~/ai/activity)
* [Handled by Ari](https://app.plain.com/~/threads/handled-by-ari)
**Handled by Ari** lists the threads Ari is working on or has answered; turn on **Show done** to include closed ones. Handed off threads sit in your queue like any other. Moving a thread back to Todo counts as your team taking over; marking it Done or snoozing it doesn't.
To see what Ari could not answer, go to [**Ari → Knowledge → Knowledge gaps**](https://app.plain.com/~/ai/knowledge-gaps). Each open gap is a task for your team, and Sidekick can draft the missing article for you. See [Knowledge gaps](/docs/product/agents/knowledge-gaps).
## What can Ari see?
Each time a customer message arrives on a thread that Ari is assigned to, it assembles a fresh snapshot of the thread to use as context.
Ari uses a well defined set of inputs to build the context of a thread, then uses that context to decide what to do next on a thread and act independently.
See [Context](/docs/product/agents/ari/context) for a full breakdown of what is included.
## Custom instructions
Custom instructions let you tell Ari how to handle support for your workspace, in your own words.
Ari applies them at every step of handling a request: understanding the customer's message, searching your knowledge sources, and writing replies and handoff messages.
For example, you can have Ari:
1. Introduce itself in a specific way on the first reply of a conversation
2. Hand every billing question to your team, with a note explaining why
3. Ask for the exact error message before troubleshooting, then pick the conversation back up when the customer answers
4. Search using your documentation's terminology when customers use different words
See [Custom Instructions](/docs/product/agents/ari/custom-instructions) for examples and guidance on writing them.
# Citations
Source: https://www.plain.com/docs/product/agents/ari/citations
Ari shows which knowledge source each answer came from, for you and for your customers.
Ari shows the knowledge it used to answer, so you and your customers can see where a reply came from and trust it.
## Inline links
Ari links directly to the relevant documentation inside its reply to customers, including relevant links that further help the customer with answering their question.
Links only ever point to your **publicly accessible** Help Center articles and public documentation pages. Ari never links a private or internal Help Center article, even when it used that content to answer.
Ari keeps links to a minimum (at most two per reply) and adds one only when it is genuinely useful to the reader.
Inline links are **on by default**. You can turn them off for your workspace on the [Ari → Preferences](https://app.plain.com/~/ai/ari/preferences/) page.
## Which knowledge was used?
Each time Ari answers from your knowledge, it adds a collapsible timeline entry titled "Ari used 3 knowledge sources".
Citations are captured at the moment Ari replies, so the entry keeps showing what Ari used even if that source is later edited, reindexed, or deleted.
This is **on by default**. You can turn it off for your workspace on the same [Ari → Preferences](https://app.plain.com/~/ai/ari/preferences/) page.
# Context
Source: https://www.plain.com/docs/product/agents/ari/context
What Ari reads before it replies: the thread, the customer, and the knowledge you give it.
[Ari](/docs/product/agents/ari) uses a well defined set of inputs to build the context of a thread, then uses that context to decide what to do next on a thread and act independently.
## How?
Each time a customer message arrives on a thread that Ari is assigned to, it assembles a fresh snapshot of the thread.
## What?
The context snapshot always contains:
1. Conversation
2. Customer details
3. Channel (e.g. Slack)
4. Files the customer attached
5. [Tone of voice](/docs/product/agents/tone-of-voice) preferences
6. Thread labels
It can optionally also include:
* Thread fields
* [Knowledge sources](/docs/product/agents/knowledge-sources) it found via a search, if generating a reply
### Conversation
Ari reads the thread's message history, and includes:
* The most recent 50 message
* Who sent each message
* When each message was sent
* Long messages are truncated to the first 2,000 characters
* Quoted email history and signatures are stripped
* Autoresponder messages are ignored
### Notes
Ari **does not** use internal notes on a thread as context, as these are private to your internal team, and could include information about the customer that Ari should not use.
### Customer details
Ari is told the customer's **name** and their **company**, taken from the thread's [tenant](/docs/product/platform/tenants). If the thread has none, Ari uses the customer's tenant memberships instead, and if they match no tenant, Ari is told that too. Nothing else about the customer is shared.
Ari greets customers by first name, or not at all if none is set.
### Channel
Ari knows what channel the conversation is happening over, e.g. email or Slack, and adapts how replies are written:
* **Email** → Fuller, more structured replies
* **Chat** → Shorter, more conversational replies
* **Discord** → Kept under Discord's 2,000 character message limit
Ari replies in the customer's language.
You can further configure this via [Tone of voice](/docs/product/agents/tone-of-voice) preferences.
### Attachments
Ari can read files a customer attaches to their message, so a screenshot, log file, or document becomes part of the context for a thread.
Ari can read:
* **Images →** PNG, JPEG, GIF, WebP
* **Documents** **→** PDF, Word, RTF, OpenDocument, Pages
* **Spreadsheets** **→** Excel, CSV, TSV, Google Sheets
* **Presentations** **→** PowerPoint, Keynote, Google Slides
* **Text** **→** Plaintext, Markdown, HTML, source code, config files, emails
Limits:
* Only files the customer attaches are read, not files your team or Ari send
* Up to 10 files per request; anything beyond that is ignored
* Each file must be under 2.5 MB
* Audio, video, and archives (e.g. `.zip`) can't be read
* Ari only reads files uploaded directly to the conversation, it does not follow links to externally hosted files (Google Drive, Dropbox, etc)
### Thread labels
All labels, up to a limit of 25, are added to the context snapshot.
You can disable a label being included by disabling the `Can be applied by Plain AI` setting on a given label.
### Thread fields
You can optionally let Ari see the [Thread Fields](/docs/product/platform/threads/thread-fields) of a thread, for additional context.
This is controlled **per field** and is **off by default**. To enable this for a given field:
1. Go to [Settings → Thread Fields](https://app.plain.com/~/settings/thread-fields/)
2. Open a field
3. Enable `"Include in Agent context"`
Only the fields you explicitly enable are shared with Ari. We apply some filtering and limits:
* Fields are sorted alphabetically by label and capped at 25
* Values are shortened to 150 characters and descriptions to 200
* If we fail to load the thread fields, Ari continues without them
Thread fields can hold anything, including PII, so only enable this setting if you're comfortable with Ari using that context to answer customer questions.
### Knowledge
Along with the static context that is built in to the snapshot, Ari is also able to dynamically search for knowledge when generating an answer to a customer question.
Rather than reading all knowledge that is indexed, Ari searches for what's relevant based on the thread:
* Ari generates search queries based on the current snapshot of the thread
* Searches for documentation up to 3 times before drafting a reply
* Each search returns the top 10 matching pages, combined and deduplicated across searches
* The full content of each matching page is given to Ari to answer from
Two guarantees follow from this:
1. If the search finds nothing relevant, Ari hands the thread off to your team rather than guessing
2. Private or internal content can power an answer, but Ari never links a customer to it in the reply
See [Knowledge Sources](/docs/product/agents/knowledge-sources) for more information.
## Adding dynamic context
Allowing Ari to understand data that sits within your own systems as part of the context it uses when answering customer questions gives Ari much more to answer from.
Achieve this via [Thread Fields](/docs/product/agents/ari/context#thread-fields).
Populate one or more thread fields that are visible to Ari with dynamic context, using:
* Plain's [GraphQL API](/docs/graphql/introduction)
* [Webhooks](/docs/webhooks)
* [Workflows](/docs/product/workflows)
* Manual
# Custom instructions
Source: https://www.plain.com/docs/product/agents/ari/custom-instructions
Adjust how Ari behaves with instructions of your own, on top of its default behavior.
Custom instructions allow you to customize the base Ari agent, so that it is more contextually aware of how to operate in your Plain workspace, and how to provide support to your customers.
## Getting started
1. Read this documentation about the proper use and limitations of this feature
2. Go to [Ari → Preferences](https://app.plain.com/~/ai/ari/preferences)
3. Write your instructions in the **Custom Instructions** field, and save. Instructions can be up to 20,000 characters
4. Ari applies the instructions from the next customer message it handles, on both new and existing threads
A good way to build confidence is to write and configure your instructions while Ari is in [Shadow Mode](/docs/product/agents/ari/shadow-mode). Allowing you to see how they change Ari's behavior before sending customer replies.
## How Ari uses them
Ari applies the instructions are specific steps when it is operating:
1. Classifying what action to take next on a thread
2. Searching your knowledge sources
3. Writing a reply and handoff message
The instructions are one block of text, but Ari applies only the parts relevant to what it is doing at each step. Mixing tone, routing, and terminology guidance in one place is fine.
Instructions never override Ari's built in guardrails. Ari only states product facts your knowledge sources support, so instructions shape how it behaves, not what it knows.
## Writing good instructions
* **Phrase preferences, not bans.**
* "Prefer app instructions where the documentation covers them" works well. "Never show API examples" can leave Ari unable to give any documented answer, so it hands off instead.
* **Facts belong in knowledge sources.**
* Ari will not state product claims that only appear in instructions. Use instructions for behavior, knowledge sources for truth.
* **Keep exact wording short.**
* Sign-offs, introductions, and handoff promises are followed most reliably when they are a sentence, not a paragraph.
## Examples
### Company context
Tell Ari who it works for, so that it is contextually aware of your business, mission, customers, and products.
```markdown theme={null}
You're an AI agent that works as part of the customer support team at Plain.
Plain provides AI first support infrastructure for B2B teams to consolidate
channels, orchestrate AI workflows, and build extensible, API first support
systems, that enable teams to automate triage, surface insights, and treat
support as a queryable data layer rather than just a ticketing tool.
```
### Custom introduction
Instruct Ari to introduce itself in a specific way when first talking to a customer.
```plaintext theme={null}
On your first reply in a conversation, introduce yourself briefly as Plain's
AI support assistant and mention that a human colleague is available if required.
Do not repeat the introduction in later replies.
```
### Custom handoff
Instruct Ari to include specific wording when handing off to a user, to ensure expectations are set correctly.
```plaintext theme={null}
When handing a conversation off to a human, let the customer know the team
typically responds within 2 hours on business days (9am to 6pm UK time), and
thank them for their patience.
```
### Escalation preferences
Customize which types of threads are routing away from Ari, even if Ari could answer from your knowledge sources.
```plaintext theme={null}
Questions about pricing, billing, invoices, or plan changes must always be
handled by a human; do not answer them yourself, hand the conversation off
instead.
```
### Clarifying questions
Ari asks for the specifics, waits, and picks the conversation back up when the customer answers. The second sentence matters: it tells Ari the question is the reply, not a reason to stop working.
```plaintext theme={null}
When a customer reports something broken or not working without including the
exact error message or what they were doing at the time, reply asking them for
those specifics instead of guessing at a fix. Still search the documentation as
normal, the question is your reply to the customer, not a reason to skip
searching.
```
### Terminology mapping
Bridge the words your customers use and the words your documentation uses, so searches land even when they do not match.
```plaintext theme={null}
Our customers often say "ticket" or "case" for what Plain and its documentation
call a "thread", and "close" for marking a thread as Done. Use the documentation
terminology when searching, and gently use the correct terms in your replies.
```
### Debugging before handoff
Have Ari collect what your team needs inside the handoff message, so the answer is waiting on the thread when they pick it up.
```plaintext theme={null}
Refund requests must always go to a human. When you hand off a billing or refund
conversation, ask the customer for their invoice number and the billing email on
the account in your handoff message, if they have not already provided them, so
the team can investigate faster.
```
### Documentation preferences
Bias which documented approach Ari leads with when your knowledge sources cover several.
```plaintext theme={null}
Our customers integrate directly against the GraphQL API and do not use the
TypeScript SDK. When the documentation shows both a TypeScript SDK example and a
raw GraphQL query or mutation, always show the raw GraphQL version.
```
### Footer
Instruct Ari to include a footer when sending a reply.
```plaintext theme={null}
End every reply with the sign-off "Best, the Plain support team" on its own line.
```
### Tone
Customize the tone and feel of Ari, this can also be done via [Tone of Voice](/docs/product/agents/tone-of-voice) preferences.
```plaintext theme={null}
Write in British English (for example organise, colour, whilst). Keep replies
warm and conversational, use contractions, keep them to a few short sentences
where possible, and address the customer by their first name.
```
### Deflect actions to MCP
Ari currently is unable to carry out actions, so instead of Ari handing off in a generic manner, you can instruct Ari to push your customers to use your MCP server instead.
```plaintext theme={null}
If a customer asks you to perform an action, if you are unable to carry it out,
let them know you can't carry out the action, explain your purpose as an agent,
and guide the customer to use our MCP instead.
```
## Limitations
There are known limitations to how Ari can use custom instructions. Keep these in mind, and contact the Plain team if you need help.
1. Override Ari's guardrails
2. Add links to replies, as Ari can only link to public knowledge sources
3. Hold a conversation open waiting for information before handing off; Ari asks in the handoff message instead
4. Guarantee which knowledge sources Ari searches; instructions steer wording, while knowledge source settings control access
# Handoffs
Source: https://www.plain.com/docs/product/agents/ari/handoffs
What Ari does when it cannot help: how it hands the thread to your team, and when.
When Ari can't or shouldn't help, it hands the thread to your team.
## What triggers a handoff
* The customer asks for a human, or to escalate
* The customer is frustrated or angry
* The request is about sales, billing, a security issue, or an incident
* Ari's search finds no relevant knowledge, or its draft doesn't pass its own quality check. This is the only handoff that records a [knowledge gap](/docs/product/agents/knowledge-gaps)
* A customer @-mentions a user on a Slack thread, unless you [turn that off](#when-a-customer-tags-a-user)
* Ari has nothing new to add, e.g. the customer repeats an answered question. It leaves an internal note and records no knowledge gap
* Anything you've told Ari to handoff via Custom Instructions
Vendor sales pitches are the exception: no reply, Ari unassigns itself.
## What happens mechanically
Ari genuinely transfers ownership. It sends a short handoff message to the customer (you can turn this off in preferences), unassigns itself, and never continues helping after handing off.
Security issues and incidents are also set to **Urgent**.
If the handoff moves the thread to needs first response, the [SLA](/docs/product/platform/slas) clock restarts from the customer's last message. Ari's handoff message doesn't count as a reply.
## Clarifying questions
If a question is ambiguous and your docs answer it differently depending on the detail, Ari asks one clarifying question instead of handing off. It never re-asks something the customer has already said.
## When a customer tags a user
Some customers @-mention a named contact on every Slack message out of habit, so Ari never gets to answer. To keep Ari working on those threads, turn off **Hand off when a teammate is tagged** on the [Ari → Preferences](https://app.plain.com/~/ai/ari/preferences) page. The other handoff triggers still apply.
## Customizing the handoff
The handoff message is written by Ari each time; there's no template to edit. To shape it, use [Custom Instructions](/docs/product/agents/ari/custom-instructions) to set expectations ("the team replies within 2 hours"), have Ari collect details your team needs, or force specific topics to always go to a human.
## Continue work after handoff
Use the `Ari unassigned` workflow trigger to start a workflow when Ari handsoff, for example:
1. Have the workflow post to Slack so that you know about the handoff
2. Kick off a Sidekick investigation
3. Automatically assign someone else from the team
Use this [Workflow Template](https://app.plain.com/~/workflows/gallery/investigate-unassigned-threads) to get started.
# Playground
Source: https://www.plain.com/docs/product/agents/ari/playground
Test Ari against your real workspace data in a private chat that leaves no trace.
The **Ari Playground** is a private and ephemeral chat for testing Ari against your workspace's real knowledge and settings, without touching a live customer thread.
Every turn runs the production agent, and actions that the agent takes are displayed as traces in the chat, to help you better understand how the agent works and how it arrived at its action.
Go to **AI → Ari →** [**Playground**](https://app.plain.com/~/ai/ari/playground) to use the feature.
## Uses
Use the playground to:
1. Try out Ari to better get a feel for the agent
2. Understand how the agent operates and makes decisions
3. Understand how the agent searches for and uses [Knowledge Sources](/docs/product/agents/knowledge-sources)
4. Build trust in Ari before enabling it
5. Testing
6. Debugging an issue seen out in the wild when Ari was replying to customers
## Reporting issues
Copy the session ID (top right) and share it with the Plain team. They can use this ID to understand the full trace of every turn in the agent session.
## FAQ
* Conversations are ephemeral: refresh, reset, or leaving clears everything by design
* Each session is limited to 40 messages per session
# Shadow mode
Source: https://www.plain.com/docs/product/agents/ari/shadow-mode
Watch how Ari would answer without it replying to any customer.
Shadow mode lets you see how Ari would handle your support before it replies to any customer.
Ari runs on your real threads but sends nothing. Instead it adds a timeline entry showing what it would have done: the reply it would have sent, or why it would have handed off to your team.
Your team keeps handling every conversation the same way they do now. Ari doesn't message the customer or change the thread, and customers never see these entires from Ari. This lets you check Ari's behavior, and build trust, on your own threads without affecting the customer experience.
New AI agents start in shadow mode. Ari only starts replying to customers once you take it live.
## What you'll see
While Shadow mode is on, Ari adds private timeline entries when new customer messages are added to thread. Each entry says one of:
1. **Would have replied** → The draft answer Ari would have sent
2. **Would have handed off** → When Ari would have passed the thread to your team, and why
3. **No response needed** → Ari decided nothing needed a reply
These entries are internal, so customers never see them.
Once Ari would have handed a thread off to your team, it stops adding entries to that thread, the same as when Ari is operating in live mode.
## Knowledge gaps in shadow mode
When Ari would have handed off because it couldn't find an answer in your knowledge sources, it records a [knowledge gap](/docs/product/agents/knowledge-gaps) exactly as it does live. The gap is grouped with repeat questions and raises a task for your team, so you can fill the holes in your documentation before Ari starts replying to customers.
The shadow mode entry on the thread doesn't mention the gap. Find it under [**Ari → Knowledge → Knowledge gaps**](https://app.plain.com/~/ai/knowledge-gaps) or as a task linked to the thread.
## Which threads Ari shadows
Ari shadows the same threads it would work on when live. A thread comes to Ari in one of two ways:
* **Automatically via workflows**
* The assignment workflow you set up for Ari decide which threads it picks up, so you can limit Shadow mode to specific tiers, labels, or channels
* If no workflow assigns threads to Ari, Shadow mode has nothing to watch
* **Manually**
* You can assign Ari to any thread yourself to see how it would handle that specific conversation
Either way, Ari runs alongside your team without interfering with your assignments.
It's added as an additional assignee rather than taking the thread over, so your queues, workflows, and assignment rules stay intact and whoever was already handling the thread keeps handling it.
## Enabling shadow mode
You switch between Shadow mode and live on the [Plain AI → Ari → Preferences](https://app.plain.com/~/ai/ari/preferences/) page.
When you're happy with Ari's drafts, turn Shadow mode off there to take it live. Ari then replies to customers on the threads assigned to it.
## Reading via GQL
In shadow mode Ari writes its outcome to the thread's timeline as a [Thread Event](/docs/graphql/events). Read it like any other timeline entry, filtering to events:
```graphql theme={null}
query ($threadId: ID!) {
thread(threadId: $threadId) {
timelineEntries(filters: { entryTypes: [THREAD_EVENT] }, first: 50) {
edges { node { entry { ... on ThreadEventEntry {
externalId
title
components { ... on ComponentContainer { containerContent {
... on ComponentText { text }
} } }
} } } }
}
}
}
```
Shadow mode events are identified by their `externalId`, which starts with `ari-dry-run`.
When Ari would have answered, the drafted reply is the `ComponentText` inside the event's `ComponentContainer`.
# Suggested responses
Source: https://www.plain.com/docs/product/agents/ari/suggested-responses
Put Ari's draft replies in the composer for a user to review, edit, and send.
**Suggested responses** surface **Ari** generated draft replies directly in the composer using your knowledge sources and the full thread context.
**Suggested responses are only available on the Horizon or Frontier plans.**
## How it works
When a new thread comes in, Plain generates a suggested reply if there is enough information in your knowledge sources and the full thread context to provide the answer. The suggestion appears directly in the composer, ready for review, edits, or sending.
The more knowledge sources you connect, the better suggestions become. This is especially effective for teams handling technical, policy heavy, or product specific questions where accuracy matters.
If Plains AI doesn't have enough information, a reply won't be generated. This helps ensure that suggested replies are always useful.
## Enabling suggested responses
1. Go to **Settings → Plain AI**
2. Toggle **Suggested responses**
Once enabled, suggestions will appear automatically in the composer whenever they're available.
Team members who prefer not to see suggested responses can toggle their visibility using the icon button directly in the composer.
## Make suggestions smarter with knowledge sources
To provide deeper and more accurate suggestions, connect your support documentation. Plain indexes it and use it alongside past conversations to generate better replies.
This matters most for B2B teams handling technical, policy-heavy, or product-specific questions. See [knowledge sources](/docs/product/agents/knowledge-sources) to get started.
You can rate any suggested response with a thumbs up or thumbs down directly in the composer. You can also leave an optional comment explaining why the suggestion was or wasn't helpful. This feedback is used to improve future suggestions.
## Bring your own suggestions
If you generate replies elsewhere, for example with your own model or an internal knowledge base, you can push them into Plain with the `addGeneratedReply` API mutation.
They appear in the composer, and the customer sees nothing until a user sends the reply.
Once your agent has suggested a reply on a thread, Ari stops generating its own suggestions there.
Your suggestions show even when **Suggested responses** is off, and take precedence over Ari's for the same message.
Your API key needs the `generatedReply:create` permission. See [suggested replies](/docs/graphql/messaging/suggested-replies) in our API docs.
# Typing indicator
Source: https://www.plain.com/docs/product/agents/ari/typing-indicator
Show customers that Ari is composing a reply in the chat widget.
If you are using [Ari](/docs/product/agents/ari) with Plain's [Chat widget](/docs/product/channels/chat), then you can configure Ari to show status and typing indicators to your customers.
## Enable setting
1. Go to [Settings → Chat](https://app.plain.com/~/settings/chat) and open on configuration for a given chat app.
2. Scroll down to the `Show agent status to users`
3. Enable toggle
Your customers will now be shown when Ari is working on a thread.
# Bring your own agent
Source: https://www.plain.com/docs/product/agents/bring-your-own-agent
Build your own agent to handle threads autonomously or to assist your team from inside Plain.
Build your own agent and run it inside Plain, either to handle threads on its own or to assist your team. Plain gives you the APIs and the surfaces in the app; you build the agent.
Because Plain is API-first, your agent can do anything a user can do without restrictions.
Depending on your requirements, building your own agent has some advantages over using Plain's built-in agents:
* You control the harness, model, architecture, prompts, context, and behavior.
* You can give it access to internal systems you would not grant a third party.
* You can reuse an agent your team already runs elsewhere, such as in Slack.
* You control what it spends on model calls.
If you'd rather not build one, [Ari](/docs/product/agents/ari) and [Sidekick](/docs/product/agents/sidekick) ship as part of Plain, and several [third-party agents](/docs/product/agents/third-party) are also available.
## Example agent types
Your agent can work threads, reply to customers, and help the users on your team. What it should and shouldn't do is up to you. These three shapes are the most common.
### Internal assistant agent
Talk to an agent from any page in Plain: click **Ask Sidekick**, then pick your agent instead of Sidekick. From there it can answer a thread, work through a task, update your [Help Center](/docs/product/help-center) or take other actions in Plain.
An internal agent normally doesn't write to customers directly, allowing you to give it access to sensitive tools and data sources.
### Support agent
A support agent handles threads end to end. Assign it to a subset or all of your threads with a [workflow](/docs/product/workflows), and it replies to the customer the way a user on your team would.
Because it writes to customers, a support agent normally has narrower access to internal tools and instead relies on your own documentation as the source of its answers.
### Triage agent
A triage agent investigates every new thread before a user opens it. It sets the thread's priority and posts an internal note with what it found, so your team picks the thread up with the work already started.
## Build your agent
Learn about how to bring your own agent to Plain in our [technical documentation](/docs/agents).
# Knowledge gaps
Source: https://www.plain.com/docs/product/agents/knowledge-gaps
See the questions Ari could not answer, and fill the gaps in your knowledge with Sidekick.
A knowledge gap is a question customers ask that your [knowledge sources](/docs/product/agents/knowledge-sources) don't answer. When [Ari](/docs/product/agents/ari) can't answer, Plain records the question, groups repeat questions into one gap, and raises a [task](/docs/product/platform/tasks) so your team knows what to document next. Gaps live under [**Ari → Knowledge → Knowledge gaps**](https://app.plain.com/~/ai/knowledge-gaps).
Knowledge gaps are in beta. The behavior described here, and the API behind it, may change without a deprecation period.
## How a gap is detected
1. **Ari can't answer.** Ari searches your knowledge sources for an answer to the customer's message. If it can't find one it trusts, it [hands the thread to your team](/docs/product/agents/ari/handoffs) and records a signal. Other handoffs, such as a customer asking for a human or a billing question, don't record a signal. This happens in [shadow mode](/docs/product/agents/ari/shadow-mode) too, so you can fill gaps before Ari goes live.
2. **Plain checks it's a real gap.** Only general product questions your docs should answer become gaps. Requests for Ari to act, questions about a customer's own account, and topics outside your product are dropped.
3. **Plain groups it.** The question joins an existing gap that covers the same topic, or opens a new one with a generated title and description. One gap per missing topic, however many customers ask.
4. **Plain raises a task.** A new gap creates one unassigned task, titled **Knowledge gap: …**. Later threads with the same question link to that task and update the gap's **Last seen** time.
## Reviewing gaps
Go to [**Ari → Knowledge → Knowledge gaps**](https://app.plain.com/~/ai/knowledge-gaps). The list shows each gap's task, title, number of threads, and when it was last seen, sorted by thread count so the most common questions come first. Switch between **Todo**, **Done**, and **Cancelled** to see open, filled, and dismissed gaps. A gap's status is its task's status: mark the task done to close the gap, and Plain reopens it if the same question comes back.
Click a gap to see its description, its task, and the threads where customers asked it. Reading 2 or 3 of those threads tells you how customers phrase the question, which is what the article needs to answer.
The task also appears on the [Tasks](/docs/product/platform/tasks) page and under **Tasks** in each linked thread's sidebar. Open the task and the gap is shown under **Knowledge gap**.
## Filling a gap with Sidekick
Sidekick ships with a built-in [skill](/docs/product/agents/sidekick/skills), **/fill-knowledge-gap**, that researches a gap and drafts the missing article. To start it:
* Open the gap's task and click **Handle with Sidekick**
* Type **/fill-knowledge-gap** in any Sidekick session, then name the gap or task, or paste the gap's link
Sidekick checks in with you at each decision:
Sidekick reads the gap and its linked threads, searches your knowledge sources and existing Help Center articles, and finds similar past threads to see how your team answered before. If you've connected a documentation tool to Sidekick, such as Notion or a custom MCP server for your docs platform, it reads that too when your Plain knowledge doesn't settle the answer. It strips customer details from everything it reads and never fetches arbitrary web pages. See [Tools and integrations](/docs/product/agents/sidekick/integrations) for what you can connect.
Sidekick writes a new standalone article, 200 to 300 words, followed by a **Sources used** list so you can check every claim. If the evidence is thin or contradictory, it asks you for the missing facts.
Sidekick asks whether to save the draft. If you agree, it picks a Help Center (preferring a private or internal one) and an article group, shows you the proposed write, and waits for your approval. It saves the article as a **Draft** for you to review and publish. It always creates a new article and never edits an existing one.
Sidekick offers to mark the gap's task done. Say yes and the gap moves to **Done**.
The session stays open after the skill finishes, so you can keep working with Sidekick:
* **Refine the draft.** Ask for a different tone, a shorter version, a code example, or a version for a different audience.
* **Pull in more context.** If you've connected integrations or [custom MCP servers](/docs/product/agents/sidekick/integrations#custom-mcp-servers), ask Sidekick to check them: your internal docs, an engineering runbook, the source code, or product analytics. It folds what it finds into the draft.
* **Save it outside Plain.** If your docs live in Notion, Mintlify, ReadMe, or another connected tool, ask Sidekick to write the page there. The gap becomes a page in your real docs without leaving the session, and Ari indexes it through your [knowledge sources](/docs/product/agents/knowledge-sources) on the next reindex. Writes to connected tools follow the usual [approval rules](/docs/product/agents/sidekick/actions-and-approvals).
* **Reply to the customers who asked.** Ask Sidekick to draft replies to the threads that raised the gap, now that the answer exists.
## API and webhooks
Everything on this page is available over the API:
* [Knowledge gaps API](/docs/graphql/knowledge-gaps): list gaps sorted by thread count or recency, filter by status, and read the task behind each one
* [Tasks API](/docs/graphql/tasks): a task raised for a gap has a `sourceLinks` entry with `sourceType: "knowledge_gap"`, which points at the gap
* [Task webhooks](/docs/webhooks/task-created): subscribe to task created, updated, and status transitioned events to react when Ari raises or your team closes a gap
# Knowledge sources
Source: https://www.plain.com/docs/product/agents/knowledge-sources
Feed docs, sitemaps, and Help Center articles to Ari and Help Center AI, and keep them indexed.
Knowledge sources are the documents Plain indexes so that Ari and Ask AI can answer from them. Add a sitemap, individual URLs, or your Help Center, and Plain keeps them indexed.
Knowledge sources are the pages and docs you feed to **Ari** and Help Center AI. The more Plain knows about your product, the better its answers.
This guide covers adding sources, how indexing works, keeping them fresh, and doing it all from the API and CLI.
## Overview
By default, Plain AI already reads everything in your **customer-facing Help Centers**, so publishing clear Help Center articles feeds Ari for free.
Knowledge sources let you add content that isn't in Plain. You manage them under [**Settings → Plain AI → Knowledge Sources**](https://app.plain.com/~/ai/knowledge-sources).
Plain AI must be enabled on your workspace to add or index sources.
## Source types
When you add a source, you pick one of two types:
1. **Sitemap** indexes a whole site. You point Plain at your `sitemap.xml` and it crawls every URL listed, one indexed page each. Pages that later drop out of the sitemap are removed on the next reindex. Prefer this whenever you can, it keeps Plain in sync as pages come and go.
2. **URL** indexes a single page. Use it for a standalone guide or anything not covered by a sitemap.
Most sites serve their sitemap at `https://yourdomain.com/sitemap.xml`. Only normal HTML or Markdown pages can be indexed, files like `.pdf` are skipped, and pages behind a login can't be crawled unless they support [token access](#authenticated-sources).
Pages over 1 MB, or with under 100 characters of text once navigation and footers are stripped, are marked failed.
When you add a sitemap, Plain fetches it first and rejects a URL that lists no pages, such as `robots.txt` or a docs homepage.
## Adding a source
1. Go to [**Settings → Plain AI → Knowledge Sources**](https://app.plain.com/~/ai/knowledge-sources)
2. Choose **Sitemap** or **URL** and paste the address
3. Save, the source indexes automatically
A large sitemap can take a while to finish, since each page is fetched and indexed on its own. You can keep working while it runs.
## Statuses
Each source, and each page within a sitemap, shows one of three statuses:
* **Pending** → Plain is fetching and indexing the content
* **Indexed** → Ready, and Plain AI can use it in replies
* **Failed** → Something went wrong, with the reason (e.g. the HTTP status, or a URL that didn't return XML); fix the cause and reindex
A health summary at the top of the page shows how much of each source is indexed and flags any failures. A sitemap's pages are grouped by status, failures first.
## What knowledge was used?
You don't have to guess which of your sources Ari drew on. On every reply, Ari cites the knowledge it used, shown as a collapsible list on the thread timeline and, where useful, as inline links in the reply itself.
See [Ari → Citations](/docs/product/agents/ari/citations) for the full behavior.
One thing to know when curating sources: Ari only ever links **publicly accessible** pages in a reply. A private or internal Help Center can still power Ari's answers, but its articles are never shown to a customer as a clickable link.
## What knowledge was missing?
When Ari can't answer a customer from your sources, Plain records the question as a [knowledge gap](/docs/product/agents/knowledge-gaps) and raises a task to fill it. Go to [**Ari → Knowledge → Knowledge gaps**](https://app.plain.com/~/ai/knowledge-gaps) to see which topics to document next, sorted by how many threads asked about them.
## Keeping content fresh
Content is reindexed automatically every week, on Monday mornings (Europe/London), so ordinary doc changes get picked up on their own.
To refresh sooner, you have three options:
1. Reindex a single source on demand with the `reindexKnowledgeSource` API call
2. Use the `@team-plain/cli` to reindex
3. Manually trigger a reindex for a source in the Plain UI
Reindexing only ingests content that changed, so frequent reindexing is cheap and safe.
## Using the GraphQL API
Everything in the UI is available over Plain's [GraphQL API](/docs/graphql/introduction).
### Reindex a source
The precise way to refresh one source in place. Requires the `knowledgeSource:create` permission.
```graphql theme={null}
mutation ReindexKnowledgeSource($input: ReindexKnowledgeSourceInput!) {
reindexKnowledgeSource(input: $input) {
knowledgeSource { __typename }
error { message code }
}
}
# variables
{ "input": { "knowledgeSourceId": "..." } }
```
### Add a source
Adds a source and queues it for indexing. Requires the `knowledgeSource:create` permission.
```graphql theme={null}
mutation CreateKnowledgeSource($input: CreateKnowledgeSourceInput!) {
createKnowledgeSource(input: $input) {
knowledgeSource { __typename }
error { message code }
}
}
# variables — type is "SITEMAP" or "URL"
{ "input": { "url": "https://docs.yourcompany.com/sitemap.xml", "type": "SITEMAP" } }
```
### List sources and status
Find source IDs and read the failure reason on any that failed. Requires the `knowledgeSource:read` permission.
```graphql theme={null}
query KnowledgeSources {
knowledgeSources(first: 50) {
edges { node {
__typename
... on KnowledgeSourceSitemap {
id url
status { __typename ... on IndexingStatusFailed { reason } }
}
... on KnowledgeSourceUrl { id url status { __typename } }
} }
}
}
```
### Delete a source
Removes the source and its content from Plain AI's index. Requires the `knowledgeSource:delete` permission.
```graphql theme={null}
mutation DeleteKnowledgeSource($input: DeleteKnowledgeSourceInput!) {
deleteKnowledgeSource(input: $input) { error { message code } }
}
# variables
{ "input": { "knowledgeSourceId": "..." } }
```
## Via the Plain CLI
The `@team-plain/cli` package wraps the API for scripting. It reads your API key from `PLAIN_API_KEY`.
```bash theme={null}
npm install -g @team-plain/cli
export PLAIN_API_KEY=plainApiKey_xxx
# Index every URL in a sitemap
plain index-sitemap https://docs.yourcompany.com/sitemap.xml
# Index a single page
plain index-url https://docs.yourcompany.com/guide
```
## Authenticated sources
Plain can index docs behind a login that accept a JWT on the URL, such as GitBook's Visitor Authentication. Generate a JWT on your side and append it to the sitemap URL, under any parameter name:
```plaintext theme={null}
https://docs.yourcompany.com/sitemap-pages.xml?jwt_token=your_jwt_here
```
The token is passed to every page in the sitemap on the same domain, and never to other domains.
Plain won't automatically reindex these, since the token expires. When it's about to, delete and recreate the source with a fresh JWT using the API.
## Markdown support
Some documentation platforms (like Mintlify and ReadMe) publish a clean Markdown version of each page alongside the rendered HTML. When indexing any page, Plain first looks for its Markdown version first, the same URL with a `.md` suffix, and uses it when one exists, falling back to the HTML page otherwise.
Markdown gives Plain AI cleaner text and better answers. It's fully automatic, there's nothing to configure, and pages without a Markdown version index exactly as before
## Best practices
* Prefer a sitemap over individual URLs, it stays in sync as pages change
* Index accurate, current docs only; stale or contradictory pages make answers worse
* Keep your sitemap honest, removing a page there retires it from Plain AI
* If your docs change frequently, reindex from CI on deploy rather than waiting for the weekly run
## Troubleshooting
### Failed source ingestion
Check the failure reason on the status. Common causes:
* The page couldn't be fetched, it's unreachable, errors, or blocks crawlers
* The URL points at a `.pdf` or other unsupported file; only HTML and Markdown pages can be indexed
* The sitemap is unreachable or isn't valid XML
* The page needs authentication
* The page is too large; pages over 1 MB of raw HTML or Markdown are skipped and marked as failed
Fix the cause, then reindex to clear the failure.
### Ari isn't using a source
* Confirm Plain AI is enabled on the workspace
* Confirm the source is **Indexed**, not pending or failed
* For Help Center content, confirm the article is published and its Help Center has customer-facing AI setting enabled
# Getting started with Sidekick
Source: https://www.plain.com/docs/product/agents/sidekick
Sidekick is your team's AI assistant inside Plain. Set it up and start asking it questions.
Sidekick is your team's AI assistant, built directly into Plain. It knows your product, your customers, and your stack, and it can act across all of them while your team stays in control.
You can ask Sidekick to investigate an error, catch you up on an account, triage a new thread, draft a reply, or run a report. It pulls the context first, then reasons, so it starts working before anyone has finished reading the thread.
A Sidekick conversation can be answered by an agent you built instead of by Plain's Sidekick. Your team picks the agent when they start the conversation. See [bring your own agent](/docs/product/agents/bring-your-own-agent).
## How Sidekick works
Sidekick's capabilities build on each other:
1. **It reads everything that matters** before it answers: the full thread, attachments, the customer's history, similar past threads, and your knowledge sources.
2. **It reaches into your tools.** Connect the tools your team already uses and Sidekick can look things up across all of them (see Tools and integrations).
3. **It takes action, with approvals.** Low-risk actions run automatically; anything irreversible waits for your explicit approval (see Actions and approvals).
4. **It can work on its own.** Workflows can start a Sidekick session automatically, on a trigger or on a schedule (see Sidekick in workflows).
## Where you use Sidekick
### Home
Home is Sidekick's own space, in your workspace's navigation bar. It's where you use Sidekick at the workspace level, across your whole queue, rather than on one thread.
Open Home and you get a greeting, a set of example prompts that rotate to show what Sidekick can do, and a composer. Ask something like *"What needs my attention right now?"* and Sidekick pulls SLA breaches, the threads that have waited longest, and anything that escalated overnight, across your entire queue.
Once you send your first message, the page moves into a conversation. Past sessions live in the sidebar and in a shelf at the bottom of the screen, so you can reopen one in place or detach it into a floating panel next to a thread. Sessions started anywhere, from a thread, a keyboard shortcut, or a workflow, all show up here.
The connect-tools area on Home shows exactly which tools are connected and prompts you to fill the gaps. The more you connect, the more Sidekick can see.
### In a thread
Open Sidekick from any thread by pressing `B` or clicking **Ask Sidekick** at the bottom of the screen. A banner above the composer shows the context Sidekick has, in this case the single thread you're viewing. Ask it to investigate, summarize, draft a reply, or act on the thread.
### On a company or tenant page
Sidekick can read a customer's full support history from their Company or Tenant page. See Sidekick on companies and tenants.
### In Slack
Your whole team can use Sidekick from Slack by @mentioning it in a connected channel, with no Plain seat required.
## What Sidekick can see
Every time you open a thread, Sidekick already has context. Without being asked, it reads:
* The full thread timeline: every message, internal note, status change, label, assignment, SLA transition, and any custom fields your team uses
* Attachments, including their contents
* The customer's profile, their company and tier, and every other thread they've opened
* Similar past threads, found semantically, even when the wording is completely different
* Your Help Center and any indexed knowledge sources
## How to enable Sidekick
Sidekick is available on all plans with Plain AI enabled. A workspace Admin or Owner enables it in **Plain AI → Sidekick**.
Each plan includes a monthly credit allowance, and Sidekick usage draws from it. See Credits for details.
## Who can use Sidekick
By default, Sidekick is available to all roles in your workspace, including Viewers. A workspace Admin or Owner can restrict this in **Sidekick → Settings**.
* **Viewer access on (default):** Everyone, including Viewers, can use Sidekick inside Plain and via Slack (by @mentioning it in a connected channel).
* **Viewer access off:** Only Support users and above can use Sidekick, both in Plain and in Slack.
## Your sessions
Your Sidekick sessions are private to you. They're saved across threads and pages, and you can pick up where you left off from the sessions shelf at the bottom of the app. As Sidekick works, you can watch its tool calls on screen in real time: what it's searching, what it's checking, and what it found.
# Actions and approvals
Source: https://www.plain.com/docs/product/agents/sidekick/actions-and-approvals
Let Sidekick act in Plain and your connected tools, with approval rules you control.
Sidekick can take real actions in Plain and in your connected tools: assign a thread, add a label, create a Linear issue, post a Slack message, update a HubSpot record. Some actions run immediately; anything higher-stakes or hard to reverse waits for your explicit approval first.
## How approvals work
Every action Sidekick can take is classified as either **runs directly** or **requires approval**, based on how reversible and how high-impact it is.
When an action requires approval:
1. You ask Sidekick to do something
2. Sidekick presents the proposed action with **confirm** and **deny** controls
3. You approve or deny
4. If you approve, Sidekick carries it out and reports the outcome back in the conversation. If you deny, it stops and you can redirect.
Sidekick never takes an irreversible action on its own. It proposes, drafts, and prepares, and you stay in control of anything that matters.
You can also approve or deny requests from Slack, without switching to Plain, if Sidekick in Slack is enabled.
An agent you built yourself uses the same cards. It decides which of its own actions to put behind one, rather than reading the classification below, and the approve and deny controls behave identically. See [tool calls](/docs/agents/tool-calls).
## Actions in Plain
### Run directly
These lower-impact actions execute immediately:
* Assign or unassign a thread
* Add or remove labels
* Change a thread's priority
* Rename a thread
* Set or update thread fields
* Add an internal note (visible to your team only, never to the customer)
* Link a thread to a Linear or Jira issue
* Draft, update, or publish Help Center articles
### Require approval
These higher-impact or harder-to-reverse actions show a confirm step first:
* Change a thread's status (for example, marking it done)
* Escalate a thread
* Merge two threads
* Lock a thread
* Mark a customer as spam
* Change a company or tenant's tier
## Actions in connected tools
What Sidekick can do in each tool depends on the integration and the scopes it was connected with. Some tools may need re-authorisation with write permissions before actions are available.
| **Integration** | **Runs directly** | **Requires approval** |
| ------------------------------------- | -------------------------------------------------- | ----------------------------------------------------------------------- |
| **Linear** | Add a comment; link a Plain thread to an issue | Create or update an issue; change an issue's state |
| **Jira** | Add a comment | Create or update an issue (assignee, labels, priority, status) |
| [**incident.io**](http://incident.io) | Create an action item | Create an incident |
| **Sentry** | Add a comment | Update an issue (resolve, ignore, or assign) |
| **Notion** | Add a comment | Create, update, or move a page; create or update a database |
| **HubSpot** | Create a note or task | Create a ticket; update a record; create an association between records |
| **Attio** | Create a note, task, or comment | Update a record; move it to a different pipeline stage |
| **Slack** | Post or update a message; add or remove a reaction | None |
Observability integrations (Datadog, Grafana, Sentry, PostHog, LaunchDarkly) are read-only. Sidekick reads from them but cannot change anything in them.
## Examples
**Escalate to engineering.** Ask Sidekick to escalate a thread and it creates a linked Linear or Jira issue, pre-filled with context from the thread, the logs, and the error data, then waits for your approval before creating it.
**Investigate an error.** Ask *"trace this error"* and Sidekick searches your logs and spans, cross-references Sentry, reads the alert channel in Slack, and names the likely root cause. Reading is always automatic; only the follow-up write actions ask for approval.
# Sidekick in Slack
Source: https://www.plain.com/docs/product/agents/sidekick/in-slack
Mention Sidekick in a Slack channel and get answers without a Plain seat.
Sidekick can live in a Slack channel. Anyone on your team can @mention it, with no Plain seat, no login, and no browser tab. The channel becomes a shared interface to your entire toolstack.
## How to enable it
You need to be a workspace **Admin** or **Owner**.
1. Go to **Sidekick → Integrations → Slack**
2. Click **Add to Slack** and complete the authorisation flow
3. Plain automatically creates an `#ask-plain` channel where your team can start straight away
4. To add Sidekick to another channel, invite the Plain bot to it in Slack, then set that channel's type to **Discussion** in **Settings → Slack**
Sidekick is active in every discussion channel and never in a customer channel, so the channel type is what controls where it responds.
## Who can use it
Anyone whose email matches your workspace domain can @mention Sidekick in Slack. They don't need a Plain account. Users with a Plain account get the same Sidekick they know from the app; everyone else on your domain can use it directly from Slack without logging in.
Sidekick responds in internal channels only. Slack Connect channels and DMs are not supported.
## How to use it
@mention Plain in any channel Sidekick has been added to and ask your question in the same message. Sidekick replies in the thread and keeps context across replies there, so you can keep the conversation going. The more specific your question, the more useful the answer.
## What Sidekick can access
Sidekick in Slack has the same tools and data as Sidekick in Plain:
**Always available:** Plain threads, customers, and tenants; your Help Center and knowledge sources; similar past threads.
**Available when connected** in **Sidekick → Integrations:** Notion, Grain, Granola, HubSpot, Attio, Linear, Jira, [incident.io](http://incident.io), Datadog, Grafana, Sentry, PostHog, LaunchDarkly, Slack, and GitHub. See Tools and integrations.
## What Sidekick can do from Slack
Sidekick can look things up and take actions, the same as in the app, including actions that require approval. When approval is needed, it shows a confirm and deny step right in Slack, so you can approve or deny without switching to Plain. See Actions and approvals.
## What teams use it for
**Engineering** (the heaviest users) investigate live incidents, run trace analysis, debug unexpected behavior and dev-environment errors, answer codebase questions, scope features into Linear, and query usage and metrics, all from Slack.
**Sales** get post-call intelligence, RFP assistance, and PTO catch-up, research feature gates, answer customer technical questions verified against the codebase, and run competitive gap analysis.
**Reporting and ops** run monitor-history audits and monitoring overviews, and ask product-analytics questions without any code spelunking.
**Product and general** ask architecture questions answered against the real implementation, investigate performance, debug across repos, and ask API-design questions.
## Slack-specific instructions
You can set a separate behavior prompt for how Sidekick responds in Slack, independent of its in-app instructions, to tune its tone, response length, or focus for a Slack context. Set it in the **Operating instructions** field in **Sidekick → Integrations → Slack**.
## Sessions and history
Sidekick conversations in Slack are scoped to the thread they happen in. They're not connected to your Sidekick sessions in the Plain app, and they're visible only to people in the same Slack thread.
# Sidekick in workflows
Source: https://www.plain.com/docs/product/agents/sidekick/in-workflows
Run a Sidekick task within a workflow or start a discussion your team can continue.
[Sidekick](/docs/product/agents/sidekick) can do work as part of a [workflow](/docs/product/workflows): triage a thread, investigate a bug, or post a report to Slack. You can run a task within the workflow or start a discussion your team can continue.
Choose between two steps:
* **Start Sidekick discussion**: starts a discussion using your workspace's tools and approval rules. This is the last step in the workflow; your team can continue the discussion afterward.
* **Sidekick action**: runs a task with tools you select, without asking for approval on each run. The workflow waits for the task to finish before continuing.
## Start a Sidekick discussion
Add **Start Sidekick discussion** in the workflow builder and write the **First message**. For example: "Investigate this bug report and tell us what you find."
Sidekick can ask for approval according to your [actions and approval rules](/docs/product/agents/sidekick/actions-and-approvals), and your team can reply in the discussion. You can also select [your own agent](/docs/product/agents/bring-your-own-agent) to handle it.
## Add a Sidekick action
A Sidekick action runs a task before the workflow continues. You can use it in a workflow triggered by a thread event, run manually, or set to a schedule.
Add a **Sidekick action** step in the workflow builder. Under **Task**, describe what Sidekick should do and where it should save or send the result.
For example: "Review this thread, add relevant labels, and set the priority to urgent if it describes an outage. Keep any labels or priority already set."
Under **Tools the AI can use**, enable the tools the task needs. You can also type **@** in the task and select a tool to enable it.
Some tools let you limit where Sidekick acts, such as which Slack channels it can post to. To use an external tool, connect its [integration](/docs/product/agents/sidekick/integrations) first.
Add any steps that should run after Sidekick finishes, then publish the workflow. For example, let Sidekick label a thread before a condition routes it to a team.
The tools you select run without asking for approval on each run, including tools that normally require approval in chat. Tools disabled in your workspace stay unavailable.
Sidekick can read Plain data without you selecting each read tool. For a connected integration, enable **Allow reads** or select a write tool, which also enables reads from that integration.
### When the task finishes
The workflow waits until Sidekick reports the task complete, then continues to the next step. If Sidekick can't complete the task, the run fails and later steps don't run.
Open the workflow run to check the result and its linked Sidekick discussion. Discussions started by workflows are visible to your workspace.
## Example tasks
Use a thread trigger for work on an individual thread, or a schedule for recurring work:
* **Triage incoming threads**: add labels and set priority before routing the thread to a team.
* **Investigate a bug**: search connected tools for related errors and add an internal note with the findings.
* **Post a weekly digest**: summarize the week's threads and post the report to a selected Slack channel.
# Tools & integrations
Source: https://www.plain.com/docs/product/agents/sidekick/integrations
Connect your tools to Sidekick and get context from Linear, GitHub, Datadog, Sentry, LaunchDarkly, Attio, HubSpot, Notion, and more, without leaving Plain.
Sidekick treats Plain's own data as home base and reaches outward through every connected integration. Instead of switching tabs to check Linear, GitHub, Datadog, or Notion, you ask Sidekick and it surfaces what's relevant, directly inside a conversation.
The more tools you connect, the more Sidekick can do. This article covers the catalog and how to connect it. For what Sidekick can *do* in these tools, and how approvals work, see **Actions and approvals.**
## How to connect an integration
You need to be a workspace **Admin** or **Owner** to connect integrations.
1. Go to **Plain AI → Sidekick → Integrations**
2. Find the integration you want and click **Connect**
3. Complete the authentication flow
4. Once connected, the integration is available to everyone in your workspace immediately
To disconnect, return to the same page and click **Disconnect**.
## How integrations work in a session
You don't configure anything per session. When you open Sidekick, it discovers which integrations your workspace has connected and uses them as needed. As it works, you can see its tool calls on screen in real time, so you always know what it looked up and why.
## Supported integrations
Every tool is opt-in and connected through **Sidekick → Integrations**. The catalog spans five categories, plus Slack, GitHub, and custom MCP servers.
### Knowledge and meetings
| **Integration** | **What Sidekick can look up** |
| --------------- | --------------------------------------------------------- |
| **Notion** | Search pages and databases for documentation and runbooks |
| **Granola** | AI meeting notes and summaries |
| **Grain** | Meeting recordings, transcripts, and call summaries |
### CRMs
| **Integration** | **What Sidekick can look up** |
| --------------- | ------------------------------------------------------------------------------------------------ |
| **HubSpot** | Contacts, companies, and deals; ticket status and pipeline stage; CRM activity and owner details |
| **Attio** | Company and contact records by email or domain; pipeline stages; account notes and tasks |
### Issues and incidents
| **Integration** | **What Sidekick can look up** |
| ------------------------------------- | --------------------------------------------------------------- |
| **Linear** | Search issues, look up status, find related projects and cycles |
| **Jira** | Search issues with JQL, look up issue details and comments |
| [**incident.io**](http://incident.io) | List active incidents, get incident details |
### Observability
| **Integration** | **What Sidekick can look up** |
| --------------- | -------------------------------------------------- |
| **Datadog** | Query metrics, logs, and monitor status |
| **Grafana** | Query dashboards and production signals |
| **Sentry** | Look up errors and exceptions, find related events |
### Analytics
| **Integration** | **What Sidekick can look up** |
| ---------------- | ------------------------------------------------------------------------------------------------------- |
| **PostHog** | Product events, session recordings, and user analytics; what a user has done in the product |
| **LaunchDarkly** | Feature flag status and targeting rules, rollout percentages, which variation is active per environment |
### Slack
| **Integration** | **What Sidekick can look up** |
| --------------- | ------------------------------------------- |
| **Slack** | Search channels and messages, look up users |
This Slack integration lets Sidekick look up Slack messages while you work in Plain.
It's separate from Sidekick in Slack, which lets your team @mention Plain inside a Slack channel.
You can enable each independently.
### Custom MCP servers
You can connect any internal tool to Sidekick through a custom MCP server, plugging your own systems into Sidekick's reach.
# Skills
Source: https://www.plain.com/docs/product/agents/sidekick/skills
Reusable routines you can invoke in Sidekick with a slash command. Built-in shortcuts and custom ones your team creates.
Skills are reusable, named routines you can run in Sidekick with a single slash command. Plain ships built-in skills for common support workflows, and your team can add custom skills for anything you do repeatedly.
## Built-in skills
Every workspace starts with three skills:
* **/draft-reply**: drafts a concise, human-sounding customer reply for the current thread, following your workspace tone settings.
* **/plain-docs**: answers questions about how Plain itself works, drawing on help.plain.com.
* **/fill-knowledge-gap**: researches a [knowledge gap](/docs/product/agents/knowledge-gaps) Ari recorded and drafts a new Help Center article to fill it, saved as a draft once you approve.
## Custom skills
Custom skills let your team encode recurring workflows so Sidekick can run them on demand. A skill has three parts:
* **Name**: the label shown in the slash-command picker and used to invoke the skill (e.g. `/quota-check`).
* **Description**: tells Sidekick when to suggest the skill automatically, without being asked directly.
* **Instructions**: the full routine Sidekick follows when the skill runs.
Custom skills are workspace-scoped: create one and it becomes available in every Sidekick session, for every user, immediately.
### Creating a skill
Open a Sidekick session and describe the skill you want. Sidekick will build it and save it to the workspace:
> Create a skill called quota-check that looks up how close a customer is to their plan limit and summarizes the result.
The skill is available to invoke as soon as it is created.
### Updating a skill
Ask Sidekick to update a skill by name and describe what should change. Instructions are fully replaced on each update, so be specific about what you want to keep, add, or remove.
## Invoking a skill
Type **/** in the Sidekick composer to open the skill picker. Select from the list or keep typing to filter by name, then press **Enter**. Sidekick runs the skill in the context of your current session: if you are on a thread, thread data is included automatically.
Skills work anywhere Sidekick is available: in a thread, on the Home page, or from a connected Slack channel.
# 3rd party agents
Source: https://www.plain.com/docs/product/agents/third-party
Third-party AI agents that connect with Plain, including Parahelp, Duckie AI, Decimal AI and Inkeep to integrate with Plain to automate customer support.
Plain integrates with several third-party AI agent providers that can automatically resolve customer support threads on your behalf. These agents connect to Plain as machine users, listen for incoming threads via [webhooks,](/docs/webhooks) and respond using your existing knowledge sources and tools.
See [building agents](/docs/agents) for a complete guide to the integration architecture.
Below is a summary of our partners:
## [Decimal](/docs/product/agents/third-party/decimal)
Decimal traces customer questions directly to relevant source code, configuration files, and release notes to generate accurate answers. Its self-healing knowledge base automatically updates as your product changes.
## [Duckie](/docs/product/agents/third-party/duckie)
Duckie brings agents that connect to 20+ tools your team already uses, including Slack, Notion, Jira, and your codebase. It can act as either an autonomous agent or a copilot.
## [Parahelp](/docs/product/agents/third-party/parahelp)
Parahelp lets you define custom procedures so its agent can act for a customer: look up subscription details, process a refund, or file a bug. It connects to tools such as Stripe, Slack, and Linear alongside your documentation.
## [Inkeep](/docs/product/agents/third-party/inkeep)
Inkeep provides **Keep**, a support copilot. Keep's Smart Assist mode analyzes the thread you're viewing and generates context-aware suggestions, including draft answers, relevant doc links, thread summaries, and follow-up to-dos.
Keep is fully conversational, so you can ask it to refine or expand its responses.
## Building your own agent
If you'd prefer to build a custom integration, Plain's API supports bringing your own agent. You can set up machine users, subscribe to webhooks, manage agent status, and handle the full reply-and-handoff flow yourself.
# Decimal
Source: https://www.plain.com/docs/product/agents/third-party/decimal
Connect Decimal's AI Support Engineers and AI agents to your Plain workspace to autonomously resolve technical support tickets.
The Decimal integration connects AI Support Engineers to your Plain workspace, autonomously resolving technical support tickets by reading your source code, logs and documentation.
Built specifically for technical support teams, Decimal combines Plain threads and Help Center articles with its understanding of your product, tracing each question back to the source it answered from.
Resolution steps are posted on threads directly in Plain, so your team sees them without changing how they work.
## How to set it up
In Plain:
1. Go to **Settings > API keys**
2. Create a new API key with the necessary permissions
3. Copy the key and paste it into the Decimal configuration
In Decimal:
1. **Go to Integrations**: Go to the **Integrations** page in the sidebar.
2. **Click Plain**: Click on the Plain integration row to open the configuration modal.
3. **Provide your API key**: Enter your Plain API key.
4. **Connect**: Click **Connect** to save the configuration.
**Chat Widget Escalation**
Once connected, Plain can be selected as an escalation target for [**Chat Widgets**](https://docs.decimal.app/chat-widgets/overview). When a customer escalates a conversation, a new Plain thread is created with the full chat history.
Check out [**Decimal's Plain integration docs**](https://docs.decimal.app/integrations/plain) for full setup instructions.
For a general overview of how AI agents work in Plain (machine users, webhooks, agent status), see [building agents](/docs/agents).
# Duckie
Source: https://www.plain.com/docs/product/agents/third-party/duckie
Connect Duckie's AI support agents to your Plain workspace to automate technical support for B2B SaaS teams.
The Duckie integration help B2B SaaS support teams resolve technical tickets faster by connecting to your existing tools and knowledge sources.
**Capabilities**
| **Capability** | **Supported** |
| ---------------------- | ---------------------------------- |
| **Knowledge Source** | ✗ |
| **Deployment Trigger** | ✓ Respond to threads |
| **Tool Actions** | ✓ Reply, update status, add labels |
## How to set it up
**Step 1: Get API Credentials**
1. Go to your Plain workspace settings.
2. Go to **API keys.**
3. Create a new API key with full permissions.
4. Copy the **Workspace ID** and **API key.**
**Step 2: Connect in Duckie**
1. Go to **Settings → Connections** in Duckie.
2. Find **Plain** and click **Connect.**
3. Enter your details:
* **Workspace ID**: Your Plain workspace ID
* **API key**: The key you created
4. Click **Connect.**
**Step 3: Using as a Deployment Trigger**
Deploy an agent to respond to Plain threads:
1. Go to **Deploy**
2. Click **Create Deployment**
3. Select your agent
4. Choose **Plain** as the trigger
5. Configure events:
* **New thread**: When a customer starts a conversation
* **Customer reply**: When a customer responds
## Available tools
| **Tool** | **Description** |
| --------------------- | -------------------------- |
| `plain_reply` | Send a reply to the thread |
| `plain_add_label` | Add a label to the thread |
| `plain_change_status` | Update thread status |
| `plain_get_customer` | Get customer information |
Check out [**Duckie's Plain integration docs**](https://docs.duckie.ai/integrations/ticketing/plain) for full setup instructions.
For a general overview of how AI agents work in Plain (machine users, webhooks, agent status), see [building agents](/docs/agents).
# Inkeep
Source: https://www.plain.com/docs/product/agents/third-party/inkeep
Connect Inkeep's AI copilot to your Plain workspace for context-aware draft replies, relevant links, and thread summaries.
The Inkeep integration brings AI-powered support directly into your Plain workspace, helping B2B support teams triage and respond faster, with less manual effort.
**Keep**, Inkeep's support copilot, is available via API in Plain. With its Smart Assist mode, it analyzes the thread you're viewing and generates intelligent, context-aware suggestions to keep the conversation moving.
## Knowledge sources
Keep works with more than your public documentation. It can also draw on private content:
* **Public docs & Help Center**: your published documentation and Help Center articles
* **Historical support tickets**: past conversations and resolutions from your support history
* **Slack conversations**: messages from internal Slack channels relevant to the topic
This means Keep's suggestions are grounded in how your team resolves issues, not only in your public docs.
## Prehook for dynamic context
Inkeep supports a prehook that lets you provide dynamic, per-ticket context to the copilot. When a support agent clicks Smart Assist, Inkeep makes a request to an API route of your choosing. Your API route returns custom information and instructions that are added as context for the copilot to consider: for example, pulling in customer-specific data like plan details or feature flags.
## How to set it up
Check out [**Inkeep's docs**](https://docs.inkeep.com/cloud/support-tools/agent-copilot/plain) for full setup instructions.
At a high level, you'll configure Inkeep with your Plain API key and set up the copilot to appear as a customer card within your Plain workspace. Any sensitive information like API keys is encrypted at storage.
# Parahelp
Source: https://www.plain.com/docs/product/agents/third-party/parahelp
Connect Parahelp's AI agent to your Plain workspace to autonomously resolve complex support tickets end-to-end.
The Parahelp integration connects your Parahelp customer agent to your Plain workspace, resolving complex support tickets end-to-end by using your tools (like Stripe, Slack, and Linear), knowledge, and procedures.
Parahelp is purpose-built for fast-moving tech companies. In addition to resolving complex support tickets, Parahelp also automates support operations (e.g., updating docs, identifying bug report patterns, and syncing with Linear).
## How to set it up
## Adding Parahelp as a team member
1. Contact the Parahelp team to set up your agent. Once you're ready to connect to Plain:
2. Go to "Settings" → "Members"
3. Click "Invite member"
4. Under "Email", enter `[company]@parahelp.com`
5. Set the role to "Admin": this permission allows Parahelp to connect to the Machine user.
6. Click "Send invite."
These permissions allow Parahelp to set up a Machine user and enable the AI agent to transfer tickets to team members.
For a general overview of how AI agents work in Plain (machine users, webhooks, agent status), see [building agents](/docs/agents).
# Tone of voice
Source: https://www.plain.com/docs/product/agents/tone-of-voice
Set the rules that control how AI sounds in customer-facing replies.
Tone of voice rules control how AI sounds in customer-facing replies. So any AI-generated responses stay consistent with your brand.
Tone rules are configured per workspace from **Settings → Plain AI → Configuration → Tone of voice**. They apply to all customer-facing AI in that workspace, including
* Suggested responses in the composer
* Ari responses
* Help Center AI chat
* Sidekick draft replies.
Plain AI automatically adjusts writing style based on the channel, like using a structured letter format for email and a concise, conversational style for chat. You don't need to set this yourself.
## How tone rules work
Rules are organized into **five rule categories**:
* **Empathy**: How much AI should acknowledge feelings and respond with understanding
* **Language**: Terminology, jargon, and level of technicality
* **Formality**: How formal or casual messages should feel
* **Personality**: How brand voice comes through
* **Warmth**: How friendly and positive messages should feel
You can fill in each category with your rule: short, free-text instructions that tell the model how to behave (e.g. "Use plain language and avoid jargon" or "Be warm and conversational").
## Generating rules from a description
If you already have a style guide or a short description of how you want to sound, you can use **Generate rules** instead of writing each rule by hand. In the popover, paste or type a general description of your brand / tone guidelines (e.g. a paragraph from your style guide). Plain tries to turn it into suggested rules and fill in the same five categories for you. Those suggestions appear as **drafts** which you can edit, and choose which to keep or reject.
# Broadcasts
Source: https://www.plain.com/docs/product/broadcasts
Send one message to many customers from Plain, and track delivery and replies per channel.
Broadcasts let you send one message to many customers at once, straight from Plain. Write it once, choose who it goes to, and Plain posts it to the connected Slack channels that match your configured audience.
Every reply will come back as a normal Plain thread, linked to the broadcast that started it.
## Before you start
You need:
* Plain's Slack integration connected
* A Slack channel per customer - Broadcasts reach customers through their connected Slack channels, so a customer without a channel cannot be reached.
* The Owner, Admin, or Support role, if you want the broadcast to send under your own name.
## Sending a broadcast
Follow these steps to send a broadcast:
* [Create a broadcast](/docs/product/broadcasts/create): the name, notification title, content, and sender
* [Broadcast targets](/docs/product/broadcasts/targets): filter customers into a list of Slack channels
* [Send and schedule](/docs/product/broadcasts/schedules): review it, post a test, then send now or schedule it
* [Read the report](/docs/product/broadcasts/reports): delivery counts, per-channel statuses, reactions, and reply threads
## The broadcast lifecycle
A broadcast has four states, and the Broadcasts list has a tab for each:
| Tab | What's in it |
| ----------- | -------------------------------------------------------------------- |
| Drafts | Broadcasts you're still writing. Editable, nothing has been sent. |
| Scheduled | Queued to send at a set time. Still editable, and still cancellable. |
| In progress | Currently resolving recipients or posting messages. |
| Sent | Finished sending, whether fully, partially, failed, or canceled. |
A broadcast can only be sent once. After it sends, the message becomes read-only and the page turns into a report on how the send went. To send the same message again, duplicate the content into a new broadcast, or filter on the threads the first one created and follow up there.
## Who can use broadcasts
Your role in the workspace decides what you can do with broadcasts. Access is role-based, not tied to your plan:
* **Viewer**: can open every broadcast and its report, but cannot create, edit, delete, or send
* **Support, Admin, and Owner**: can create, edit, delete, and send broadcasts, and can create, edit, and delete [audiences](/docs/product/broadcasts/audiences)
Anyone who can open **Broadcasts** can see every broadcast in the workspace. There is no per-broadcast visibility.
## Using the API
You can create, update, send, and test broadcasts through the [GraphQL API](/docs/graphql/introduction). The [API key](/docs/graphql/authentication) needs all four of the following permissions:
* `broadcasts:create`
* `broadcasts:send`
* `broadcasts:read`
* `workspaceSlackIntegration:read`
`workspaceSlackIntegration:read` is required because a broadcast resolves its audience into Slack channels, which means reading your Slack integration.
To create, update, list, and delete saved audiences, the key also needs:
* `broadcastAudience:create`
* `broadcastAudience:edit`
* `broadcastAudience:read`
* `broadcastAudience:delete`
Use `broadcastSendTargetRecipients` to see who a send target resolves to before you save it. Use `sendTestBroadcast` to post a draft to up to 10 named Slack channels. A test send does not change the broadcast's status.
## Resources
* [Broadcast audiences](/docs/product/broadcasts/audiences): build and reuse the filter that decides who a broadcast reaches
* [Slack](/docs/product/channels/slack): connect the integration and set up customer channels
* [Tenants](/docs/product/platform/tenants): the customer accounts and tenant fields you filter on
* [Tiers](/docs/product/platform/tiers): the service levels you filter on
* [GraphQL API](/docs/graphql/introduction): the data model behind the API permissions above
# Broadcast audiences
Source: https://www.plain.com/docs/product/broadcasts/audiences
Build a reusable filter that decides who a broadcast reaches, resolved fresh on every send.
An audience is a saved, reusable set of recipients. Build the filter once, name it, and any broadcast can reach it without you rebuilding the same rules.
Audiences live at **Broadcasts → Audiences**. You can also build a filter directly on a broadcast and save it as an audience from there.
Access follows the same roles as [broadcasts](/docs/product/broadcasts#who-can-use-broadcasts). Users with the Viewer role cannot create, edit, or delete audiences. Support, Admin, and Owner can.
## How an audience resolves
Audiences are filters, not fixed lists. Plain resolves them fresh every time: when you preview a broadcast, and again when it sends.
That matters in both directions:
* A customer that becomes Enterprise tomorrow is inside "Tier is Enterprise" tomorrow, with no editing.
* A broadcast scheduled for next week sends to whoever matches on the day it goes out, not to whoever matched when you scheduled it.
The chain is: **filter → matching customers → their connected Slack channels**.
A channel only becomes a recipient if it's an enabled customer channel. Plain skips discussion channels and disabled channels without reporting it, even if you pinned them by name. The count you see in the preview is the real, post-filtering number.
A customer with no connected Slack channel cannot be reached by a broadcast, no matter how well they match the filter. If your count is lower than expected, check this first.
## Filter dimensions
Click **Add filter** to pick a dimension. Each opens a searchable list of values.
| Dimension | What it matches |
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Audience** | Another saved audience. Only available on a broadcast, not inside another audience. |
| **Channel** | Specific Slack channels, pinned by identity. Doesn't change when a channel is renamed. |
| **Channel name** | Channels whose name contains the text you type, matched case-insensitively at send time. Picks up channels created or renamed after you saved the filter. |
| **Tenant** | Specific tenants by name. |
| **Tier** | Every tenant on the chosen tier or tiers. |
| **Tenant field** | Any visible tenant field, such as region, plan, or owner. |
Tenant fields with no fixed set of values (a free-text or date field, for example) don't appear in the menu, because there's nothing to pick from.
## How filters combine
Two rules:
* **Different dimensions narrow.** "Tier is Enterprise" plus "Tenant field: Region is EU" reaches only Enterprise customers in the EU.
* **Values inside one dimension widen.** "Tier is Enterprise, Growth" reaches both tiers.
### Is / is not
Each filter pill can be flipped from **is** to **is not**. "Tier is not Free" reaches everyone except Free tier.
### AND / OR
Filter pills sit in groups. You can set the operator between pills in a group, and between groups, to `AND` or `OR`.
An **is not** filter joined by `OR` widens the audience instead of narrowing it. "Tier is Enterprise OR tier is not Free" reaches every customer that either half reaches, because the two sides add together. Plain shows a warning when you build one. Check the channel count below the filter before you send.
### Nesting limits
The builder supports groups of pills, one level deep. An **is not** can only negate a single pill, not a whole group, and not another negation. If you need something deeper, split it into two audiences and combine them with the **Audience** dimension.
## Creating an audience
Go to **Broadcasts → Audiences** and click **New Audience**.
Name it for who it reaches, such as "Enterprise, NA" or "Design partners".
The channel list at the bottom fills in as you build, showing who this resolves to right now.
Click **Save Audience**.
An audience with no filters at all reaches every customer. That's valid, so name it clearly.
### Saving a filter from a broadcast
While building **Deliver to** on a broadcast, click **Save as Audience**. Name it and save.
The filter you built collapses into a single audience pill on that broadcast. The rules haven't changed. They now live in the audience instead of on the broadcast.
## Using an audience in a broadcast
In the broadcast's **Deliver to** section, click **Add filter → Audiences** and pick one.
An audience behaves like any other dimension, so you can combine it:
* **"Design partners" AND "Tier is Enterprise"**: only design partners who are also Enterprise.
* **"Design partners" OR "Beta users"**: everyone in either.
* **"All customers" AND NOT "Internal channels"**: everyone except the ones in that audience.
An audience can't be defined in terms of another audience. Nesting stops at one level, so you can always see what a filter resolves to.
## The audiences list
Each row shows:
* **Name**
* **Created by**
* **Filter**: the rules in plain words
* **Targets**: how many channels it resolves to right now
* **Last edited**
Click a row to edit it.
## Editing and deleting
### Editing
Editing an audience changes who every broadcast using it reaches, including broadcasts already scheduled but not yet sent. Broadcasts that have already sent are unaffected; their recipient list is a record of what happened.
### Deleting
Deleting an audience removes it from the picker so no new broadcast can use it. Broadcasts already sent to it keep their history.
You can't delete an audience while a broadcast targeting it is scheduled or currently sending. Cancel or wait for that broadcast first.
## Troubleshooting
### No Tenants match this filter. Widen it to reach someone.
The filter is too narrow, or a value has changed. Nobody matches at all.
### Tenants match, but none of them have a connected Slack channel that is enabled.
The right customers matched, but Plain has no channel to post in. Check the Slack integration and whether those channels are enabled customer channels.
### Everything this filter matched was removed by the exclusions on this Broadcast.
You've unchecked every channel the filter found. Clear the **excluding N** pill, described in [Broadcast targets](/docs/product/broadcasts/targets).
### This audience was written through the API and says more than this editor can show.
The filter uses a shape the visual builder can't draw, such as a filter created through the API. The builder goes read-only rather than dropping part of it without telling you. Edit it through the API, or rebuild it as a new audience.
### The count is lower than the number of matching customers
Expected. Customers without an enabled, connected customer channel aren't reachable, and one customer can have zero, one, or several channels.
## Resources
* [Broadcast targets](/docs/product/broadcasts/targets): use an audience on a broadcast, and exclude single channels
* [Broadcasts](/docs/product/broadcasts): what a broadcast is and the four states it can be in
* [Tenants](/docs/product/platform/tenants): the customer accounts and tenant fields you filter on
* [Tiers](/docs/product/platform/tiers): the service levels you filter on
* [Slack](/docs/product/channels/slack): connect the integration and set up customer channels
# Create a broadcast
Source: https://www.plain.com/docs/product/broadcasts/create
Name a broadcast, write its notification title and content, and pick who it appears to come from.
A broadcast is one message posted to many Slack channels. Writing one means giving it an internal name, a customer-facing notification title, the content itself, and a sender.
Click **New broadcast** from the Broadcasts page. It stays a draft, editable and unsent, until you send or schedule it from the review step.
Creating a broadcast requires the Support, Admin, or Owner role. Users with the Viewer role can open existing broadcasts but cannot create one.
## Name
The internal name, used in the Broadcasts list and in your team's search. It never leaves Plain, so name it for your team: "Q3 pricing update. Enterprise" is more useful than "Hello".
## Notification title
What recipients see in their Slack notification banner, before they open the message. This is customer-facing.
By default the notification title copies whatever you type as the name. As soon as you edit the notification title yourself, it stops following the name and stays as you wrote it.
A notification title is required before you can send.
## Content
The composer is a formatted text editor that becomes a Slack message when it sends. Press `/` anywhere to insert a block.
Available blocks:
* Text, headings, bullet lists, numbered lists, quotes, and code
* Dividers
* Images
* **Buttons**: a labeled link, rendered as a real Slack button
* `@here` and `@channel` mentions
Select text to show the formatting toolbar.
## Sender
Under **Send from**, choose who the message appears to come from:
* **Your workspace**: posts under your workspace's name and avatar. This is the default.
* **A user**: posts under their name and avatar. Only users with the Owner, Admin, or Support role can be picked.
The message still goes out on Plain's Slack app either way; the sender controls the name and avatar shown.
## Options
**Unfurl Slack links and media** controls whether Slack expands link previews and embedded media in your message. It's on by default, matching Slack's own behavior. Turn it off for a message with several links that would otherwise become a wall of previews.
## Resources
* [Broadcast targets](/docs/product/broadcasts/targets): the next step, building the audience
* [Send and schedule a broadcast](/docs/product/broadcasts/schedules): review it, test it, then send or schedule it
* [Broadcasts](/docs/product/broadcasts): what a broadcast is and what you need before writing one
# Broadcast reports
Source: https://www.plain.com/docs/product/broadcasts/reports
Read the delivery counts, per-channel statuses, reactions, and reply threads a send produces.
Once a broadcast starts sending, its page becomes a report that updates live while the send is running. The report has four parts: delivery counts, the recipient list, reactions, and the threads customers started by replying.
Anyone who can open **Broadcasts** can read the report, including users with the Viewer role.
## Delivery
Four counts, taken from one snapshot so they always add up:
* **Recipients**: total channels this send targeted
* **Delivered**: messages Slack confirmed
* **Pending**: waiting to be posted, or mid-post
* **Failed**: will not arrive
A send that reaches some channels and not others finishes as **Partially sent**, which is deliberately distinct from **Sent**.
## Recipients
Every channel the send targeted, with a status icon each. The list keeps the name each channel had when the send resolved, so renaming a channel afterwards doesn't rewrite the record of where a message went.
Hover a failed channel to see why. Plain reports these reasons:
| Reason shown | What to do |
| ------------------------------------------------------------------------- | -------------------------------------------------------------- |
| Plain is not connected to this channel | Reconnect the channel, or remove it from the audience. |
| Plain's Slack app is missing a permission it needs to post | Reconnect the Slack integration. |
| Slack rejected this message's formatting | Simplify the message content and send again. |
| Slack rate limited this channel's workspace for longer than we could wait | Retry in a new broadcast; Slack was throttling that workspace. |
| The Broadcast has no sender set / no notification title | Fix the broadcast and send again. |
Failures shown here are final. Anything worth retrying was already retried automatically before it landed in this list.
## Reactions
Emoji reactions on the messages this broadcast posted, totaled across every channel. The count keeps updating after the send finishes. It's a readout only, so you can't react from here.
## Threads
Every Plain thread created when someone replied to one of this broadcast's messages, newest first. These behave like any other thread: assign them, label them, reply to them. A broadcast creates a thread only when someone replies, not one per customer.
## Resources
* [Broadcasts](/docs/product/broadcasts): the four states a broadcast can be in
* [Send and schedule a broadcast](/docs/product/broadcasts/schedules): what happens before a report exists
* [Slack](/docs/product/channels/slack): the integration and customer channels a send depends on
# Broadcast schedules
Source: https://www.plain.com/docs/product/broadcasts/schedules
Review a broadcast, post a test to internal channels, then send it now or schedule it for later.
Every broadcast goes through a review step before it sends. Review is also where you post a test, pick between sending now and scheduling, and where you edit or cancel a broadcast that's already queued.
Sending, scheduling, editing, and canceling require the Support, Admin, or Owner role.
## Review
**Save and continue** saves the broadcast and moves you to the review step. Review shows the message exactly as it will arrive, with the audience locked so you can read it without changing it.
## Send a test
On review, the **Test broadcast** section lets you post the broadcast as it currently stands to up to 10 channels you name.
A test is a real Slack message posted to real channels. Pick an internal channel unless you mean to send it to a customer.
Plain records test sends in the broadcast's history but they don't change its status. The broadcast stays a draft.
## Send or schedule
At the bottom of the review step, pick when it goes out:
* **Send now**: posts immediately. The button says **Send broadcast now**.
* **Schedule**: pick a date and time. The time is in your local timezone, and you can't pick a time in the past.
A send cannot be undone. Once a broadcast starts posting, you can't recall the messages or edit them.
## Edit or cancel a scheduled broadcast
Once scheduled, the broadcast moves to the **Scheduled** tab. From there you can:
* **Edit**: change the message, audience, or send time, then **Save changes**.
* **Cancel send**: clears the send time and returns the broadcast to a draft. Nothing is lost, and you can reschedule it.
Canceling only works while the broadcast is still scheduled. Once it starts resolving recipients, it can no longer be stopped.
## Resources
* [Broadcast reports](/docs/product/broadcasts/reports): the next step, watching delivery and replies
* [Broadcast targets](/docs/product/broadcasts/targets): the audience this send resolves
* [Write a broadcast](/docs/product/broadcasts/create): the message itself
# Broadcast targets
Source: https://www.plain.com/docs/product/broadcasts/targets
Filter customers into a list of Slack channels, then drop individual channels from this broadcast.
The **Deliver to** section on a broadcast is where you build the target audience. You add filters, Plain resolves them into a list of Slack channels, and you can drop any channel from the list before you send.
Building or changing a target requires the Support, Admin, or Owner role. Users with the Viewer role can see the resolved channel list, but they cannot change the filter.
## Add filters
See [Broadcast audiences](/docs/product/broadcasts/audiences) for the full detail. In short:
* Add filters on **Audience**, **Channel**, **Channel name**, **Tenant**, **Tier**, or any visible **Tenant field**. A [tenant](/docs/product/platform/tenants) is a customer account in Plain, a [tier](/docs/product/platform/tiers) is the service level you put it on, and a [tenant field](/docs/product/platform/tenants) holds your own data about it.
* Plain resolves those filters into a list of Slack channels and shows you the channels, and the customer behind each one, as you build.
* Uncheck any channel in that list to drop it from this broadcast only.
* Save a filter you'll reuse as an **Audience**.
The count above the channel list is how many channels receive the message, after exclusions.
A broadcast with no filter at all reaches every customer with a connected channel. Plain shows the count before you send, so check it before confirming.
## Exclude individual channels
Filters describe a rule. To send to everything the rule matches except a few named channels, exclude those channels on the broadcast.
In the **Deliver to** section, uncheck any channel in the resolved list. An **excluding N** pill appears next to your filters, and the count above the list drops.
Two things to know:
* Exclusions belong to the **broadcast**, not to the audience. Excluding a channel here never changes who the audience reaches elsewhere.
* Unchecked channels stay visible in the list, so you can check them again.
Clear the **excluding N** pill to restore every channel at once.
## Resources
* [Broadcast audiences](/docs/product/broadcasts/audiences): every filter dimension, how filters combine, and saved audiences
* [Send and schedule a broadcast](/docs/product/broadcasts/schedules): the next step, review and send
* [Write a broadcast](/docs/product/broadcasts/create): the message itself
# Channels
Source: https://www.plain.com/docs/product/channels
Every channel Plain supports, and how messages from each become threads in one queue.
Channels are the surfaces through which customers contact your team. Plain unifies all of them into a single, structured thread queue with consistent workflows, SLAs, labels, and reporting regardless of where a conversation originates.
Plain supports the following channels: **Slack, Email, Chat, Microsoft Teams, Discord, Contact Forms, and the Headless Support Portal.**
## Slack
For companies running shared Slack Connect channels with customers, Plain ingests messages from those channels and surfaces them as threads. Your team can triage, respond, and track requests directly from Plain, and replies post back to Slack as if sent from there. You can control exactly which channels sync using ingestion modes, and set up auto-join rules so new channels are picked up automatically.
[Learn more about Slack →](/docs/product/channels/slack)
## Email
Route your support email address (e.g. `support@yourcompany.com`) into Plain to handle all inbound email as structured threads. You can send and receive emails, manage alternate addresses, and keep everything tracked alongside your other channels: with none of the inbox chaos.
[Learn more about Email →](/docs/product/channels/email)
## Live chat
Embed Plain's chat widget directly in your product or website so customers can reach out without leaving your app. Each conversation becomes a thread in Plain. You can customize the widget appearance, configure business hours, and show agent or AI status to customers in real time.
[Learn more about Chat →](/docs/product/channels/chat)
## Microsoft Teams
For customers who work primarily in Microsoft Teams, Plain can sync messages from selected Teams channels directly into your support queue. Your team responds from Plain and replies are posted back into Teams: keeping customers in their preferred environment while giving your team full structure and context.
[Learn more about Microsoft Teams →](/docs/product/channels/microsoft-teams)
## Discord
Designed for developer-focused and community-driven products, the Discord integration brings messages from your Discord server into Plain as threads. Instead of losing threads in busy channels, your team can triage and respond with the same workflows used across every other channel.
[Learn more about Discord →](/docs/product/channels/discord)
## Contact forms
Contact forms let you define a structured intake process for threads. You control the fields, required information, and routing, so threads arrive in Plain with the context your team needs to respond immediately. Forms can be embedded in your Help Center or linked to directly.
[Learn more about Contact Forms →](/docs/product/channels/contact-forms)
## Headless support portal
For teams that want full control over the support experience, Plain exposes a GraphQL API to power custom-built, in-app support portals. Customers can submit requests, view thread history, and track status. All within your own UI. This is the right choice when the out-of-the-box chat widget or Help Center doesn't fit your product's design or requirements.
[Learn more about the Headless Support Portal →](/docs/headless-portal)
# Chat
Source: https://www.plain.com/docs/product/channels/chat
Embed Plain's chat widget in your product so customers can start a thread without leaving it.
Plain's chat widget lets you embed a branded, live chat interface on your website or app, allowing customers to reach out without leaving your product. You handle every message directly from Plain: alongside your email, Slack, and other support channels.
## Set up and customize chat
### 1. Create your chat app
In Plain, go to **Settings → Chat**. Press **Create a Chat App**.
### 2. Add chat to your webpage(s)
After creating your Chat app, you will be provided with a snippet of code that you can embed in your website or app. This will add the chat widget to your site. We recommend adding this to all pages of your site.
### 3. Customize your chat experience
You can customize some aspects of the Chat widget by providing additional information to the `Plain.init` function.[Learn more about customization here.](/docs/product/channels/chat-customization)
## Configure your customer's experience
There are multiple settings you can configure once you click into your chat app, including:
* Show business hours to users.
* Show agent status to users.
* Enable unread messages notifications.
You can also go to [**Settings → Auto-responders**](/docs/product/platform/auto-responses) to configure an auto-response based on Tier, label, or business hours.
## Content security policy (CSP)
If you are using a Content Security Policy on your website, you need to add the following to your CSP
```bash theme={null}
script-src https://chat.cdn-plain.com;
connect-src https://chat.uk.plain.com https://prod-uk-services-attachm-attachmentsuploadbucket2-1l2e4906o2asm.s3.eu-west-2.amazonaws.com;
style-src https://fonts.googleapis.com;
img-src https://prod-uk-services-workspac-workspacefilespublicbuck-vs4gjqpqjkh6.s3.amazonaws.com https://prod-uk-services-attachm-attachmentsbucket28b3ccf-uwfssb4vt2us.s3.eu-west-2.amazonaws.com https://i0.wp.com;
```
The S3 bucket URLs are required for the chat widget to display the workspace logo, uploading and showing attachment files. All these S3 buckets are owned by Plain. Agent profile pictures are powered by Gravatar, hence the requirement for `i0.wp.co`
# Authentication
Source: https://www.plain.com/docs/product/channels/chat-authentication
Identify signed-in customers in the chat widget by passing an email and an HMAC hash.
Authentication allows you to identify who you're talking to and personalize replies using known account context.
* Route and prioritize requests more accurately based on known customer attributes.
* Prevent duplicate customer records and avoid exposing information to the wrong person.
* Improve security without adding a second login for the customer.
Whether you're embedding chat behind a login or offering open access on your marketing site, the right authentication method ensures customers are identified correctly and securely.
## Authentication options
By default, customers chatting with you will be anonymous. You can pass customer details, if you know them, in the `Plain.init` function call:
```js theme={null}
Plain.init({
// ... Other options
customerDetails: {
fullName: 'John Doe', // Optional
shortName: 'John', // Optional
chatAvatarUrl: 'https://picsum.photos/32/32', // Optional
externalId: 'your_internal_user_id', // Optional. Your system's ID for this customer
tenantIdentifier: { tenantId: 'ten_01ABC' }, // Optional. Or { externalId: 'your_tenant_id' }
},
});
```
You can also include an email address using the `email` field property in the `CustomerDetails` object. To ensure the email is verified, Plain requires an `emailHash`, which is a secure hash of the email address and a secret.
There are two ways to provide this hash. You can generate it yourself if the user is already authenticated in your product, or you can use Plain's built-in email verification flow. Both methods are outlined below.
## Manual email verification - when you already know the user's identity
Use this approach when the user is logged into your application and you already know their verified email address.
If the chat widget is embedded in an authenticated environment (such as a customer dashboard), you can securely associate the session with the correct customer in Plain.
### Steps
1. **Generate a secret**
Go to the Chat settings page in Plain and generate a secret.
2. **Calculate the email hash on your backend**
```ts theme={null}
import * as crypto from 'node:crypto';
const secret = 'your_chat_secret_here';
const email = 'johndoe@example.com';
const hmac = crypto.createHmac('sha256', secret);
hmac.update(email);
const hash = hmac.digest('hex');
```
3. **Initialize the widget with both the email and hash**
```js theme={null}
const email = 'johndoe@example.com';
const emailHash = await fetchHashFromBackend(email);
Plain.init({
customerDetails: {
email,
emailHash,
// Optional: additional customer details
fullName: 'John Doe',
shortName: 'John',
chatAvatarUrl: 'https://picsum.photos/32/32',
},
});
```
Always calculate the email hash server-side to protect your secret.
Because the `emailHash` is treated as a bearer credential, it must be protected like any other authentication token. Leaking the hash, or the secret used to generate it, would allow unauthorised parties to impersonate the customer.
## Built-in email verification - when you don't know the user's identity
Use this method when you do not know who the user is and want to verify their identity before they can chat. This is ideal for public pages, such as marketing sites, or anywhere you don't manage authentication yourself.
Plain's built-in verification flow prompts users to verify their email before they can start chatting.
To enable this, set the `requireAuthentication` option to `true` when initializing the Plain widget:
```js theme={null}
Plain.init({
requireAuthentication: true
});
```
When enabled:
* Users will be asked to enter their email address
* Plain sends them a one-time code via email
* After verification, they can start a conversation
# Customization
Source: https://www.plain.com/docs/product/channels/chat-customization
Configure the chat widget's appearance, launcher, welcome screen, and customer details.
Everything the chat widget shows is configurable through `Plain.init`: the launcher, the welcome screen, the theme, and the customer details you pass in.
## General customization
Plain's chat widget is fully customizable, so you can tailor it to match your brand and user experience. From appearance and behavior to pre-filled thread metadata and structured inputs, you can shape the chat flow to fit your support model.
* Stay on brand with color, layout, and logo options
* Capture structured data at the point of contact to help with triage and routing
* Adapt the chat interface to different use cases across your product
* Surface links to docs, forms, or key actions before a user starts a conversation
Whether you want to embed chat in a billing portal, an onboarding wizard, or a usage dashboard, the widget can adapt to fit your exact workflow and customer experience.
### General customization code
```js theme={null}
Plain.init({
appId: 'your_chat_app_id_here',
// Optional. Hides the launcher, you can manually show it by calling `Plain.open()` (default: false)
hideLauncher: false,
// Optional. A collection of links shown on the Welcome screen
links: [
{
// Optional, supported icons are:
// 'bell',
// 'book',
// 'bug',
// 'bulb',
// 'chat',
// 'integration',
// 'discord',
// 'discord_muted',
// 'email',
// 'slack',
// 'slack_muted',
// 'link',
// 'pencil',
// 'send',
// 'support',
// 'error'
icon: 'book',
text: 'View our docs',
url: 'https://www.plain.com/docs',
},
],
// Optional. The entry point of the Chat.
entryPoint: {
// Type is either 'default' or 'chat'. 'default' will open the intro screen, 'chat' opens up straight into a chat.
type: 'chat',
// Optional. The external ID of which chat to open. If not provided it will default to the last conversation the user had.
externalId: 'example_external_id',
// Optional. Prevents the user from going back to the intro screen to start a new chat.
singleChatMode: false,
},
// Optional. Lets you specify which HTML element to insert the chat into.
// When specified, launcher will be hidden by default.
// Note: this must be a DOM Element, not a CSS selector string.
embedAt: document.querySelector('#embed'),
// Optional. Hides the 'Powered by Plain' branding
hideBranding: false,
// Optional. The color scheme of the Chat, is either 'auto', 'light', or 'dark'
// 'auto' uses the user's browser preference to decide between light and dark mode (default: 'auto')
theme: 'light',
// Optional. Various styling options
// Colors can also be passed in this format { light: '#FFFFFF', dark: '#000000' }. Based on the theme chosen by you or browser preference
style: {
brandColor: '#22C55E', // This will be used in various places in the chat widget such as the primary chat button and send message button
brandBackgroundColor: '#22C55E', // Used for the background of the chat widget on the intro screen
launcherBackgroundColor: '#000000', // These can also be passed in this format { light: '#FFFFFF', dark: '#000000' }
launcherIconColor: '#FFFFFF',
},
// Optional. Logo which is shown in the header of the chat intro screen.
// If you have uploaded a logo in your chat settings, this setting will take priority over that.
logo: {
// url to get the logo from
url: 'http://example.com',
// Optional. Alt text which is displayed on hover of the logo
alt: 'An example logo',
},
// Optional. Position of the chat widget when it is floating.
// See https://developer.mozilla.org/en-US/docs/Web/CSS/position for more information - only bottom, right, and zIndex are supported
position: {
right: '10px',
bottom: '10px',
zIndex: '1000', // Optional. Controls the stacking order of the widget
},
// Optional. Allows you to set fields for the threads which are created from chats
threadDetails: {
// See the data model documentation for more information on these fields
},
// Optional. Lets you customize the buttons which are used to start a new chat on the intro screen
chatButtons: [
// See the chat buttons documentation for more information on these fields
],
});
```
## Enhance your chat flows with Plain's data model
Plain's chat widget integrates directly with the platform's data model. This lets you tag and structure threads from the start, so your team can triage without reading each one first.
You can set the following on threads created from chat:
* Labels
* Priority
* Tiers
* Tenants
* External IDs
There are several places in the configuration you can add these with varying effects:
### All threads
Providing a top-level `threadDetails` object in the `Plain.init` function will set these fields on all threads created from the chat widget. You could, for example, set a label based on the page the user is on.
### Thread details code
```js theme={null}
Plain.init({
// ...Other options
threadDetails: {
// Optional. Labels to be set on created threads.
// To find a label id open the Plain app and go to 'Settings' -> 'Labels', you can select `Copy label ID` from the overflow menu on each label
labelTypeIds: ['lt_01JDAB92EBHP3DSXS43DW96WBS'],
// Optional. Priority to set on created threads (1 = urgent, 2 = high, 3 = normal, 4 = low)
priority: 3,
// Optional. A tier to be set on created threads.
// To find a tier ID open the Plain app and go to 'Settings' -> 'Tiers'. Select a tier and then copy the ID from the URL.
// You can also specify a tier by its external ID e.g { externalId: 'example_external_id' }
tierIdentifier: { tierId: 'tier_01JDABCAZBDFKH7WA6WNBDSA2F' },
// Optional. A tenant to be set on created threads.
// To find a tenant ID open the Plain app and click 'Tenants' under 'Browse' in the left sidebar. Select a tenant and then copy the ID from the URL.
// You can also specify a tenant by its external ID e.g { externalId: 'example_external_id' }
tenantIdentifier: { tenantId: 'te_01HT539973HNVZFSDXWHPT8FH1' },
// Optional. An external ID to be set on created threads.
externalId: 'example_external_id',
// Optional. Thread fields to be set on created threads.
threadFields: [
{ key: 'my_string_field', type: 'STRING', stringValue: 'some value' },
{ key: 'my_bool_field', type: 'BOOL', booleanValue: true },
{ key: 'my_enum_field', type: 'ENUM', stringValue: 'option_1' },
]
}
});
```
### Chat buttons
You can configure the primary chat button and any additional chat buttons that appear on the intro screen with text and an icon using the `chatButtons` array.
You can also pass `threadDetails` to a chat button to set fields on the thread when the user creates a chat from that specific button. If you provide the same field in both the top-level `threadDetails` and a chat button, any single value fields will be overridden by the chat button and any multi-value fields will be appended to.
### Chat buttons code
```js theme={null}
Plain.init({
// ...Other options
chatButtons: [
{
// Optional. The name of the icon to display, see above for full options
icon: 'bulb',
// The text to display on the button
text: 'Give feedback',
// Optional. Allows you to set fields for the threads which are created with this button. See above for full options.
threadDetails: {},
// Optional. Form elements which the user must fill out before sending a chat message. See below for full options.
form: {},
}
]
})
```
### Chat forms
Similar to chat buttons, chat forms allow you to set fields on the thread when the user creates a chat. These are specific to each `chatButton`.
### Chat forms code
```js theme={null}
Plain.init({
// ...Other options
chatButtons: [
{
// ...Other chat button options
form: {
// The fields which make up the form. All form fields must be filled in for a user to send a chat message
fields: [
{
type: 'dropdown',
// Optional. The placeholder text displayed on the dropdown
placeholder: 'Select a topic...',
// The options which are available in the dropdown, minimum of 1 required.
options: [
{
// Optional. An icon to be displayed on the dropdown option. See above for full options.
icon: 'bug',
// The text which is displayed for this dropdown option
text: 'Bug report',
// Optional. Enables setting values on threads when this option is selected by the user. See above for full options
threadDetails: {},
}
],
},
{
type: 'text',
placeholder: 'Your order number',
// The 'key' of the thread field that will be set
threadFieldKey: 'order_number'
}
]
},
}
]
})
```
### Additional methods
The Plain object has additional methods to provide more control over the chat widget. This may be helpful to keep the chat widget in sync with your existing application state.
### Additional methods code
```js theme={null}
// This takes the same arguments as Plain.init. It will update the chat widget in-place with the new configuration.
// Only top-level fields are updated, nested fields are not merged.
Plain.update({ ... });
// This takes the same arguments as `customerDetails` in Plain.init.
// This will update just the customer details in the chat widget. This may be useful if you have asynchronous authentication state
Plain.setCustomerDetails({ ... })
// Opens and closes the widget if using the default, floating mode
Plain.open();
Plain.close();
// These are event listeners that will be fired when the chat widget is opened or closed respectively
// These return a function that can be called to remove the listener
Plain.onOpen(() => {
// Opened
});
Plain.onClose(() => {
// Closed
});
// Returns whether or not the chat widget is initialized
Plain.isInitialized();
// This returns an array with debug logs that have been collected by the chat widget
// This is useful if you are contacting Plain support with an issue regarding the chat widget
// This will redact sensitive information such as customer details
Plain.exportDebugLogs();
```
# Contact forms
Source: https://www.plain.com/docs/product/channels/contact-forms
Define exactly what a customer submits, and turn each submission into a structured thread.
Contact forms give you full control over how customer requests enter Plain. They let you pre-triage, structure, and prioritize conversations. Contact forms allow you to:
* Collect upfront information so threads get triaged to the right person or team, automatically.
* Add labels, SLAs, and thread metadata during submission so high-value requests get surfaced immediately.
* Gather key product or account details without follow-up questions, so your team can jump straight into solving.
## Getting started with contact forms
Plain does not provide any UI components or a drop-in script tag. Instead, you use your own UI components and then use Plain's API. Our [Typescript](/docs/graphql/sdk) [SDK](https://github.com/team-plain/typescript-sdk/) is a great place to get started.
When a contact form is submitted, you first [**create the**](/docs/graphql/customers/upsert) [**customer**](/docs/graphql/customers/upsert) in Plain and then [**create**](/docs/graphql/threads/create) [**a thread**](/docs/graphql/threads/create) in Plain.
Depending on your desired behavior, you can also do other things as part of the form submission, such as:
* [Add the customer to a](/docs/graphql/customers/customer-groups) [customer](/docs/graphql/customers/customer-groups) [group](/docs/graphql/customers/customer-groups)
* [Set](/docs/graphql/labels/add)[labels](/docs/graphql/labels/add)
* Add a priority to a thread
A contact form can take any shape, and can ask questions specific to your product.
### Examples you can customize
#### Floating contact form
This shows a floating contact form (bottom right) built in **Next.js**.
[**View demo ↗**](https://example-nextjs-floating-form.vercel.app/) | [**View source on Github↗**](https://github.com/team-plain/example-nextjs-floating-form)
*Screenshot of the floating contact form*
#### Advanced contact form
This example uses structured inputs based on the topic and includes built-in categorization logic.
[**View demo ↗**](https://example-nextjs-advanced-contact-form.vercel.app/) | [**View source on Github↗**](https://github.com/team-plain/example-nextjs-advanced-contact-form)
Our Contact forms are available on all pricing plans: Foundation, Horizon, and Frontier.
# Create a thread from a Slack message
Source: https://www.plain.com/docs/product/channels/create-from-slack
Create a thread from a top-level Slack message in a connected customer channel.
`createThreadFromSlackMessage` creates a thread from a top-level message in a connected customer Slack channel. The thread includes the replies Plain has already received for that message. It is the trigger for [API-only ingestion](/docs/product/channels/slack-ingestion-modes#api-only), and it works in every other [ingestion mode](/docs/product/channels/slack-ingestion-modes) too.
The channel must already be connected in **Settings → Slack**, enabled, and set as a customer channel. Pass `slackChannelId` and `slackMessageTimestamp`. Replies are rejected.
You need an API key with the `thread:create` permission.
## Getting the created thread
Ingestion runs asynchronously. The mutation returns `thread` only when that Slack message was already a thread. When the request is accepted and the thread is still being created, `thread` is null and `error` is null.
Fetch the thread with `threadBySlackPermalink`, or subscribe to [`thread.thread_created`](/docs/webhooks/thread-created). Calling the mutation again for the same message returns the existing thread rather than creating a second one.
`threadBySlackPermalink` returns null until the thread exists. The permalink is the Slack link to the top-level message.
## Messages the mutation rejects
The mutation returns `cannot_create_thread_from_slack_message` when:
* The Slack channel is not connected to this workspace
* The connected channel is disabled
* The connected channel is not a customer channel
* The message is a reply rather than a top-level message
## After the thread exists
Replies in that Slack thread attach to the Plain thread. New top-level messages in an API-only channel do not become threads until you call the mutation again.
# Customize your Slack bot
Source: https://www.plain.com/docs/product/channels/customize-your-slack-bot
Change the name and logo your Slack bot uses when it posts on your behalf.
Your Slack support experience should feel like an extension of your brand, not a third-party tool. Plain lets you customize your Slackbot name and logo, so every interaction feels familiar, professional, and on-brand.
## Customizing your Slack bot
Go to **Settings → General** in Plain, upload a logo, and set your public name. This is your company name.
In Slack, go to **Configuration → Bot User**, click **Edit**, and change the name.
The name set in Plain's workspace settings takes precedence over the one set in Slack.
# Discord
Source: https://www.plain.com/docs/product/channels/discord
Ingest threads from a private server or public community through Plain's Discord integration.
Whether you run a private server or a public community for feedback, Plain's Discord integration brings Discord conversations into the same queue as every other channel.
## Getting started
Go to **Settings → Channels → Discord** and click **Connect to Discord**. Choose the server you want to use, then complete the Discord authorization.
Check that the Plain bot shows as online in your server before continuing.
Add the Plain bot to every forum you want support coverage in. It only ingests from forums it has been added to.
On the Discord integration page in Plain, click **Connect your profile**. Each person on your team does this once, so their replies post to Discord under their own name.
### How it works
* **Thread ingestion:** You can configure which channels to ingest from the Discord settings page in Plain. All channels will be **off by default**, and you can **toggle on** the ones you'd like to ingest threads from. Only threads from **forum channels** are ingested. Text channels and private threads are not supported.
* **Responding to threads:** You can reply to Discord threads **directly from Plain**. Responses appear in the associated Discord thread under the Plain bot, keeping the communication loop connected.
### Reporting for Discord
Discord threads are included in your workspace's Reporting section. Metrics include total volume, time to first response, time to close, and team performance, filtered by Discord as a channel.
### Auto-responders for Discord
Auto-responders can be configured to reply automatically to Discord threads based on conditions like time of day, channel, or message content. To create one, go to **Settings → Auto-responders**, select "Discord" as the channel, and define your message and criteria.
### Webhooks for Discord
Webhooks support Discord events, enabling advanced workflows and integrations. The Discord-specific event types you can subscribe to are:
* `thread.discord_message_received`
* `thread.discord_message_sent`
* `thread.discord_message_updated`
General thread events such as `thread.created` and `thread.status_transitioned` are also available. Common use cases:
* Routing threads to specific users
* Auto-labeling based on message content
* Triggering incidents from high-priority messages
* Syncing support metrics with internal dashboards
Webhooks are managed in **Settings → Webhooks**. Full webhook docs are available [here](/docs/webhooks).
### CSAT for Discord
You can natively send CSAT surveys to customers who have submitted a request through Discord. See the [CSAT article](/docs/product/platform/csat) for more information.
### Known limitations
* Support for showing and sending **mentions** and **reactions** in Plain
* Displaying **avatars** from Discord as the customer avatar in Plain
* Verifying and connecting Discord users to existing Plain customers via their email addresses
Discord is available on our Horizon and Frontier pricing tiers.
# Email
Source: https://www.plain.com/docs/product/channels/email
Set up email in Plain in two steps: sending through DNS records, and receiving by forwarding.
Getting email working in Plain takes two steps: setting up sending (DNS records) and setting up receiving (email forwarding). Once done, every customer email lands in Plain as a structured thread with assignments, SLAs, and full context.
## What you'll need
* **Admin access to your DNS settings**
Required to add two DNS records for email authentication.
* **Admin access to your email provider**
Required to configure forwarding. For example, if you use Google Workspace, you'll need access to the Admin Console.
## Choosing a support email address
Your support email must use your company's own domain. Public providers like `@gmail.com`, `@yahoo.com`, or `@icloud.com` are not supported.
Good options:
* `support@yourcompany.com`
* `help@yourcompany.com`
* `hello@yourcompany.com`
* `contact@yourcompany.com`
Personal addresses like `sonia@yourcompany.com` can work, but don't scale as well or communicate team-based support.
## Setup steps
Complete these in order:
1. **Set up email sending**. Add two DNS records so Plain can send authenticated emails from your domain. We recommend completing this step first; DNS changes need time to propagate before receiving will work correctly.
2. **Set up email receiving**. Forward emails from your support address to Plain so inbound messages appear as threads.
## Optional configuration
Once the basics are working, you can also:
* Add **alternate addresses** (e.g. `billing@`, `security@`) to handle multiple inboxes from one workspace
* Set up **BCC addresses** to automatically archive all outbound emails to customers
* Use **email discussions** to loop in colleagues or external parties on any thread without the customer seeing
# Alternate email addresses
Source: https://www.plain.com/docs/product/channels/email-alternate-addresses
Manage multiple support email addresses from a single Plain workspace.
You can configure up to 25 alternate email addresses in Plain, letting you manage multiple support inboxes (e.g. `support@`, `billing@`, `security@`) from a single workspace.
If you need separate queues across two workspaces on the same domain, this won't apply, reach out to [support@plain.com](mailto:support@plain.com) to set it up.
## How to set up alternate addresses
Go to **Settings → Email** and scroll to the Alternate addresses section.
Each alternate address must:
* Use the **same domain** as your primary support address. For example, if your primary is `help@acme.com`, valid alternates include `billing@acme.com`, but not `billing@acme.io`.
* Be **forwarded to Plain** the same way as your primary address. By setting up a forwarding rule in your email provider or domain registrar pointing to your Plain inbound address. No additional DNS records are required; the DNS setup you completed for your primary address covers all addresses on the same domain.
## How alternate addresses work
Once configured:
* Plain automatically replies from the address the customer originally contacted. A thread started by emailing `billing@acme.com` will use that address on all replies by default.
* You can manually select any of your configured addresses when composing a reply, if you need to send from a different one.
* The thread shows which address the customer contacted, so context is always clear to your team.
## Why use alternate addresses
* **Unify your workflows**: manage all incoming support emails in one place without switching tools or accounts
* **Keep communication consistent**: customers always receive replies from the address they used, automatically
* **Maintain branding across departments**: use purpose-specific addresses like `billing@` or `security@` without complicating your setup
# Email avatars
Source: https://www.plain.com/docs/product/channels/email-avatars
How email clients decide which avatar to show next to your emails, and what you can control.
The avatar shown next to your emails in a customer's inbox is controlled entirely by their email client, not by Plain. Plain sends clean, authenticated messages and does not embed avatars in outgoing emails.
That said, there are steps you can take to influence what email clients display.
## BIMI (Brand indicators for message identification)
BIMI lets you display your brand logo next to emails by adding a DNS record pointing to an SVG file of your logo. Gmail and several other providers support it, with caveats:
* To guarantee your logo appears in Gmail, you need a **Verified Mark Certificate (VMC)**: a third-party certification that costs around **200/year**.
* Without a VMC, some clients may still show your logo, but support is inconsistent.
**Best for:** Teams investing in high-trust branding who work with enterprise customers.
## Gravatar
Some email clients display avatars based on the sender's email address using [Gravatar](https://gravatar.com). You can create a free Gravatar profile for your support email address and, where supported by the recipient's client, your chosen image will appear.
**Best for:** Lightweight branding when BIMI isn't feasible.
## Gmail profile picture
If your support email is managed through Google Workspace, Gmail may display the profile picture associated with that account. You can update it from Gmail settings or through the Admin Console. This only affects how your email appears in Gmail clients.
**Best for:** teams on Google Workspace who want avatar control without extra setup.
# BCC email addresses
Source: https://www.plain.com/docs/product/channels/email-bcc-addresses
BCC every outbound email to an address of your own, to keep a copy outside Plain.
If you want to keep a record of all the emails your team send to customers, you can set up **BCC email addresses**.
## How it works
When you configure BCC email addresses in your workspace's **Email Settings**, we'll automatically include them in the BCC field of every outgoing message to at least one customer.
This applies to all emails sent from your workspace members to customers. You can use up to five different email addresses.
## Why is this useful
You could use this feature in order to maintain an archive of all customer communications for compliance or auditing purposes. You could also feed all outbound emails to an external system, like a CRM.
# Email deliverability troubleshooting
Source: https://www.plain.com/docs/product/channels/email-deliverability-troubleshooting
How Plain handles email delivery, retry logic, bounce types, SMTP error codes, and common troubleshooting gotchas.
If you are experiencing errors when sending an email, we first recommend checking out our set up guide for sending emails with Plain.
## Best practices
### Warm-up your domain
A common cause of email deliverability issues is trying to send too much, too soon. When you have only recently onboarded a new vendor like Plain to send emails, we recommend slowly ramping up your activity.
### Use a sub-domain for non-Support email sends
Use a subdomain (like `marketing.atrium.co`) for non-Support customer communication.
## Known errors
### 550 DMARC check failed
Occurs if your SPF record has the `-all` strict flag.
### 550 5.7.515
This error occurs if you send more than 5000 emails to Microsoft Consumer Inboxes using the same 5322.From address.
### 550 5.7.26
Returned when there is a misconfiguration in your SPF, DKIM or DMARC records.
# Email discussions
Source: https://www.plain.com/docs/product/channels/email-discussions
Loop other people into a thread over email, without exposing them to the customer.
**Email discussions** let you loop additional people into a thread via email: whether the thread came in through Slack, MS Teams, email, or another channel. They are part of a broader feature called Thread Discussions.
Thread discussions are **never visible to the customer** the thread belongs to. They are internal by default.
## Prerequisites
Email discussions require the Email channel to be configured in your Plain workspace. If you can already send and receive support emails in Plain, you're good to go.
## Starting an email discussion
1. Open a thread and click the **+** icon in the sidebar next to **Thread Discussions**. You can also press **D** on your keyboard.
2. In the modal that opens, select **Email** as the channel. If you have Slack connected, you'll see Slack channels listed alongside the Email option.
3. Enter your recipients and write your message.
4. Send. Recipients will receive an email containing your message, with all previous thread messages appended below: similar to a forwarded email in their inbox.
The thread history is only appended to the first message of the discussion. Subsequent replies won't re-include it.
## Replies
When a recipient replies, their response appears in the **Thread Discussions** sidebar and in the thread timeline alongside all other thread activity. These replies remain internal. They are never shown to the customer.
# Set up email receiving (forwarding)
Source: https://www.plain.com/docs/product/channels/email-receiving
How to set up email forwarding into Plain, manage email copies, and understand spoofing detection.
To receive customer emails in Plain, you need to forward messages from your support address (e.g. `support@example.com`) to your Plain workspace's inbound address. Once set up, every customer email will appear in Plain as a thread.
You can find your Plain inbound address in **Settings → Email** in your workspace.
**CC'd recipients** on inbound emails are not currently displayed in Plain or preserved on outbound replies. If you need to keep other parties in the loop, consider using BCC email addresses (see below).
## Google Workspace
1. [Open the Gmail routing settings](https://admin.google.com/u/0/ac/apps/gmail/routing) in your Admin Console.
2. Click **Email forwarding using recipient address map** → **Configure** or **Add another rule**.
3. Configure the forwarding rule
* Pick a name for the forwarding rule. For instance: `Plain forwarding`
* Enter your support email under 'Address' (e.g. `support@example.com`)
* Enter your Plain inbound email address under 'Map to address' (e.g. `abcdefg@inbound.postmarkapp.com`)
* Choose 'All incoming messages'
4. If you also want emails to stay in your original inbox, check **Also route to original destination**.
Enable this if your support address points to a Google Group and you still want messages delivered there.
5. Click **Save.**
### Microsoft 365
1. Sign in to the [Microsoft 365 Admin Center](https://admin.microsoft.com).
2. Go to **Exchange admin center → Mail flow → Rules**.
3. Click **Add a rule → Create a new rule**.
4. Set the condition: **The recipient is** → your support email address.
5. Set the action: **Redirect the message to** → your Plain inbound address.
6. Save and enable the rule.
## Other providers
Forwarding can also be configured through your domain registrar (e.g. DNSimple, Namecheap) or any other email host that supports forwarding rules. The goal is the same: any email sent to your support address should be forwarded to your Plain inbound address.
If you need help configuring this, [get in touch](mailto:help@plain.com).
## Archiving inbound emails (Google Workspace)
To keep copies of all inbound emails for compliance or audit purposes, set up a secondary routing rule in Google Workspace that delivers a copy to a separate archive address.
1. Go to [Default Routing](https://admin.google.com/u/0/ac/apps/gmail/routing) in Gmail settings.
2. Click **Configure** or **Add another rule**.
3. Under **Specify envelope recipients to match**, choose **Single recipient** and enter your support email (e.g. `support@example.com`).
4. Scroll down, tick **Add more recipients** under **Also deliver to**. Click **Add** and enter a dedicated archive address (e.g. `support.archive@example.com`).
5. Click **Save**, then scroll down and select **Perform this action on non-recognized and recognized addresses**. Click **Save** again.
Always use a separate address for your archive. Never the same as your support inbox, or it may cause issues processing emails in Plain.
## Archiving outbound emails (BCC)
To keep a copy of all emails your team *sends* to customers, set up BCC email addresses in **Settings → Email**.
Plain adds your configured BCC addresses to every outbound message sent to a customer, without affecting the email your customer receives. This applies to messages sent by workspace members, not system notifications.
You can configure up to five BCC addresses. They cannot be the same as your primary support address or any alternate address.
Common uses:
* Compliance and audit archives
* Feeding outbound emails into a CRM or external system
## Spoofing detection
Plain runs authentication checks on every inbound email to estimate whether it was genuinely sent by the address it claims to be from. Each email is classified as `PASS`, `FAIL`, or `UNKNOWN`.
The checks use SPF, DKIM, DMARC, and related forwarding headers. When those are missing or inconclusive, we use a leading email spam platform as a fallback signal.
* **FAIL**: checks strongly suggest the sender could be spoofed
* **UNKNOWN**: not enough data to make a confident decision
For both `FAIL` and `UNKNOWN`, Plain displays a warning banner on the thread. The safest verification step, especially on a `FAIL`, is to reply and confirm the sender can respond from the same address.
# Set up email sending (DNS records)
Source: https://www.plain.com/docs/product/channels/email-sending
Add the DNS records Plain needs to send email from your own domain.
To send emails from your company's domain (e.g. `support@yourcompany.com`) through Plain, you need to add two DNS records to your domain. This is a required step before you can enable the email channel. It ensures your messages are authenticated and reliably reach your customers' inboxes.
## Step 1: Get your DNS record values from Plain
Go to **Settings → Email** in your Plain workspace. Plain shows two DNS records for you to copy:
1. A **TXT record**: enables DKIM, which cryptographically authenticates your emails
2. A **CNAME record**: passes SPF checks
Keep this page open while you complete the next step.
## Step 2: Add the records to your DNS provider
Follow the guide for your provider. The field names vary slightly, but you're always adding the same two records.
### DNSimple
1. Go to [dnsimple.com/dashboard](https://dnsimple.com/dashboard)
2. Choose your domain → click **DNS → Manage**
3. Add a **TXT Record**: Name = Hostname value from Plain, Content = Value from Plain
4. Add a **CNAME Record**: Name = Hostname value from Plain, Content = Value from Plain
### Namecheap
1. Go to [namecheap.com/domains/list](https://ap.www.namecheap.com/domains/list/)
2. Select your domain → click **Manage → Advanced DNS**
3. Add a **TXT Record**: Host = Hostname value from Plain, Value = Value from Plain
4. Add a **CNAME Record**: Host = Hostname value from Plain, Value = Value from Plain
5. Click **Save all changes**
### GoDaddy
1. Go to [account.godaddy.com/products](https://account.godaddy.com/products)
2. Select your domain → click **DNS**
3. Add a **TXT Record**: Name = Hostname value from Plain, Content = Value from Plain
4. Add a **CNAME Record**: Name = Hostname value from Plain, Content = Value from Plain
5. Confirm and save
### Cloudflare
1. Go to [dash.cloudflare.com](https://dash.cloudflare.com) and select your domain
2. Click **DNS → Records → Add record**
3. Add a **TXT Record**: Name = Hostname value from Plain, Content = Value from Plain
4. Add a **CNAME Record**: Name = Hostname value from Plain, Target = Value from Plain
5. Set **Proxy status** to **DNS only** (grey cloud) on the CNAME record. Proxied mode will break email authentication
6. Save both records
### AWS route 53
1. Open the [Route 53 console](https://console.aws.amazon.com/route53/) and select your hosted zone
2. Click **Create record**
3. Add a **TXT Record**: Record name = Hostname value from Plain, Value = Value from Plain
4. Add a **CNAME Record**: Record name = Hostname value from Plain, Value = Value from Plain
5. Save changes
Using a different provider? The process is the same: add one TXT record and one CNAME record using the values shown in Plain's **Settings → Email** page.
## About email deliverability
We don't ask you to modify your SPF record directly. Instead, the CNAME record you add resolves through Postmark's infrastructure, which handles SPF automatically.
When a receiving mail server processes an email sent from Plain, it:
* Verifies the DKIM signature using the TXT record
* Verifies SPF by checking the `Return-Path` header, which resolves to Postmark's authorized sending IPs
* Checks DMARC alignment when your domain requires it
If your domain has a strict SPF record ending in `-all`, some providers may reject emails from Plain. Fix this by adding `include:spf.mtasv.net` to your existing SPF TXT record.
If you require DMARC **strict** alignment, where the `Return-Path` domain must exactly match the `From` domain, get in touch and we can discuss options.
## How long does it take?
DNS changes propagate within **10 minutes** in most cases, but can take up to **24–48 hours** depending on your provider and TTL settings. Once Plain detects the records, your email channel will activate automatically.
# Spam & auto-reply detection
Source: https://www.plain.com/docs/product/channels/email-spam-and-auto-reply-detection
Mark senders as spam, filter spam with AI, and how Plain detects auto-replies.
Plain filters two kinds of unwanted email: senders you mark as spam, and automated replies it detects on the way in. You can also use AI to catch spam that gets past both.
## Marking customers as spam
To mark a sender as spam, use a keyboard shortcut:
* Go to one of the threads associated with the spam address.
* Press ⌘ + K and select **Mark customer as spam**.
Once marked, threads will still be created but will be ignored, meaning you won't receive notifications or see them in your To-Do list.
You can see customers that you've previously marked as spam from **Settings → Spam.**
## Using AI to ignore spam
The [AI prompt condition](/docs/product/workflows/workflows-conditions) in workflows can act on the content of a thread, not only its metadata, and it's well-suited to detecting spam.
### Setting up a spam detection workflow
The AI condition prompt is a free-text field (up to 2,500 characters). A well-written prompt is the most important part of making this reliable.
**Example prompt:**
> This thread is spam or should be silently ignored. Examples include: unsolicited sales or partnership outreach, automated phishing or credential-harvesting messages, irrelevant mass marketing unrelated to our product, test messages with no genuine support intent, or messages that appear to be sent to the wrong company entirely. **If you are uncertain whether this is spam, treat it as spam and return true.**
Your workflow structure would be:
1. **Trigger:** Thread created (or Message added, if you want to catch spam that arrives as a follow-up)
2. **Condition:** AI prompt condition with your spam criteria
3. **Action:** Set Status → Ignored
### What this approach handles well
* **Text-based outreach spam:** Sales emails, partnership pitches, automated marketing blasts: anything where the message content reveals the intent.
* **Misdirected messages:** Threads clearly sent to the wrong company or product.
* **Phishing and credential requests:** Messages asking for login details, wire transfers, or similar.
* **Pattern-based spam:** If your spam follows recognizable patterns, you can describe them explicitly in the prompt and the model will apply them consistently.
- **Attachment content is not analyzed**
- **No external reputation signals**: the AI condition works exclusively with data Plain has stored about the thread and customer. There is no access to email headers, sender IP reputation, domain blocklists, or DMARC/SPF results. If you need header-level or network-level filtering, that has to happen upstream at your email provider before the thread reaches Plain.
- **Already-flagged spam customers are handled separately**: if a customer has already been marked as spam in Plain, workflows do not run for their new threads. They are caught earlier in the pipeline and their threads are automatically closed as ignored.
## Auto-reply detection
When emails are forwarded into Plain, every inbound message is automatically scanned before a thread is created. If Plain detects that an email is an auto-reply, such as an out-of-office response or any other automated message, it is filtered out. No thread is created and no notifications are sent.
This prevents common feedback loops where automated systems reply to each other indefinitely, and keeps your queue free from noise.
### What Plain detects
Plain checks a combination of standard email headers and subject line patterns to identify auto-replies.
**Email Headers**
* `Auto-Submitted` (RFC 3834): the industry standard for marking automated messages
* `Precedence: auto_reply` or `Precedence: bulk`
* `X-AutoReply`
* `X-Autorespond`
* `X-Amazon-Auto-Reply`
* `X-amzn-vacation` (Amazon WorkMail)
* `X-SFDC-AutoResponse` (Salesforce)
* `X-ServiceNow-Generated`
* `X-Front-Autoreply` (Front)
* `X-CodeTwo-AutoResponse` / `X-C2AutoRespond`
* `X-MDAutoResponse`
* `X-QQ-Auto-Reply`
* `X-MS-Exchange-Generated-Message-Source`
#### Subject line patterns
Plain also recognizes common auto-reply subject prefixes across more than 20 languages, including:
* **English:** "Automatic reply:", "Out of office:", "Autoreply:", "Auto response:"
* **German:** "Automatische Antwort:", "Abwesenheitsnotiz:"
* **French:** "Réponse automatique :"
* **Spanish:** "Respuesta automática:"
* **Japanese:** "自動応答:"
* **Chinese:** "自动答复:", "自動回覆:"
* And many more (Polish, Czech, Turkish, Arabic, Hebrew, Greek, Russian, and others)
Subject line patterns alone are not sufficient to filter an email. Plain requires at least one supporting header signal alongside a matching subject to reduce the chance of false positives.
### What happens to filtered emails
Emails identified as auto-replies are filtered out before a thread is created. They do not create threads, trigger workflows, or generate notifications. Filtered emails are not stored in Plain, so there is no self-serve way to review or recover them from workspace settings. If you think a legitimate message was filtered by mistake, [contact us](mailto:support@plain.com).
### If auto-replies are slipping through
Some automated systems do not include standard auto-reply headers. In these cases, Plain cannot identify the message as automated and will create a thread as normal.
The best workaround is to set up a **workflow rule** that catches these threads after creation:
1. Go to **Settings → Workflows** and create a new rule.
2. Set the trigger to **Thread created**.
3. Add a condition using **Message contains** with phrases typical of automated acknowledgements. For example: *"your request has been received"*, *"a ticket has been created"*, *"this is an automated response"*.
4. Optionally, add a **Sender email includes** condition to scope it to known helpdesk addresses.
5. Set the action to **Mark as Done**.
This combination keeps your queue clean without affecting legitimate threads from the same senders.
If you are seeing a high volume of looped threads from a specific source, [contact us](mailto:support@plain.com). We may be able to add the sender's headers to Plain's detection rules.
# Microsoft Teams
Source: https://www.plain.com/docs/product/channels/microsoft-teams
Sync messages from selected Microsoft Teams channels into Plain and reply from the queue.
Our Microsoft Teams integration lets you sync messages from selected Teams channels to Plain and respond directly to customers from the platform.
Microsoft Teams is only available on the [**Horizon and Frontier pricing plans**](https://www.plain.com/pricing).
A short demo of the Microsoft Teams integration:
## Get started
### Prerequisites:
* A Microsoft Teams account
* A Microsoft 365 admin who can grant OAuth admin consent for the Plain app. This is required to connect your workspace to Plain
## Notes
### Initializing a conversation from Plain
In addition to responding to customer messages, you can also create a Teams conversation from Plain by using our "Create Thread" functionality.
To create a thread, open the command palette (cmd / ctrl + k) and select "Create Thread". Select the channel type ("MS Teams"), the Teams channel, and a title for the thread.
### Replying as
By default, Plain sends messages to Teams as the Plain bot. If you want messages to appear as sent by you personally, you can authorize your individual Teams account. Go to **Settings → Microsoft Teams** and click **Authorize replies** to link your personal Microsoft account. This is also required to @mention other Teams users directly from Plain.
### @-Mentions
To mention another user directly from Plain in a Teams message, you must have the Teams user integration set up via **Authorize replies** (see above).
# Initial setup
Source: https://www.plain.com/docs/product/channels/microsoft-teams-installation
Install the Plain app in Microsoft Teams and connect your first channels.
To setup Plain in your own Teams channels, you'll need to do the following.
## Configuration
* In Plain, go to **Settings** → **Microsoft Teams**.
* Press `Connect to Teams`.
* You will be redirected to Microsoft to complete an OAuth admin consent flow. Sign in with a **Microsoft 365 administrator account** and grant the requested permissions.
## Add Plain to channels
Next, add the Plain app to Teams by installing it from the [Microsoft Teams App Store](https://teams.cloud.microsoft/l/app/03035b20-fa14-4893-baa4-380f6ad971ad?source=app-details-dialog).
Plain can be added to **standard** channels, **chats** channels, as well as **shared** channels.
The app can be added to channels by navigating to the "…" overflow menu in the channel list next to either an individual channel name or a whole Team group of channels. From there, select "Manage Team / Channel" and then go to the "Apps" tab. Next, select "Get more apps" and search for and select Plain.
## How Plain threads are created from Teams
Plain creates threads differently depending on whether a message comes from someone with an account who's email address matches the domain you're using in Plain, or any other user.
* **Team member messages**: Top-level posts from members of your team do not create a Plain thread on their own. A thread is created, however, if someone not on your team replies to that post: at which point Plain backfills the entire conversation (the original post and all replies) into a new thread.
* **Other messages**: Any top-level message from a user not on your team will immediately create a new thread in Plain.
## Authorize Teams users in Plain
By default, responses are sent from the Plain bot and will appear with a header indicating which support agent is responding.
To send replies from your own Teams user instead, link your Microsoft account by navigating to **Settings → Microsoft Teams** and click **Authorise replies**. This must be done for each support agent in Plain, as they'll each want to use their own Teams accounts.
This is also required if you want to @-mention other Teams users directly from Plain. Replies from Plain with a linked "authorize replies" account will then appear transparently like any other reply in Teams.
# Migrating to the store app
Source: https://www.plain.com/docs/product/channels/microsoft-teams-migrating
Move from the manually uploaded Teams app to the Microsoft Teams Store version.
Plain is now published on the Microsoft Teams App Store. If you previously used the custom application method where you uploaded the .zip file, you can now migrate to the official store version of the Plain Teams application.
Using the App Store version of Plain ensures that you always receive the latest feature updates and bug fixes. However, the .zip installation will continue to work.
## Migrating from the zip to the app store version of Plain
Migrating to the App Store version requires no changes on the Plain side. While the old app is removed, and before you've installed the new one, your channel will not be sending any messages to Plain. The gap is short, but do this outside peak hours.
The Teams Store version has screenshots in its listing, as shown above. The manually uploaded .zip version does not.
Remove the old version of the .zip Plain app from each channel one-by-one and then add the new Plain app to that Teams team / channel again.
1. Go to the "…" overflow menu next to each Team or Channel name and select "Manage Team / Channel" and then selecting the "Apps" tab. There you can remove the old application and add the new one
2. After adding the new Store-based application to all of your channels, your administrator can uninstall the custom application in the [Teams Admin portal](https://admin.teams.microsoft.com/policies/manage-apps).
Once you have updated the application in every Teams channel, the migration is complete.
# Shared channels
Source: https://www.plain.com/docs/product/channels/microsoft-teams-shared-channels
Support customers through Microsoft Teams shared channels.
Plain supports helping your customers via Microsoft Teams Shared Channels.
## Getting started
Providing support via Teams "Shared" channels requires you to add the Plain application to your shared channel specifically. Depending on your Azure security settings, this might require a Teams administrator.
Ensure you have our Teams application version `1.0.8` or greater installed. This will automatically be the case if you use our [published Teams App Store application](https://teams.cloud.microsoft/l/app/03035b20-fa14-4893-baa4-380f6ad971ad?source=app-details-dialog). If you are still using the uploaded zip, you can check the current version in the [Teams Admin Portal](https://admin.teams.microsoft.com/policies/manage-apps).
1. Add the Plain bot to your shared channel by clicking on the overflow menu of your shared channel and selecting "Manage channel".
2. Go to the "Apps" tab (this might be hidden behind the "+" in the channel view header) and check the box next to "Plain" and select "Add" to add the application to the channel. Ensure the "Added to Channel" column says "Yes".
3. Any new threads authored in that shared channel by a user who is not also a Plain user (i.e. any messages not from your support agents) will create a new Plain thread.
## Troubleshooting
If you cannot invite users from a particular Azure tenant to your shared channel, ensure that Azure B2B Direct Connect security settings are configured on both your and your partner's Azure Entra settings. Both must be set to allow one another. See the [Microsoft documentation](https://learn.microsoft.com/en-us/entra/external-id/b2b-direct-connect-overview) for further info.
If messages don't appear in Plain from your shared Teams channel, you may need to reauthorize the Plain workspace integration:
* Go to Plain channel settings for Microsoft Teams and click "Disconnect", then "Reconnect" next to your workspace integration.
* When reconnecting, sign in with a Teams Administrator Microsoft account.
* Clicking "Disconnect" will not delete any threads or existing data. While disconnected, Plain does not pick up new Teams messages. Reconnecting should only take a few seconds.
# Slack
Source: https://www.plain.com/docs/product/channels/slack
Sync messages from selected Slack channels into Plain as threads, and reply from Plain without leaving the queue.
Plain syncs messages from the Slack channels you choose into Plain as [threads](/docs/product/platform/threads/what-is-a-thread). Your team triages and replies from Plain, and the reply posts back to Slack. Threads from Slack carry the same [labels](/docs/product/platform/labels), [SLAs](/docs/product/platform/slas), and [workflows](/docs/product/workflows) as every other channel, and can be linked to [Linear](/docs/product/integrations/linear), [Jira](/docs/product/integrations/jira), or [incident.io](/docs/product/integrations/incident.io).
## Set up Slack
In Plain, go to **Settings → Slack** and click **Connect to Slack**. Follow the prompts to authorize the right Slack workspace.
Grant permission for replies to be sent from your Slack profile. This is what lets Plain replies appear in Slack under your name.
In **Settings → Slack**, set up auto-join rules that match your channels by prefix or suffix.
To add a channel by hand instead, open it in Slack, type `/invite`, select **Add apps to this channel**, and choose **Plain**.
Each channel can be a Customer channel, a [Discussion](/docs/product/channels/slack-discussions) channel, or disabled. Set this in **Settings → Slack**.
Messages from connected channels appear as threads in Plain. Type your response in Plain and it posts back to Slack as if you typed it there.
[Ingestion modes](/docs/product/channels/slack-ingestion-modes) control which messages become threads, so a busy channel does not have to create one per message.
## Manage connected channels
**Settings → Slack** lists every channel the Plain bot has joined, one list per connected Slack workspace. Each row sets that channel's type and [ingestion mode](/docs/product/channels/slack-ingestion-modes).
Three controls sit above the list:
* **Search channels**: filters the list by channel name
* **Type**: narrows the list to **Customer**, **Discussion**, or **Disabled** channels
* **Ingestion mode**: narrows the list to one or more modes, including **Team default** for channels with no override
The count beside those controls, such as `11 of 34`, is how many rows your current search and filters show out of the total. It is not a quota. Your plan caps enabled customer channels separately, and Plain shows a notice at the top of the page once you reach that cap.
Select rows with the checkboxes to change the type or ingestion mode of several channels at once. A private channel you are not a member of appears locked, so ask to be invited in Slack before you manage it.
## Sidekick in Slack
[Sidekick](/docs/product/agents/sidekick), Plain's assistant agent, works from Slack. Mention Plain in a connected channel and it answers there, with access to your knowledge sources, tool integrations, and support history.
Sidekick posts under its own identity and avatar, so it is clear when a reply came from AI rather than a person.
To enable it, a workspace Admin or Owner goes to **Settings → AI → Sidekick → Integrations → Slack**. Plain then creates an `#ask-plain` channel. Sidekick is active in every discussion channel and never in a customer channel, so to add it to a channel such as `#engineering` or `#incidents`, invite the Plain bot there and set the channel's type to **Discussion** in **Settings → Slack**. See [Sidekick in Slack](/docs/product/agents/sidekick/in-slack) for the full setup.
## AI in Slack
Every AI feature in Plain works on Slack threads, the same as on any other channel. See [Ari](/docs/product/agents/ari) for customer-facing replies and [AI triage](/docs/product/platform/ai-triage) for automatic labeling and prioritization.
AI can generate inaccurate responses, so review them before they reach a customer. Every AI feature in Plain is opt-in.
## CSAT in Slack
You can send [CSAT surveys](/docs/product/platform/csat) to customers who contacted you through Slack, with no extra setup per channel.
## Internal collaboration
[Slack discussions](/docs/product/channels/slack-discussions) let you start an internal Slack thread with your team from a thread in Plain. When the team resolves it, mark the discussion resolved and the thread returns to **Close the loop** status, so the customer still gets an answer.
## Structured intake
[Slack contact forms](/docs/product/channels/slack-contact-forms) collect structured answers instead of a free-text message, and turn each submission into a thread with its fields already set.
## Automation
[Workflows](/docs/product/workflows) act on Slack threads like any other: auto-triage, prioritization, labeling, and routing all run on triggers and conditions you define.
## Security and data retention
Plain reads only from channels the bot has been invited to. [Slack data retention](/docs/product/security/slack-data-retention) sets out what Plain stores from those channels and when it is deleted, for IT and security review.
# Slack workflow forms
Source: https://www.plain.com/docs/product/channels/slack-contact-forms
Collect structured answers in Slack instead of free text, and turn each into a thread.
Slack contact forms allow you to create structured intake processes for customer threads. When integrated with Plain, these forms automatically convert Slack messages into Plain threads.
## Here's how to set this up
First, create a new Slack workflow that collects the information you need and make it post a message to the Slack channel:
Afterwards, the workflow will be triggered whenever your customer uses the form.
And this will create a thread in Plain once triggered:
# Slack digests
Source: https://www.plain.com/docs/product/channels/slack-digests
Post a recurring summary of your queue into Slack, so the team sees it without opening Plain.
From frontline responders to support leadership, Slack digests help teams to stay aligned and responsive, without constantly checking the queue. Delivered via Slack, they provide clear, timely updates tailored to how different team members work.
## Setting up a digest
1. Go to **Settings → Notifications → Slack**
2. Choose the **Digest type** you want to enable
3. Set the **delivery time (UTC)**
4. Toggle it on
## Available digests
### Daily standup
Gives your support team a focused view of what's waiting in their queue.
### Daily summary
Gives a high-level view of your support queue's overall health. Workspaces using [Ari](/docs/product/agents/ari) also see how many threads it handled and handed off in the last 24 hours.
### Themes
Surfaces the main topics coming up in your support threads.
# Slack discussions
Source: https://www.plain.com/docs/product/channels/slack-discussions
Start an internal Slack thread from a Plain thread, and bring the answer back.
**Slack Discussions** let you loop in product, engineering, or anyone else on your team directly from a support thread, without losing context or switching tools.
Available on the **Horizon plan and above**.
## How Slack discussions work
You can start a Slack Discussion by clicking **Start discussion** from inside a thread in Plain, or by pasting the thread link directly into a Slack channel where the Plain bot is installed.
Once a discussion has started, messages, reactions, and attachments added in Slack will also show up in the Plain thread. Once resolved, Plain automatically moves the thread to **Close the loop**.
# In-Slack actions
Source: https://www.plain.com/docs/product/channels/slack-in-slack-actions
Triage, assign, resolve, and snooze threads from inside Slack.
In-Slack actions help your team triage, assign, resolve, and investigate support threads without leaving Slack. You can take action instantly, create visibility on ownership and status, and now get AI assistance: all from inside the conversation.
## Use Slack actions to manage threads
The following actions can be performed directly from [a notification in your Slack channel](https://help.plain.com/article/notifications):
* **Reply in Slack**
Opens the original customer Slack thread so you can jump in instantly.
* **I'll take this**
Assigns the thread to you in Plain.
* **Status dropdown**
Lets you update the thread status directly from Slack. All your custom statuses (e.g. Investigating, Waiting on customer) are available in one click.
* **Snooze until tomorrow**
Snooze a thread so that you have a reminder to look at it later.
* **Set priority to urgent**
Automatically change a thread's priority to signal to your team that it needs immediate attention.
* **Open in Plain**
Go to the thread in Plain, directly from the Slack notification.
## Update thread status with emojis
You can also take action directly in the customer's Slack thread, using emojis on the top-level message.
* ✅ → Marks the thread as **Done** in Plain
* 👀 → Marks the thread as **Investigating**
Plain syncs these emoji reactions with thread status in real-time, but only when the setting is toggled 'On' from **Settings → Notifications.**
If the thread isn't already assigned and you react with ✅ or 👀, Plain auto-assigns it to you, as long as your role in Plain isn't *Viewer*.
## Ask Sidekick from Slack
You can invoke Sidekick, Plain's AI assistant, directly in a connected Slack channel by @mentioning it. Sidekick responds in the thread with full access to your knowledge sources, tool integrations (Linear, Datadog, GitHub, and more), and support history.
This means you can investigate, draft replies, and pull context from your connected tools without ever leaving Slack.
Sidekick in Slack is enabled by a workspace Admin or Owner in **Settings → AI → Sidekick → Integrations → Slack**.
# Ingestion modes
Source: https://www.plain.com/docs/product/channels/slack-ingestion-modes
Choose which Slack messages become threads, so a busy channel does not create one per message.
Plain gives you full control over how Slack messages are ingested and structured into support threads. This flexibility helps you reduce noise, improve triage, and adapt Slack-based support to match your unique workflows.
You can configure ingestion modes at two levels from **Settings → Slack**:
* **Workspace default**: applies to all channels unless overridden. Set this on the main Slack integration settings page.
* **Per channel**: override the workspace default for any individual channel. Each connected channel has its own ingestion mode selector in the channel list. Pick **Team default** to remove the override, or **Revert to team defaults** from the channel's overflow menu to clear its mode, emoji, and audience overrides at once.
Being able to customize ingestion is the difference between reactive support and proactive support. You get to shape Slack around your processes, not the other way around.
## Why your ingestion mode matters
The way support threads are created can make or break your team's ability to stay organized and responsive. With the right ingestion mode, you can:
* Avoid fragmented or duplicated threads
* Ensure high-priority messages are captured
* Filter out irrelevant Slack noise
## Ingestion modes explained
### Time-based (default)
**What it does:**
Groups messages into threads based on timing and sender.
* Messages from the **same user** within **2 hours** = 1 thread
* Messages from **different users** within **1 hour** = 1 thread
**Best for:**
Teams that want minimal setup and reasonable defaults, with no AI involved. Keeps Slack support flowing without needing AI or manual triage.
### AI-based (recommended)
**What it does:**
Groups related messages into threads based on meaning, not timing.
* Messages are semantically evaluated for similarity
* Grouped only if part of the same conversation
**Best for:**
Teams with complex, multi-topic conversations where time-based grouping falls short. This ingestion mode offers smarter grouping that understands context, reducing the likelihood of fragmented threads.
### One-to-one
**What it does:**
Creates a one-to-one link between Slack threads and Plain threads.
* Every new Slack thread = a new Plain thread
* Replies in Slack stay attached to the same Plain thread
**Best for:**
Teams that want strict mirroring of Slack threads in Plain. Perfect for customers who self-organize in Slack or when internal routing and ownership depend on thread boundaries.
### Manual emoji
**What it does:**
Only ingests Slack threads when a team member adds a specific emoji reaction (e.g. 👀).
* Only Plain users can trigger ingestion by default. Set the channel's audience to **Anyone** to let customers trigger it too
* Must be added to the top-level channel message
* Thread is added once per reaction. Removing it won't delete the Plain thread
**Best for:**
Noisy or general-purpose channels (e.g. Slack community) where only a few messages require action. Especially useful if the majority of messages in a Slack channel are not related to support and your team can flag the relevant threads.
### API-only
**What it does:**
Creates a thread only when you call [`createThreadFromSlackMessage`](/docs/product/channels/create-from-slack).
* Messages in the channel do not create threads on their own
* There is no ingestion emoji
* The channel does not post a public welcome message
* After a thread exists, replies in that Slack thread attach to it
**Best for:**
Workspaces whose own integration decides which Slack messages become threads. You can also call the mutation in any other ingestion mode: if the message is already a thread, the mutation returns that thread.
# Help Center
Source: https://www.plain.com/docs/product/help-center
A hosted Help Center with a knowledge base, Ask AI, and a customer inbox for tracking threads.
Plain's Help Center brings together everything your customers need into one fully integrated, branded experience. Powered by AI and tightly connected to your support workflows, it helps customers get unblocked fast, and gives your team the tools to provide great support as you scale.
## One workspace, multiple Help Centers
You can create as many Help Centers as you need from a single Plain workspace, each with its own domain, branding, access rules, and content. They all feed into the same support queue, so your team works from one place regardless of which Help Center a customer came from.
Common setups include:
* A **public Help Center** for general documentation, indexed by search engines and open to anyone
* A **customer-facing portal** requiring login, scoped to specific tiers or tenants (e.g. Enterprise customers only)
* An **internal knowledge base** for your support team, not visible to customers at all
* A **sales or onboarding hub** with content tailored to a specific audience
Each Help Center is configured independently, so your public docs and your internal runbooks never bleed into each other. The access and authentication options covered in the next article apply per Help Center.
## What's included
### Ask AI
Let customers ask questions in their own words and get instant, accurate answers, powered entirely by your published knowledge base articles.
* Native to your Help Center, no extra tools or integrations needed
* Smart handoff to your team when the AI reaches its limit, with full conversation context passed through
* Every Ask AI interaction is logged, so your team can spot gaps and improve documentation over time
### Knowledge base
Write and maintain help articles in a WYSIWYG editor. Use AI to generate articles directly from support thread answers, or to tidy, expand, and rewrite existing context.
* Organize articles into groups with drag-and-drop structure
* Draft, publish, and unpublish articles instantly
* Highlight any section and click ✨ to rewrite, shorten, expand, or fix the tone with AI
### Customer Inbox
Give your customers a single place to submit, track, and reply to their threads, no matter which channel they used to get in touch: Slack, email, chat, and more.
* **Configurable visibility**: customers see either their own threads or every thread from their company
* **No password needed**: Customers log in using a 6-digit code sent to their email
* **Fully integrated with your Plain queue**: new requests land alongside all other channels automatically
* **Additional recipients**: Customers can CC colleagues into a conversation directly from the portal, keeping multiple stakeholders at an account in the loop
# AI generate Help Center articles
Source: https://www.plain.com/docs/product/help-center/ai-generate-help-center-articles
Turn a good answer in a thread into a draft Help Center article in a few keystrokes.
When your team writes a great answer to a customer, that knowledge shouldn't stay buried in a thread. Plain can generate a draft help article from any thread in a few keystrokes, pulling the context from the thread and structuring it as a ready-to-edit article.
## Generate an article from a support thread
1. **Open any thread** in Plain
2. Press ⌘ + K (or Ctrl + K on Windows) to open the command palette
3. Type **"Generate Help Center article"** and select it
4. Choose the **Help Center** you want to publish to
5. Plain opens the Knowledge Base editor with the article already generated, including a suggested title and structured content drawn from the thread
## Review and publish
Once the article is generated, review it before publishing:
* **Highlight any text** and click the **✨ icon** to refine it with AI, rewrite, shorten, expand, simplify, or enter a custom prompt
* Edit manually as you would any article
* When you're happy, change the status to **Published** and click **Save**
See [Knowledge base →](/docs/product/help-center/help-center-knowledge-base) for the full list of Edit with AI actions and formatting options.
# Customer Inbox
Source: https://www.plain.com/docs/product/help-center/customer-inbox
Give customers one place to open, track, and reply to their threads, whatever channel they used.
Plain's Customer Inbox gives your customers a single, consistent place to create, track, and reply to their threads, regardless of which channel they used to get in touch (Slack, email, chat, and more). New requests submitted via the Customer Inbox flow straight into your team's Plain queue alongside all other channels.
## Enabling the Customer Inbox
To turn on Customer Inbox:
1. Go to **Help Center → Customer Inbox** in Plain
2. Toggle on **Customer Inbox**
3. Select your preferred **Thread visibility** option (see below)
4. Click **Save**
Once enabled, customers will see a **"Your requests"** section in the sidebar of your Help Center.
Email must be enabled as a channel before you can use the Customer Inbox.
## Logging in
Customers log in using a 6-digit code sent to their email, no password needed. The email address must match a customer record in Plain. Once authenticated, they'll see their threads organized by status.
Customers see a simplified version of Plain's internal statuses:
| What your team sees | What the customer sees |
| -------------------------------------------- | ---------------------- |
| Needs first response, Investigating, Snoozed | In progress |
| Waiting for customer | Waiting for you |
| Done | Done |
## Submitting new requests
Customers can submit new threads directly from the Customer Inbox. By default this is a single open text field, but you can configure a custom form to collect structured information upfront, like issue type, priority, or product area. See [Custom forms →](/docs/product/help-center/customer-inbox-custom-forms) for details.
## Thread visibility
Control which threads each customer can see under **Help Center → Customer Inbox → Thread visibility**:
* **Only their own threads** (default): Each customer sees only requests they submitted themselves
* **Threads from anyone in their tenant**: Useful for B2B teams where multiple contacts at the same account need visibility across shared requests
* **All threads from their company**: Broadest visibility, showing everything linked to their company in Plain
### Thread visibility overrides
Overrides are only available when your global thread visibility is set to **Only their own threads**. They let you grant specific customers broader visibility on top of that default without changing what everyone else can see.
#### Threads from anyone with the same tenant
* **Add tenant**: Grants all customers belonging to a specific tenant visibility of each other's threads
* **Add customer**: Grants a specific individual customer visibility of all threads from their tenant
#### Threads from anyone with the same company
* **Add company**: Grants all customers belonging to a specific company visibility of each other's threads
* **Add customer**: Grants a specific individual customer visibility of all threads from their company
This gives account admins elevated visibility without opening it to everyone on the account.
## Additional recipients
You can allow customers to add additional recipients to both new and existing threads from the Customer Inbox. When enabled, customers can CC colleagues into a conversation directly from the portal, useful for keeping multiple stakeholders at an account in the loop.
Enable this under **Help Center → Customer Inbox → Additional recipients**.
# Custom forms
Source: https://www.plain.com/docs/product/help-center/customer-inbox-custom-forms
Replace the customer inbox's free-text field with a structured intake form.
By default, customers submit new requests through a single open text field. Custom forms let you replace this with a structured intake form, collecting exactly the information your team needs upfront, like issue type, priority, or product area, so threads arrive in Plain ready to action.
## Creating a custom form
1. Go to **Help Center → Customer Inbox**
2. Click into the **Custom Form** section
3. Add the fields you want customers to fill in
4. Click **Save**
## Field types
When adding a field, choose from:
* **Dropdown**: Let customers choose from predefined options. Good for issue type, product area, or request category
* **Text input**: A short single-line field. Good for version number, account ID, or order reference
* **Text area**: A longer freeform field. Good for a detailed description of the issue
## Automating actions on submission
You can configure actions that are applied to the thread automatically the moment a customer submits the form:
* **Add assignee**: Route the thread to a specific team member
* **Add label**: Tag the thread with a relevant label
* **Add priority**: Set the thread priority based on the request type
* **Add thread fields**: Populate custom thread fields with values from the form
## What your team sees
When a customer submits a form:
* A new thread is created in Plain
* A timeline entry shows the values the customer submitted
* Any configured actions are applied to the thread automatically
# Ask AI
Source: https://www.plain.com/docs/product/help-center/help-center-ask-ai
Ask AI answers customer questions from your published articles, and hands off when it cannot.
Ask AI is a conversational assistant built directly into your Help Center. It answers customer questions instantly, powered entirely by the articles you've published in your knowledge base, no separate setup or third-party tools needed.
When Ask AI reaches the limit of what your knowledge base covers, it hands off to your team with full context of the conversation.
Ask AI only reads articles that are **Published** in your knowledge base. Draft articles are not used.
## How it works
When a customer visits your Help Center and asks a question, Ask AI searches your published articles and generates a synthesized answer, referencing the relevant articles so customers can read further if they want to.
If a customer isn't satisfied, they can either type *"Talk to a human"* or click **Continue with a human** in the top right corner. Either way, a new thread is instantly created in your Plain workspace with the full Ask AI conversation attached, so your team has complete context and never asks the customer to repeat themselves.
Every Ask AI conversation is logged and visible to your team in Plain, so you can spot recurring questions and find gaps in your knowledge base.
## Getting the best out of Ask AI
Ask AI is only as good as the articles behind it. If customers are getting vague or incorrect answers, the most common cause is the quality or structure of the underlying content. A few things that help:
* **Write one article per topic**: Ask AI retrieves and cites tightly scoped articles more accurately than long, multi-topic pages
* **Use clear, descriptive titles**: Ask AI uses article titles to understand relevance
* **Write in plain language**: Straightforward prose works better than heavily technical or jargon-heavy content
* **Keep articles up to date**: Outdated content leads to outdated answers
## Ask AI vs Ari
Ask AI is the self-service assistant in your Help Center, it answers questions from customers browsing your docs. Ari is Plain's AI agent that works within support threads, drafting replies and helping your team respond faster. They're complementary but separate features.
## Controlling AI access
You can enable or disable Ask AI's access to your knowledge base independently of your Help Center's human access settings, for example, keeping a Help Center set to "Your team only" while still allowing AI features to reference the articles. See [Access & Authentication →](/docs/graphql/help-center/authentication) for details.
# Customization
Source: https://www.plain.com/docs/product/help-center/help-center-customization
Set your Help Center's domain, logo, colors, and layout.
Tailor your Help Center to reflect your brand, from your domain and logo to your brand colors and analytics scripts. All customizations settings live under Help Center in your Plain workspace.
## Domains
By default, every new Help Center is assigned to a free `support.site` subdomain. You can claim yours or set up a fully custom domain under **Help Center → Domains.**
* `support.site` **subdomain**: Claim `yourcompany.support.site` at no extra cost, available immediately
* **Custom domain**: Point your own domain (e.g. `help.yourcompany.com`) to your Help Center by adding a CNAME record. SSL is handled automatically once the record propagates.
## Branding
Configure your Help Center's visual identity under **Help Center → Appearance**:
* **Primary brand color**: Plain generates a full color palette from this, applied across buttons, links, and interactive elements
* **Logo**: Shown in the header of every Help Center page
* **Favicon**: The small icon shown in browser tabs
* **AI agent avatar**: The avatar shown when Ask AI responds to customers
* **Social preview image**: The image shown when your Help Center URL is shared on social media or in messaging apps
You can also set your Help Center's public-facing name under **Help Center → General**. This is separate from the internal name only your team sees.
## Article copy button
Under **Help Center → Appearance**, you can enable a copy button that appears on all article pages. Once enabled, you can configure exactly what it offers:
* **Copy actions**: Choose which formats customers can copy the article in: plain text or Markdown
* **AI handoff actions**: Let customers open the current article as context directly in ChatGPT, Claude, or Cursor
## Custom scripts
To add analytics or other third-party scripts to your Help Center, go to **Help Center → General** and find the **Custom code** section. You can inject scripts into both the `` and `` of every page.
Common uses include:
* Analytics (Google Analytics, Google Tag Manager, Segment)
* A/B testing tools
* Custom event tracking
# Knowledge base
Source: https://www.plain.com/docs/product/help-center/help-center-knowledge-base
Write and manage the articles behind your Help Center and Ask AI.
Plain's knowledge base is where you create and manage the help articles that power your public Help Center, Ask AI, as well as features including [Ari](/docs/product/agents/ari) and [Sidekick](/docs/product/agents/sidekick). Articles are written in a rich text editor, no code needed, and published instantly.
## Creating and organizing articles
Create new articles and organize them into groups from the **Help Center sidebar**.
* Click **New article** to start writing, or generate one from a support thread (see [Turn answers into articles →](/docs/product/help-center/ai-generate-help-center-articles))
* Group articles into categories to build clear navigation for your customers
* **Drag and drop** to reorder articles and groups at any time
* Articles are **Draft** by default, they're not visible to customers or Ask AI until you publish them
* Change status to **Published** and click **Save** to make an article live instantly
## Edit with AI
Refine an article with AI. When you hover over any section of your article, click the **✨ icon** to choose an editing action:
* **Tidy up**: Clean up awkward phrasing
* **Shorten**: Make text more concise
* **Expand on this**: Add more detail or context
* **Simplify**: Use more accessible language
* **Fix spelling and grammar**: Correct typos and errors
* **Fix tone**: Adjust tone to match your brand
You can also enter a **custom prompt** to make more tailored changes.
## Formatting options
The editor supports a range of formatting to help you create rich, informative articles:
* Headings (H1-3)
* Basic text formatting (**bold**, *italic*, ~~strikethrough~~ etc.)
* Links
* Lists
* Tables
* Collapsible sections
* Quotes
* Custom HTML for embeds/iframes etc.
* Images
* Code blocks with syntax highlighting:
```js theme={null}
function doThing() {
console.log('hello world');
}
```
* Callouts
Example info callout
Example warning callout
Example success callout
Example tip callout
This is a table:
| Table heading | Table heading | Table heading |
| --------------- | ------------- | ------------- |
| | | |
| Example content | | |
Example content | *Example content* | **Example content** |
This is a collapsible section
Content inside your section
This is a quote
> Example quote content
# Migration
Source: https://www.plain.com/docs/product/help-center/help-center-migration
Move an existing knowledge base into Plain from Markdown or HTML.
You can migrate an existing knowledge base into Plain from Markdown or HTML.
## Before you start
Make sure your existing content is exported in **Markdown** or **HTML**. Most Help Center platforms support at least one of these export formats.
## Option 1: Using our API
If you're comfortable with scripting, you can create articles programmatically using the `upsertHelpCenterArticle` mutation in Plain's GraphQL API. Try it out in the [API Explorer](https://app.plain.com/developer/api-explorer/).
## Option 2: Automated script
If you'd rather not script the migration yourself, Plain has a ready-made script that automatically imports Markdown files into your Help Center. Reach out to our support team to get access and guidance on running it.
# Search engine and AI discovery
Source: https://www.plain.com/docs/product/help-center/help-center-seo-and-ai-discovery
How search engines and AI assistants read your Help Center, and the metadata they see.
Search engines and AI assistants read your Help Center to judge whether content is current and who wrote it. Every public Help Center publishes what they look for, with the author and last updated date on each article. There is nothing to configure.
## Discovery surfaces
* **Article pages**: JSON-LD with `author`, `publisher`, `datePublished`, and `dateModified`, plus matching meta tags
* **Markdown**: append `.md` to an article URL for the Markdown version, with `author`, `published`, and `updated` in the frontmatter
* **`llms.txt`**: at the root of your domain, lists every published article with its Markdown URL, description, author, and updated date
* **`sitemap.xml`**: lists every page with a `lastmod` timestamp
An article line in `llms.txt`:
```plaintext theme={null}
- [Quickstart](https://help.yourcompany.com/article/quickstart.md): Get your team live in four phases. (author: Youmna, updated: 2026-09-02)
```
## Who is named as the author
The author is the public name of the user who created the article. Articles created by a [machine user](/docs/agents), by Plain, or by a user who has left the workspace are attributed to your workspace instead.
`published` is the last publish time and is absent on drafts.
## Private Help Centers
A Help Center behind authentication publishes the same surfaces, but only to authenticated visitors, so search engines and AI crawlers can't read them.
# Integrations
Source: https://www.plain.com/docs/product/integrations
Every integration Plain offers, grouped by CRMs, issue trackers, incident management, and importers.
Plain integrates with the tools modern B2B support teams rely on. From CRMs and issue trackers to incident management platforms and historical data importers. These integrations are put the context on the thread, so your team does not switch tabs to find it.
Integrations are available on **Horizon** and **Frontier** [plans](https://www.plain.com/pricing).
## CRM integrations: Customer & account context
Plain's CRM integrations bring account-level context directly into the support experience. The moment a thread starts, your team can see account ownership, relationships, and tier, without switching tabs.
CRM data syncs into Plain non-destructively: your CRM won't overwrite edits you've made in Plain:
* Contacts or people sync as **Customers**
* Accounts or companies sync as **Tenants**
* New contacts and companies continue syncing automatically after the initial import
* Changes to records you've edited in Plain are preserved. Your CRM won't overwrite them
Plain supports CRM integrations with Salesforce, HubSpot, Attio.
**Salesforce and HubSpot** support a sync of data back to the CRMs: when a thread is associated with a Plain tenant, the thread details are automatically posted to the CRM as a note.
## Importers: Historical data migration
Plain's importers bring your history across in one pass. Migrate your full support history, including conversations, contacts, and internal notes without starting from scratch.
Note: All Importer integrations, including Zendesk, Freshdesk, Intercom, Front, and HelpScout, are available on the Horizon and Frontier plans.
**Sync limitations**
Importers are designed for one-way migration, not real-time two-way sync. New records created in your old platform continue syncing to Plain automatically. Updates to existing records, status changes, edits, reassignments, are not synced after initial import. No webhooks or auto-responders are triggered.
## Issue tracker integrations: Logging customer feedback
Stay tightly aligned with product by turning support signals into structured feedback. Link threads to issues and get automatically prompted to close the loop with customers when their bug is fixed or feature request is shipped.
Key capabilities:
* **Auto-update status**: when a linked issue is completed or canceled, the thread automatically moves to Close the Loop status.
* **Track product insights**: view top customer requests, see which accounts requested them, and break down by tier or request volume.
* **Company-level breakdowns**: see which customers requested what, sorted by tier and request volume
Plain supports: Linear, Jira, Shortcut, GitHub
## Incident management: Support during firefighting
Coordinate with engineering during incidents while keeping customers in the loop. Link support threads to active incidents, track incoming requests, and follow up automatically when the incident is resolved.
What you can do:
* **Create new incidents** directly from any Plain thread
* **Link existing incidents** to active threads.
* **Track resolution status**: When an incident is marked as resolved, linked threads automatically return to your queue for follow-up
* **Post-incident clean-up**: find and update every affected customer
Plain supports integrations with [Incident.io](http://Incident.io) and Rootly.
# Attio
Source: https://www.plain.com/docs/product/integrations/attio
Bring Attio companies and people into Plain for account context on every thread.
Connect Attio to Plain to bring your companies and people into your support workspace. Once connected, your team sees account context like company tier, funding stage, and custom attributes directly on every thread.
This integration is a one-way sync from Attio into Plain. Write-back actions are planned for a future update.
## How Attio maps to Plain
| **Attio** | **Plain** |
| ----------------------------------- | ------------ |
| Person (associated with list entry) | Customer |
| Company (list entry) | Tenant |
| Company attribute | Tenant field |
Tenants are the company-level grouping in Plain. Customers belong to tenants, the same way people belong to companies in Attio.
## Before you start
Create a list in Attio with only the companies you want in Plain.
You can sync all companies in your Attio account, but it may take longer than needed. We recommend filtering to the companies you need, such as active customers. Plain picks up changes to the list.
## Set up the sync
Go to **Settings → Integrations → Attio**, click **Connect**, and authorize Plain's access.
Click **Set up sync**, then select the list you want to sync. Plain shows how each Attio entity maps to Plain as you go, then lets you pick which company attributes to bring across.
Click **Enable sync**.
The first sync starts immediately. You'll see live progress as tenant fields, tenants, and customers are imported.
## How the sync works
After the first sync, Plain checks Attio for changes approximately once an hour, starting one hour after the previous run finishes.
**What syncs:**
* New companies added to your selected list become new tenants
* Changes to company attributes (like name or URL) sync to Plain
* New attributes on Attio companies become available for syncing as tenant fields. You'll need to enable them before we sync their data.
* New people associated with a list entry become new customers in Plain
* A person added to an existing Attio company joins the matching tenant in Plain
**What doesn't sync:**
* Companies with no name. These are filtered out and will not be imported.
* Updates to existing people. Plain only creates new customer records and doesn't overwrite existing ones. If you edit a customer in Plain, future syncs leave your changes alone.
* Contact removals. If you remove a contact from an Attio company, the tenant relationship in Plain stays intact. Remove it manually in Plain if needed.
* Company deletes and merges. If a company is deleted or merged in Attio, the corresponding tenant in Plain is not removed or updated. You'll need to remove or reassign stale tenants manually in Plain.
## Tenant fields
Plain imports properties from your Attio companies that are set as visible.
You can choose which properties you want to import during your first sync, or update it anytime in **Settings → Integrations → Attio → Sync settings**.
Toggle visibility on the fields you want to display in thread views. Visible tenant fields appear on the tenant card in every thread, so your team sees CRM context like company tier or funding stage without leaving Plain.
Changes apply on the next sync. If you turn a field on, we'll start syncing its data. If you turn it off, we'll remove its data from Plain.
## Sync history
The Attio settings page shows:
* The next scheduled sync time and the list it will use.
* A table of past runs with status, duration, and list name.
If a run fails, Plain marks it as failed and reschedules it automatically. You don't need to do anything for transient failures. If runs fail repeatedly, contact support.
## Write back to Attio
Workflow actions for Attio are not yet available. They are planned and this page will be updated when they ship.
## Disconnecting
Go to **Settings → Integrations → Attio** and click **Disconnect**. The sync stops immediately. Records already in Plain stay where they are.
## Troubleshooting
**A sync run failed.** Plain reschedules failed runs automatically. If the next run also fails, check that your Attio credentials are still valid and that the list you selected still exists.
**A person isn't appearing in Plain.** Confirm the person is associated with a company that's in your selected list. Plain only syncs people whose companies are in the list.
**A field I expected isn't showing on the tenant card.** Go to **Settings → Tenant fields** and check that visibility is toggled on for that field.
# CRM
Source: https://www.plain.com/docs/product/integrations/crm
How Plain's CRM integrations work, and what they sync in each direction.
Plain's CRM integrations brings your customer and account data into Plain, so your team has full context on the customer and the account as soon as a thread starts.
Note: All CRM integrations, including Salesforce, Hubspot, and Attio, are available on our Horizon and Frontier plans.
## Key capabilities
* **Sync customer data automatically**: contacts and accounts from your CRM appear in Plain as customers and companies
* **Add context to every thread**: see customer and company details in Plain as soon as a thread starts
* **Write back to your CRM**: add notes or tickets in your CRM when work happens in Plain
# Freshdesk
Source: https://www.plain.com/docs/product/integrations/freshdesk
Import your Freshdesk support history into Plain with the built-in importer.
When you connect your Freshdesk account, Plain imports your full support history so your team can hit the ground running without losing context.
## What gets imported
* **Contacts → Customers**
* **Tickets → Threads**
* Resolved/closed tickets are imported as **Done**
* Open/pending tickets are imported as **Todo**
* All messages including private notes are carried over
* **Tags → Labels**
* **Original timestamps**: every thread and every message keeps the date it had in Freshdesk, so your history stays in order
CSAT ratings and custom fields on tickets are not imported. See [Migrating from another tool](https://help.plain.com/article/migration) for the full picture of what you'll rebuild in Plain.
Importing history doesn't trigger [SLAs](/docs/product/platform/slas), [auto-responses](/docs/product/platform/auto-responses), or [workflows](/docs/product/workflows), so nothing goes out to your customers and no SLA clocks start on old tickets.
## Ongoing sync
After the initial import, new tickets, messages, and contacts sync to Plain every 5 minutes. Changes to existing tickets, such as status, priority, or assignee, are not synced after the initial import. No auto-responses are triggered by the sync.
## How to set it up
1. Go to **Settings → Freshdesk importer**
2. Follow the prompts to connect your Freshdesk account
3. Your existing data will begin syncing into Plain
A large history takes time to come across.
To stop syncing, return to **Settings → Freshdesk importer** and click **Disconnect**.
# Front
Source: https://www.plain.com/docs/product/integrations/front-importer
Import your Front conversation history into Plain with a script built on the API.
Bringing your Front history into Plain takes a short script built on the Plain API. Your team keeps every past conversation, searchable alongside live ones, from the day you switch. This page covers how Front's data maps onto Plain's and the decisions to make before you run it.
## What gets imported
* **Contacts → Customers**
* **Conversations → Threads**
* You choose which conversations land as **Done** and which stay **Todo**. Most teams import archived conversations as Done
* All messages and comments are carried over. Comments appear as internal comments in Plain
* **Tags → Labels**
* **Original timestamps**: every thread and every message keeps the date it had in Front, so your history stays in order
* **A link back to the original conversation**, so anyone reading the thread in Plain can still find it in Front
CSAT ratings and custom fields on conversations are not imported. See [Migrating from another tool](https://help.plain.com/article/migration) for the full picture of what you'll rebuild in Plain.
Importing history doesn't trigger [SLAs](/docs/product/platform/slas), [auto-responses](/docs/product/platform/auto-responses), or [workflows](/docs/product/workflows), so nothing goes out to your customers and no SLA clocks start on old tickets.
## Mapping Front onto Plain
A few decisions to make before you write the script:
* **Comments and replies.** Front keeps user comments separate from messages sent to the customer. Import comments as internal comments in Plain so private discussion stays private.
* **Inboxes.** Plain routes with [labels](/docs/product/platform/labels), [Tiers](/docs/product/platform/tiers), and [workflows](/docs/product/workflows) rather than a set of shared inboxes. Add the inbox name as a label on each thread if you want to keep it, and set up your live routing separately.
* **Conversation IDs.** Use the Front conversation ID as each thread's external reference. That's what makes the import safe to re-run.
* **Companies.** Plain sets a customer's [Company](/docs/product/platform/companies) automatically from their email domain, so most conversations need no mapping by hand.
* **Assignees.** Decide which Front users map to the people you've already invited to Plain, and what to do with anyone who has left.
## Running the import
If you'd like a hand scoping the script or a review before you run it at full volume, email [help@plain.com](mailto:help@plain.com).
# Github
Source: https://www.plain.com/docs/product/integrations/github
Link threads to GitHub issues to track customer-reported bugs and requests.
Connect your Github workspace to Plain to track feature requests and bugs reported by customers. Link a Github issue or PR to any thread and get automatically prompted to follow up when the issue is resolved.
The initial set up must be performed by someone with both the Workspace Admin role in Plain and the Org Owner role in GitHub.
## Setup
1. Go to **Settings → GitHub** and connect your GitHub account.
2. Connect your Github workspace
## Linking threads to Github stories
Once you've linked your GitHub account to Plain, you can create a GitHub issue (or link an existing GitHub issue or PR) directly to a customer request in the sidebar - or by pressing `i`.
# Help Scout
Source: https://www.plain.com/docs/product/integrations/help-scout
Import your Help Scout conversations, notes, and contacts into Plain.
When you connect Help Scout, Plain imports your full support history, including conversations, internal notes, and attachments, so your team can pick up exactly where they left off.
## What gets imported
* **End users → Customers**
* **Conversations → Threads**, including internal notes
* Help Scout statuses are mapped to the equivalent thread statuses in Plain
* **Tags → Labels**
* **Attachments**: any files attached to Help Scout conversations are carried over
* **Original timestamps**: every thread and every message keeps the date it had in Help Scout, so your history stays in order
Each mailbox connects separately, so repeat the setup below for every mailbox you want in Plain.
CSAT ratings and custom fields on conversations are not imported. See [Migrating from another tool](https://help.plain.com/article/migration) for the full picture of what you'll rebuild in Plain.
Importing history doesn't trigger [SLAs](/docs/product/platform/slas), [auto-responses](/docs/product/platform/auto-responses), or [workflows](/docs/product/workflows), so nothing goes out to your customers and no SLA clocks start on old conversations.
## Ongoing sync
After the initial import, new customers, conversations, notes, and tags from Help Scout sync to Plain hourly. Changes to existing conversations, such as status or assignee, are not synced after the initial import. No auto-responses are triggered by the sync.
## How to set it up
1. Go to **Settings → Help Scout**
2. Follow the steps to connect your Help Scout account
3. Connect each mailbox you want to import. Every mailbox is authorized on its own
4. Once connected, your historical data will begin importing automatically
A large history takes time to come across.
To stop syncing, return to **Settings → Help Scout** and click **Disconnect**.
# HubSpot Service Hub
Source: https://www.plain.com/docs/product/integrations/hub-spot-service-hub
Import HubSpot Service Hub ticket history into Plain with a script built on the API.
Bringing your HubSpot Service Hub ticket history into Plain takes a short script built on the Plain API. Your team keeps every past ticket, searchable alongside live threads, from the day you switch. This page covers how Service Hub's data maps onto Plain's and the decisions to make before you run it.
This page is about moving your support history into Plain. To sync company and contact context onto live threads, see the [HubSpot integration](/docs/product/integrations/hubspot).
## What gets imported
* **Contacts → Customers**
* **Tickets → Threads**
* You choose which pipeline stages land as **Done** and which stay **Todo**
* The emails and notes on the ticket are carried over. Notes appear as internal comments in Plain
* **Ticket properties you choose → Labels**
* **Original timestamps**: every thread and every message keeps the date it had in HubSpot, so your history stays in order
* **A link back to the original ticket**, so anyone reading the thread in Plain can still find it in HubSpot
CSAT ratings and custom properties on tickets are not imported. See [Migrating from another tool](https://help.plain.com/article/migration) for the full picture of what you'll rebuild in Plain.
Importing history doesn't trigger [SLAs](/docs/product/platform/slas), [auto-responses](/docs/product/platform/auto-responses), or [workflows](/docs/product/workflows), so nothing goes out to your customers and no SLA clocks start on old tickets.
## Mapping service hub onto Plain
Ticket pipelines and stages are set up differently in every HubSpot account, so plan this against your own:
* **Pipelines and stages.** Plain has three status groups: **Todo**, **Snoozed**, and **Done**, rather than stages you define. Decide which of your stages land as Done and which stay Todo. If the stage matters for reporting, add it as a [label](/docs/product/platform/labels) so you keep it.
* **Where the conversation lives.** A ticket's history sits in the activities associated with it: emails and notes. Decide which of those your team reads back, and import those. Anything written for users should come in as an internal comment in Plain.
* **Ticket IDs.** Use the HubSpot ticket ID as each thread's external reference. That's what makes the import safe to re-run.
* **Companies.** Plain sets a customer's [Company](/docs/product/platform/companies) automatically from their email domain. If the companies in HubSpot don't line up with domains, create a [Tenant](/docs/product/platform/tenants) per company instead and attach customers to it.
* **Categories, sources, and priority.** Plain threads carry a priority and labels rather than a set of custom properties. Pick the ones your team filters on and bring those across as labels.
* **Owners.** Decide which ticket owners map to the users you've already invited to Plain, and what to do with anyone who has left.
## Running the import
If you'd like a hand scoping the script or a review before you run it at full volume, email [help@plain.com](mailto:help@plain.com).
# HubSpot
Source: https://www.plain.com/docs/product/integrations/hubspot
Bring HubSpot customer and company data into Plain, and write tickets and notes back.
Plain's HubSpot integration brings your customer and company data into Plain, so your team has full context the moment a thread starts.
The Hubspot integration is available on **Horizon** and **Frontier** plans.
This integration syncs information from HubSpot into Plain. From Plain, you can create tickets and notes in HubSpot using [workflows](/docs/product/workflows).
## How Hubspot maps to Plain
| **Hubspot** | **Plain** |
| ----------------------------------- | ------------ |
| Person (associated with list entry) | Customer |
| Company (list entry) | Tenant |
| Company attribute | Tenant field |
Tenants are the company-level grouping in Plain. Customers belong to tenants, the same way people belong to companies in Hubspot.
## Before you start
Create a list in Hubspot with only the companies you want in Plain.
You can sync all companies in your Hubspot account, but it may take longer than needed. We recommend filtering to the companies you need, such as active customers. Plain picks up changes to the list.
## Set up the sync
Go to **Settings → Integrations → HubSpot**, click **Connect**, and authorize Plain's access.
Click **Set up sync**, then select the list you want to sync. Plain shows how each HubSpot entity maps to Plain as you go, then lets you pick which company attributes to bring across.
Click **Enable sync**.
The first sync starts immediately. You'll see live progress as tenant fields, tenants, and customers are imported.
## How the sync works
After the first sync, Plain checks Hubspot for changes approximately once an hour, starting one hour after the previous run finishes.
**What syncs:**
* New companies added to your selected list become new tenants
* Changes to company attributes (like name or URL) sync to Plain
* New attributes on Hubspot companies become available for syncing as tenant fields. You'll need to enable them before we sync their data.
* New people associated with a list entry become new customers in Plain
* A person added to an existing Hubspot company joins the matching tenant in Plain
**What doesn't sync:**
* Companies with no name. These are filtered out and will not be imported.
* Updates to existing people. Plain only creates new customer records and doesn't overwrite existing ones. If you edit a customer in Plain, future syncs leave your changes alone.
* Contact removals. If you remove a contact from an Hubspot company, the tenant relationship in Plain stays intact. Remove it manually in Plain if needed.
* Company deletes and merges. If a company is deleted or merged in HubSpot, the corresponding tenant in Plain is not removed or updated. You'll need to remove or reassign stale tenants manually in Plain.
## Tenant fields
Plain imports properties from your Hubspot companies that are set as visible.
You can choose which properties you want to import during your first sync, or update it anytime in **Settings → Integrations → Hubspot → Sync settings**.
Toggle visibility on the fields you want to display in thread views. Visible tenant fields appear on the tenant card in every thread, so your team sees CRM context like company tier or funding stage without leaving Plain.
Changes apply on the next sync. If you turn a field on, we'll start syncing its data. If you turn it off, we'll remove its data from Plain.
## Sync history
The Hubspot settings page shows:
* The next scheduled sync time and the list it will use.
* A table of past runs with status, duration, and list name.
If a run fails, Plain marks it as failed and reschedules it automatically. You don't need to do anything for transient failures. If runs fail repeatedly, contact support.
## Write back to Hubspot
You can use [Workflow actions](/docs/product/workflows/workflows-actions) to create tickets and notes on Hubspot when a workflow is [triggered](/docs/product/workflows/workflows-triggers). For example, you could automatically add a note to a customer account whenever they create a new thread in Plain
## Disconnecting
Go to **Settings → Integrations → Hubspot** and click **Disconnect**. The sync stops immediately. Records already in Plain stay where they are.
## Troubleshooting
**A sync run failed.** Plain reschedules failed runs automatically. If the next run also fails, check that your Hubspot credentials are still valid and that the list you selected still exists.
**A person isn't appearing in Plain.** Confirm the person is associated with a company that's in your selected list. Plain only syncs people whose companies are in the list.
**A field I expected isn't showing on the tenant card.** Go to **Settings → Tenant fields** and check that visibility is toggled on for that field.
# Incident.io
Source: https://www.plain.com/docs/product/integrations/incident.io
Link threads to incident.io incidents and follow up automatically when they resolve.
Plain's [incident.io](https://incident.io) integration allows you to coordinate with engineering during incidents while keeping customers in the loop. Link support threads to active incidents, track incoming requests, and follow up automatically when the incident is resolved.
## How to set it up
1. Go to **Settings → Incident.io**
2. Connect your Incident.io account
3. During setup, you'll be asked to [create an API key](https://docs.incident.io/integrations/api-create-incident) in Incident.io. Make sure it has these permissions:
* View data (e.g. incidents and org settings)
* Create incidents
* Edit incidents
4. Once connected, use the **Thread links** panel in any thread to create a new incident, link to an existing one, or set incident details like severity, team, and service.
When an incident is resolved in Incident.io, Plain automatically moves all linked threads to your **Close the Loop** list, so your team can follow up with customers.
The Incident.io integration is available on Horizon and Frontier plans.
# Intercom
Source: https://www.plain.com/docs/product/integrations/intercom
Import your Intercom conversations, contacts, and admins into Plain.
The Intercom importer migrates all your past conversations, contacts, and admins into Plain.
## What gets imported
* **Contacts → Customers**
* **Admins → Users**
* **Conversations → Threads**
* All historical conversations, including chat and email, are brought into Plain
* Each thread shows **Intercom** as the source channel, with a direct link to the original conversation for reference
* **Snoozed conversations arrive as Todo**, so look for them there rather than in Snoozed
* Open conversations arrive with the right response state: waiting on your first reply, waiting on your next reply, or **Investigating** where your team replied last
* **Attachments**: files sent in Intercom conversations are carried over with their messages
* **Assignee**: the admin a conversation was assigned to in Intercom becomes the assignee on the thread in Plain, so your queues arrive already owned
* **Priority**: conversations marked as priority in Intercom arrive as **Urgent** in Plain. Everything else arrives as **Normal**
* **Original timestamps**: every thread and every message keeps the date it had in Intercom, so your history stays in order
If a customer already exists in Plain, imported threads are associated with them to avoid duplicates.
CSAT ratings and custom fields on conversations are not imported. See [Migrating from another tool](https://help.plain.com/article/migration) for the full picture of what you'll rebuild in Plain.
Importing history doesn't trigger [SLAs](/docs/product/platform/slas), [auto-responses](/docs/product/platform/auto-responses), or [workflows](/docs/product/workflows), so nothing goes out to your customers and no SLA clocks start on old conversations.
## Ongoing sync
After the initial import, new contacts, admins, and conversations sync from Intercom into Plain every hour. Changes to existing conversations, such as status or assignee, are not synced after the initial import. No auto-responses are triggered by the sync.
## How to set it up
1. Go to **Settings → Intercom**
2. Follow the prompts to connect your Intercom account
3. Start the import
A large history takes time to come across.
To stop syncing, return to **Settings → Intercom** and click **Disconnect**.
Attribution is preserved. You'll be able to see which admin handled a conversation and what was said. Imported data will not trigger auto-responders or webhooks in Plain.
# Jira
Source: https://www.plain.com/docs/product/integrations/jira
Link threads to Jira issues to track customer-reported bugs and requests.
Connect your Jira workspace to Plain to track feature requests and bugs reported by customers. Link a Jira issue to any thread and get automatically prompted to follow up when the issue is resolved.
## Setup
1. Go to **Settings → Jira**
2. Connect your Jira workspace
## Linking threads to Jira issues
Once connected, open any thread and use the **Thread links** panel in the sidebar or press i to create a new Jira issue or link an existing one.
When the Jira issue is completed, canceled, or deleted, the thread automatically moves to **Close the Loop**.
To use all features of the Jira integration, including selecting assignees, users need the **Browse users and groups** permission in their Jira workspace.
# Jira Service Management
Source: https://www.plain.com/docs/product/integrations/jira-service-management
Import Jira Service Management request history into Plain with a script built on the API.
Bringing your Jira Service Management request history into Plain takes a short script built on the Plain API. Your team keeps every past request, searchable alongside live threads, from the day you switch. This page covers how JSM's data maps onto Plain's and the decisions to make before you run it.
This page is about moving your support history into Plain. To link Jira issues to live threads and get prompted when they're resolved, see the [Jira integration](/docs/product/integrations/jira).
## What gets imported
* **Customers → Customers**
* **Requests → Threads**
* You choose which statuses land as **Done** and which stay **Todo**
* Comments are carried over. Comments your team marked as internal appear as internal comments in Plain
* **Labels and components you choose → Labels**
* **Original timestamps**: every thread and every comment keeps the date it had in Jira, so your history stays in order
* **A link back to the original request**, so anyone reading the thread in Plain can still find it in Jira
CSAT ratings and custom fields on requests are not imported. See [Migrating from another tool](https://help.plain.com/article/migration) for the full picture of what you'll rebuild in Plain.
Importing history doesn't trigger [SLAs](/docs/product/platform/slas), [auto-responses](/docs/product/platform/auto-responses), or [workflows](/docs/product/workflows), so nothing goes out to your customers and no SLA clocks start on old tickets.
## Mapping Jira Service Management onto Plain
Workflows and request types are configured per project, so plan this against your own setup:
* **Statuses.** Plain has three status groups: **Todo**, **Snoozed**, and **Done**. Every Jira project defines its own workflow, so decide which of your statuses land as Done and which stay Todo. If the original status matters for reporting, add it as a [label](/docs/product/platform/labels) so you keep it.
* **Public and internal comments.** JSM marks each comment as visible to the customer or to your team only. Carry that distinction across so internal discussion arrives as an internal comment in Plain.
* **Request types.** Plain routes with [labels](/docs/product/platform/labels), [Tiers](/docs/product/platform/tiers), and [workflows](/docs/product/workflows) rather than a form per request type. Bring the request type across as a label, and rebuild the forms you need with [contact forms](/docs/product/channels/contact-forms) or the [customer inbox](/docs/product/help-center/customer-inbox).
* **Issue keys.** Use the issue key as each thread's external reference. That's what makes the import safe to re-run.
* **Organizations.** Plain sets a customer's [Company](/docs/product/platform/companies) automatically from their email domain. If your JSM organizations don't line up with domains, create a [Tenant](/docs/product/platform/tenants) per organization instead and attach customers to it.
* **Assignees.** Decide which Jira assignees map to the users you've already invited to Plain, and what to do with anyone who has left.
## Running the import
If you'd like a hand scoping the script or a review before you run it at full volume, email [help@plain.com](mailto:help@plain.com).
# Linear
Source: https://www.plain.com/docs/product/integrations/linear
Connect Plain to Linear to track issues and follow up with customers.
Connect your Linear workspace to Plain to track feature requests and bugs reported by customers. Link a Linear issue to any thread and get automatically prompted to follow up when the issue is completed or canceled.
## Setup
1. Go to **Settings → Linear**
2. Connect your Linear workspace
## Linking threads to Linear issues
Once connected, open any thread and use the **Thread links** panel in the sidebar or press i to create a new Linear issue or link an existing one.
When the Linear issue is completed or canceled, the thread automatically moves to **Close the Loop**.
### Linking Linear issues to threads via the API
You can also link a Linear issue to a Plain thread using the `createThreadLink` mutation, including from a machine user API key. This is useful for automation workflows that open Linear issues and want to surface them inside Plain without any manual steps.
For this to work, the Linear integration must be connected in **Settings → Linear** before you can link issues via the API. See the [**Linear integration guide**](/docs/product/integrations/linear) for setup steps.
Your machine user API key also needs:
* `threadLink:create`
* `threadLink:read`
## Linear templates
If you've created issue templates in Linear, you can choose which template to use each time you create a new issue from Plain.
## Customer requests
If your Linear workspace supports Customer Requests, Plain creates a Customer and a Customer Request when linking a thread to a Linear issue.
## Troubleshooting
### Status updates not syncing to Plain
If Linear issue status changes are not updating in Plain. For example, a thread does not move to **Close the Loop** when the linked issue is marked Done. Check whether a Team filter is restricting the Plain integration in Linear's Application settings.
In Linear, go to **Settings → API → Applications → Plain** and check the team access configuration. If the integration is scoped to specific teams, issues belonging to other teams will not send webhook events to Plain, meaning their status changes will be silently skipped.
Set the integration to allow **All teams** to ensure status updates sync for issues across your entire Linear workspace.
# Polytomic
Source: https://www.plain.com/docs/product/integrations/polytomic
Sync Plain data into your warehouse or other business tools with Polytomic.
Polytomic syncs your Plain data to the rest of your data stack without writing code. Connect in minutes to move data between Plain and tools like Segment, Snowflake, S3, and more.
## How to connect Polytomic to Plain
1. **Generate a Plain API key** - Follow our API key instructions to create a key with the right access level.
2. [**Add the connection in Polytomic**](https://www.polytomic.com/integrations/plain) - In Polytomic, go to **Connections** → **Add Connection** > **Plain**.
3. **Paste your API key** - Enter the API key you generated in Plain.
4. **Click Save** - Your Plain connection is now ready to use.
## Syncing data from Plain
* **Bulk Syncs** - Use these to move large amounts of data from Plain into data warehouses, databases, or cloud storage like S3.
* **Model Syncs** - Use these to send Plain data to other SaaS tools (like Salesforce, HubSpot, or Attio), spreadsheets, or webhooks.
# Pylon
Source: https://www.plain.com/docs/product/integrations/pylon
Import your Pylon history into Plain with a script built on the API.
Bringing your Pylon history into Plain takes a short script built on the Plain API. Your team keeps every past conversation, searchable alongside live ones, from the day you switch. This page covers how Pylon's data maps onto Plain's and the decisions to make before you run it.
## What gets imported
* **Contacts → Customers**
* **Issues → Threads**
* You choose which Pylon states land as **Done** and which stay **Todo**
* All messages including internal notes are carried over. Notes appear as internal comments in Plain
* **Tags → Labels**
* **Original timestamps**: every thread and every message keeps the date it had in Pylon, so your history stays in order
* **A link back to the original issue**, so anyone reading the thread in Plain can still find it in Pylon
CSAT ratings and custom fields on issues are not imported. See [Migrating from another tool](https://help.plain.com/article/migration) for the full picture of what you'll rebuild in Plain.
Importing history doesn't trigger [SLAs](/docs/product/platform/slas), [auto-responses](/docs/product/platform/auto-responses), or [workflows](/docs/product/workflows), so nothing goes out to your customers and no SLA clocks start on old tickets.
## Mapping Pylon onto Plain
A few decisions to make before you write the script:
* **Statuses.** Plain has three status groups: **Todo**, **Snoozed**, and **Done**. Decide which of your Pylon states land as Done and which stay Todo. If the original state matters for reporting, add it as a [label](/docs/product/platform/labels) so you keep it.
* **Accounts.** Plain sets a customer's [Company](/docs/product/platform/companies) automatically from their email domain, so most teams get their account grouping back without mapping anything. If your accounts don't line up with domains, create a [Tenant](/docs/product/platform/tenants) per account instead and attach customers to it.
* **Issue IDs.** Use the Pylon issue ID as each thread's external reference. That's what makes the import safe to re-run.
* **Slack and Teams issues.** Issues that started in a shared Slack or Microsoft Teams channel come across as threads with their message history. Connecting those channels for live support is set up separately, in [Channels](/docs/product/channels).
* **Assignees.** Decide which Pylon users map to the users you've already invited to Plain, and what to do with anyone who has left.
## Running the import
If you'd like a hand scoping the script or a review before you run it at full volume, email [help@plain.com](mailto:help@plain.com).
# Rootly
Source: https://www.plain.com/docs/product/integrations/rootly
Link threads to Rootly incidents so support and engineering work from the same picture.
Plain's Rootly integration keeps support and engineering working from the same picture during incidents. Link support threads to Rootly incidents and follow up automatically with customers when the incident is resolved.
## Prerequisites
1. Create a Global [API key](https://rootly.com/account/api-keys) in Rootly and ensure the Incident Response Role and On-Call Role are set to "Owner" and "Admin" respectively.
## How to set it up
1. Go to **Settings → Rootly**
2. Connect your Rootly account
3. Once connected, use the **Thread links** panel in any support thread to start a new incident, link to an existing one, or set optional fields like severity, team, and service
When an incident is marked **Resolved** in Rootly, all linked threads automatically return to your **Todo** view in Plain so you can close the loop with customers.
The Rootly integration is available on Horizon and Frontier plans.
# Salesforce
Source: https://www.plain.com/docs/product/integrations/salesforce
Bring Salesforce accounts and contacts into Plain, and write notes back.
Connect Salesforce to Plain to bring your companies and people into your support workspace. Once connected, your team sees account context like company tier, funding stage, and custom attributes directly on every thread.
The Salesforce integration is available on **Horizon** and **Frontier** plans.
This integration syncs information from Salesforce into Plain. From Plain, you can create notes in Salesforce using [workflows](/docs/product/workflows).
## How Salesforce maps to Plain
| **Salesforce** | **Plain** |
| ----------------------------------- | ------------ |
| Person (associated with list entry) | Customer |
| Company (list entry) | Tenant |
| Company attribute | Tenant field |
Tenants are the company-level grouping in Plain. Customers belong to tenants, the same way people belong to companies in Salesforce.
## Before you start
Create a list in Salesforce with only the companies you want in Plain.
You can sync all companies in your Salesforce account, but it may take longer than needed. We recommend filtering to the companies you need, such as active customers. Plain picks up changes to the list.
## Set up the sync
Go to **Settings → Integrations → Salesforce**, click **Connect**, and authorize Plain's access.
Click **Set up sync**, then select the list you want to sync. Plain shows how each Salesforce entity maps to Plain as you go, then lets you pick which company attributes to bring across.
Click **Enable sync**.
The first sync starts immediately. You'll see live progress as tenant fields, tenants, and customers are imported.
## How the sync works
After the first sync, Plain checks Salesforce for changes approximately once an hour, starting one hour after the previous run finishes.
**What syncs:**
* New companies added to your selected list become new tenants
* Changes to company attributes (like name or URL) sync to Plain
* New attributes on Salesforce companies become available for syncing as tenant fields. You'll need to enable them before we sync their data.
* New people associated with a list entry become new customers in Plain
* A person added to an existing Salesforce company joins the matching tenant in Plain
**What doesn't sync:**
* Companies with no name. These are filtered out and will not be imported.
* Updates to existing people. Plain only creates new customer records and doesn't overwrite existing ones. If you edit a customer in Plain, future syncs leave your changes alone.
* Contact removals. If you remove a contact from an Salesforce company, the tenant relationship in Plain stays intact. Remove it manually in Plain if needed.
* Company deletes and merges. If a company is deleted or merged in Salesforce, the corresponding tenant in Plain is not removed or updated. You'll need to remove or reassign stale tenants manually in Plain.
## Tenant fields
Plain imports properties from your Salesforce companies that are set as visible.
You can choose which properties you want to import during your first sync, or update it anytime in **Settings → Integrations → Salesforce → Sync settings**.
Toggle visibility on the fields you want to display in thread views. Visible tenant fields appear on the tenant card in every thread, so your team sees CRM context like company tier or funding stage without leaving Plain.
Changes apply on the next sync. If you turn a field on, we'll start syncing its data. If you turn it off, we'll remove its data from Plain.
## Sync history
The Salesforce settings page shows:
* The next scheduled sync time and the list it will use.
* A table of past runs with status, duration, and list name.
If a run fails, Plain marks it as failed and reschedules it automatically. You don't need to do anything for transient failures. If runs fail repeatedly, contact support.
## Write back to Salesforce
You can use [Workflow actions](/docs/product/workflows/workflows-actions) to create notes on Salesforce when a workflow is [triggered](/docs/product/workflows/workflows-triggers). For example, you could automatically add a note to a customer account whenever they create a new thread in Plain
## Disconnecting
Go to **Settings → Integrations → Salesforce** and click **Disconnect**. The sync stops immediately. Records already in Plain stay where they are.
## Troubleshooting
**A sync run failed.** Plain reschedules failed runs automatically. If the next run also fails, check that your Salesforce credentials are still valid and that the list you selected still exists.
**A person isn't appearing in Plain.** Confirm the person is associated with a company that's in your selected list. Plain only syncs people whose companies are in the list.
**A field I expected isn't showing on the tenant card.** Go to **Settings → Tenant fields** and check that visibility is toggled on for that field.
# Salesforce Service Cloud
Source: https://www.plain.com/docs/product/integrations/salesforce-service-cloud
Import Salesforce Service Cloud case history into Plain with a script built on the API.
Bringing your Salesforce Service Cloud case history into Plain takes a short script built on the Plain API. Your team keeps every past case, searchable alongside live threads, from the day you switch. This page covers how Service Cloud's objects map onto Plain's and the decisions to make before you run it.
This page is about moving your support history into Plain. To sync account and contact context onto live threads, see the [Salesforce integration](/docs/product/integrations/salesforce).
## What gets imported
* **Contacts → Customers**
* **Cases → Threads**
* You choose which case statuses land as **Done** and which stay **Todo**
* The case's email and comment history is carried over. Anything written for your team appears as an internal comment in Plain
* **Case fields you choose → Labels**
* **Original timestamps**: every thread and every message keeps the date it had in Salesforce, so your history stays in order
* **A link back to the original case**, so anyone reading the thread in Plain can still find it in Salesforce
CSAT ratings and custom fields on cases are not imported. See [Migrating from another tool](https://help.plain.com/article/migration) for the full picture of what you'll rebuild in Plain.
Importing history doesn't trigger [SLAs](/docs/product/platform/slas), [auto-responses](/docs/product/platform/auto-responses), or [workflows](/docs/product/workflows), so nothing goes out to your customers and no SLA clocks start on old tickets.
## Mapping service cloud onto Plain
Service Cloud is configured differently in every org, so plan this against your own setup:
* **Statuses.** Plain has three status groups: **Todo**, **Snoozed**, and **Done**. Your case statuses are yours to define, so decide which land as Done and which stay Todo. If the original status matters for reporting, add it as a [label](/docs/product/platform/labels) so you keep it.
* **What counts as the conversation.** A case's history can sit across emails, comments, and feed activity. Decide which of those your team reads back, and import those. Comments meant for users should come in as internal comments in Plain.
* **Case IDs.** Use the case ID as each thread's external reference. That's what makes the import safe to re-run.
* **Accounts.** Plain sets a customer's [Company](/docs/product/platform/companies) automatically from their email domain. If your accounts don't line up with domains, create a [Tenant](/docs/product/platform/tenants) per account instead and attach customers to it.
* **Case origin, type, and priority.** Plain threads carry a priority and labels rather than a set of custom picklists. Pick the fields your team filters on and bring those across as labels.
* **Owners.** Decide which case owners map to the users you've already invited to Plain, and what to do with anyone who has left.
## Running the import
If you'd like a hand scoping the script or a review before you run it at full volume, email [help@plain.com](mailto:help@plain.com).
# Shortcut
Source: https://www.plain.com/docs/product/integrations/shortcut
Link threads to Shortcut stories to track customer-reported bugs and requests.
Connect your Shortcut workspace to Plain to track feature requests and bugs reported by customers. Link a Shortcut story to any thread and get automatically prompted to follow up when the story is resolved.
## Setup
1. Go to **Settings → Shortcut**
2. Connect your Shortcut workspace
## Linking threads to shortcut stories
Once connected, open any thread and use the **Thread links** panel in the sidebar or press i to create a new Shortcut story or link an existing one.
When the story is completed, canceled, or deleted, the thread automatically moves to **Close the Loop**.
# Zendesk
Source: https://www.plain.com/docs/product/integrations/zendesk
Import your Zendesk support history into Plain with the built-in importer.
When you connect your Zendesk account, Plain imports your full support history so your team can hit the ground running without losing context.
## What gets imported
* **End users → Customers**
* **Tickets → Threads**
* Closed/solved tickets are imported as **Done**
* All others are imported as **Todo**
* All messages including internal notes are carried over. Notes appear as internal comments in Plain
* **Tags → Labels**
* **Original timestamps**: every thread and every message keeps the date it had in Zendesk, so your history stays in order
CSAT ratings and custom fields on tickets are not imported. See [Migrating from another tool](https://help.plain.com/article/migration) for the full picture of what you'll rebuild in Plain.
Importing history doesn't trigger [SLAs](/docs/product/platform/slas), [auto-responses](/docs/product/platform/auto-responses), or [workflows](/docs/product/workflows), so nothing goes out to your customers and no SLA clocks start on old tickets.
## Ongoing sync
After the initial import, new tickets, messages, and tags continue syncing to Plain automatically. Changes to existing tickets, such as status, priority, or assignee, are not synced after the initial import. No auto-responses are triggered by the sync.
## How to set it up
1. Go to **Settings → Zendesk importer**
2. Enter your Zendesk subdomain (e.g. `plain` for `https://plain.zendesk.com`)
3. Follow the prompts to connect your account and begin the import
A large history takes time to come across.
To stop syncing, return to **Settings → Zendesk importer** and click **Disconnect**.
# AI slash commands
Source: https://www.plain.com/docs/product/platform/ai-slash-commands
Get writing help from Plain AI in the composer with a slash command.
Use AI slash commands to get writing help from Plain AI directly in the composer, no context switching needed.
## How it works
While drafting a reply in a support thread, start a new line with / followed by your instruction, then press **Enter**. Plain AI will reference the last 5 messages in the thread alongside whatever you have already written in the composer, and replace the composer content with a ready-to-edit draft.
Slash commands work for:
* Drafting a reply from scratch
* Editing or rewriting something you have already written
* Adjusting tone, length, or clarity
## Examples
These are examples. You can give it any prompt:
* `/fix typos`
* `/ask for a screenshot and workspace ID so we can debug`
* `/ask what version of the SDK they are using`
* `/rephrase this to sound more helpful`
* `/summarize and confirm the issue before escalating`
## Things to know
* The slash command must be on its own line at the top level of the composer. It will not trigger inside a list or block element.
* The AI response replaces the entire composer content, including any text you had written before the slash command.
* Only the last 5 messages in the thread are included as context. Earlier messages in a long conversation will not be referenced.
* If the request fails, you will see a "Failed to generate or edit message" notification. Retry by typing the command again.
* Requires Plain AI to be enabled in your workspace settings.
# AI triage
Source: https://www.plain.com/docs/product/platform/ai-triage
Let Plain AI label, prioritize, and summarize new threads before anyone reads them.
Auto-triage automatically handles the first layer of triage: labeling, prioritizing, summarizing, and surfacing similar issues.
## Auto-labeling
When a new thread is created, Plain AI will automatically assign 1–2 labels based on the content of the request. If a thread is already labeled (via API or form), auto-labeling won't override it.
If you want to exclude a label from auto-labeling, you can uncheck the **Can be applied by Plain AI** setting on the label.
## Thread titles
In Slack and other real-time channels, threads arrive with no subject line. Auto-generated thread titles solve this by creating short, descriptive summaries for each conversation.
## Urgency detection (Beta)
Plain AI automatically detects when a thread is urgent, flagging threads that mention downtime, blockers, or critical impact.
## Thread catch-ups
Plain AI automatically generates and keeps an up-to-date summary for each thread, so you can see what happened without re-reading the full timeline.
## Similar threads (Beta)
To prevent duplicate work and speed up resolution, Plain AI can show **similar threads** in the thread details panel.
# Auto-responses
Source: https://www.plain.com/docs/product/platform/auto-responses
How auto-responses work in Plain, when they fire, and when they are suppressed.
Auto-responses send an automatic reply when a new thread is created. Use them to keep customers informed straight away, reduce duplicate follow-ups, and give your team time to respond thoughtfully.
To set up auto-responses, go to **Settings → Auto-responses.**
## When auto-responses are triggered
Auto-responses fire when a new thread is created. You can restrict them to specific conditions. All conditions on a single auto-response must match for it to send. If you have multiple auto-responses configured, they are evaluated in order and the first match wins.
Available conditions:
* **Support email address**: only applies when the thread comes in via email
* **Inside or outside business hours**: matches based on your workspace business hours setting
* **Label**: matches threads that have a specific label applied
* **Tier**: matches threads belonging to a specific customer tier
* **Priority**: matches threads at a specific priority level
If your workspace does not have business hours configured, the outside business hours condition always evaluates as matched (it is treated as always true).
## When auto-responses are suppressed
Even when an auto-response is enabled, it will not send in the following situations:
* **Thread created by an agent**: auto-responses only fire for threads created by a customer or a machine user (e.g. via API). Threads opened manually by a support agent are skipped.
* **Customer is marked as spam**: no auto-response is sent to spam customers.
* **Thread has no inbound message**: if the thread has no first customer message, the auto-response is skipped.
* **Thread is no longer in Todo when the response is due**: if the thread has been resolved, snoozed, or otherwise moved out of Todo before the scheduled send time, the auto-response is canceled.
* **An agent or machine user has already replied**: if any non-customer message is sent before the auto-response fires, for example during the response delay window, the auto-response is canceled. This prevents a second reply when a user has already picked the thread up.
## Response delay
You can configure a delay (up to 15 minutes) between when the thread is created and when the auto-response is sent. The delay is calculated from the timestamp of the first inbound message.
The delay window is also a safety net: if an agent replies within the delay, the auto-response is automatically suppressed so the customer does not receive two responses.
## Supported channels
Auto-responses can be enabled across multiple support channels:
* **Email**: for inbound messages to your support inbox
* **Chat**: for sessions started in your website widget
* **Slack**: for threads started via Slack Connect
* **API**: for programmatically created threads
* **Microsoft Teams**: for threads started via a Teams channel
* **Discord**: for threads started via a Discord server
You can configure separate auto-responses per channel, or share one across multiple channels.
## Using dynamic fields in automated replies
You can include variables in automated email responses to personalize messages automatically. Supported fields include:
* `{{ customer.fullName }}`
* `{{ customer.shortName }}`
* `{{ customer.email }}`
* `{{ thread.ref }}`
* `{{ thread.id }}`
### Basic usage
Place the variable directly in the reply template where you want the value to appear. For example:
```plaintext theme={null}
Hi {{ customer.shortName }},
Thanks for contacting us about thread {{ thread.ref }}. We will respond shortly.
```
## Advanced use: Custom auto-responses via API
If you need more control, such as dynamic content or conditional logic, you can build interactive auto-responses using Plains API and webhooks. See our [Thread auto-responders](/docs/graphql/threads/autoresponders) documentation for more.
# Business hours
Source: https://www.plain.com/docs/product/platform/business-hours
Define when your team works, so SLAs and workflows respect it.
Business hours tell Plain when your team is working. You can create multiple named schedules to represent different teams, regions, or support tiers. Other features check these schedules before acting: SLAs can pause overnight and workflows can route based on time of day.
Use business hours to avoid SLA timers ticking through weekends or on-call routing firing at unhelpful hours.
## Where business hours are used
Business hours feed into several other features:
* **SLAs.** SLA targets can be configured to count only business hours by referencing one or more schedules. With that turned on, if your first-response SLA is four hours and a thread arrives at 16:30 on a Friday with a Mon–Fri 09:00–17:00 schedule, the breach point is 12:30 on Monday rather than 20:30 on Friday. SLA targets that don't reference any schedules count time 24/7. [See SLAs →](/docs/product/platform/slas)
* **Workflows.** Workflows can condition on whether the current time is inside or outside a specific business hours schedule. Useful for "page the on-call only during EMEA working hours" or "auto-snooze new threads that arrive overnight". [See Workflows →](/docs/product/workflows)
* **Live Chat availability.** The Live Chat widget shows your team as online or offline based on business hours (and individual agent presence), so customers know whether to expect an immediate reply or an emailed follow-up.
## Setting up business hours
Go to **Settings → Business hours** to create and manage your schedules.
### Creating a schedule
1. Click **Add schedule** to create a new named schedule.
2. Give it a descriptive name like "EMEA Hours" or "US Hours".
3. Choose the **timezone**. All time ranges for this schedule are evaluated in this timezone.
4. For each day of the week, set the working time ranges. A typical office team uses Mon–Fri 09:00–17:00. A team running split shifts might use 07:00–11:00 and 14:00–20:00 on the same day. Days with no entries count as outside business hours for that schedule.
5. Save. SLA targets and workflow conditions can now reference this schedule.
Set business hours *before* you define your SLAs. Getting the schedules right first avoids surprises like first-response timers ticking through your weekend.
### Multiple schedules
On higher-tier plans, you can create multiple schedules to represent different teams or regions. Each schedule is independent, with its own name, timezone, and weekly hours. Reference specific schedules in SLA targets and workflow conditions to apply the right hours to the right threads.
## Common patterns
### Single team, single timezone
Most teams start here. One schedule, Mon–Fri, 09:00–17:00 in your local timezone. SLAs and workflows all reference the same schedule.
### Regional coverage
If you have support teams in EMEA, North America, and APAC, create a schedule for each region with its own timezone and working hours. Use workflow conditions to route threads to the right team based on which schedule is currently active, and configure SLA targets to count down against the relevant regional schedule.
### Holidays and one-off closures
Plain does not currently support date-specific holiday schedules. The business-hours screen describes your normal weekly schedule and does not have a way to mark specific dates as closed.
## What happens outside business hours
When a schedule says you're closed:
* SLA timers pause on any SLA target configured to respect that schedule. SLA targets configured for 24/7 keep counting.
* Workflow conditions checking "outside business hours" for that schedule evaluate to true. Conditions without a schedule check continue to run as normal, so triage, assignment, and labeling don't stop.
* Live Chat shows as offline when all schedules are closed. Customers see the offline message you've configured.
Need to confirm what your current schedules look like? **Settings → Business hours** shows the exact ranges, timezones, and names Plain is using right now.
# Companies
Source: https://www.plain.com/docs/product/platform/companies
Companies group customers by their email domain and carry tier and SLA settings.
A company groups customers by their email domain, so your team sees which account a thread belongs to and the company's tier and SLA apply automatically.
In Plain you can see what company a customer belongs to, so you have more context when providing support.
The company is automatically set based on the email domain of the customer and is visible throughout the support app. For example if a customer has the email with the domain **@nike.com** then their company will be **Nike**.
You can browse threads by company in Plain as well as associate a company with a tier to manage their SLAs.
Companies can also be listed and updated [**via the API**](/docs/graphql/companies).
## Viewing your company in Plain
Each Company has a Company page in Plain where you can add or see the following information:
* Domain
* Contract Value
* Account Owner
* Tier
* Support Volume (# of threads within the past 30 days)
* Individual customers
* Open issues (via Linear, JIRA, etc.)
* Open threads
You can also favorite a company so that it always appears in your lefthand sidebar.
# Customer surveys (CSAT)
Source: https://www.plain.com/docs/product/platform/csat
How to set up and manage customer satisfaction surveys in Plain.
Customer Surveys let you collect satisfaction ratings at the end of support threads. Use them to track sentiment over time and catch recurring issues early.
## How it works
When a thread moves to Done, Plain schedules a short survey asking the customer to rate their experience: Positive, Neutral, or Negative. There's also an optional comment field.
Surveys go out across all channels (Slack, Discord, email, and chat), so customers get them in the same place they had the conversation.
If a thread's status changes before the survey is sent, the scheduled send is canceled. Plain only sends the survey after the thread's most recent transition to Done.
## Where responses show up
* The thread sidebar, alongside the rest of the conversation.
* Insights > Reporting > CSAT, for aggregate trends and filtering by rating.
## Setting up a survey
Go to Settings > Customer survey and click New customer survey.
For each survey you can configure:
* The message text
* A send delay (for example, 15 minutes after a thread is marked Done)
* Targeting rules (which customers or tiers receive it)
* A frequency limit (how frequently any one customer can be surveyed)
## Best practices
Start with one general survey before adding segment-specific ones. Check the CSAT reporting view regularly to spot patterns, and set the frequency limit to avoid the same customer getting a survey after every thread.
Customer Surveys are available on Horizon and Frontier tiers. Contact [sales@plain.com](mailto:sales@plain.com) to upgrade.
# CSAT scores
Source: https://www.plain.com/docs/product/platform/csat-scores
How Plain measures CSAT: one response per thread, grouped by when the survey was submitted.
This article covers CSAT reporting in Insights. For setting up surveys, see [Customer Surveys (CSAT)](/docs/product/platform/csat).
Plain sends a short survey after a thread is resolved. Customers rate their experience as Positive, Neutral, or Negative and can leave an optional comment.
## Metrics
| Metric | What it measures |
| ----------------- | --------------------------------------------------------------------------------------------------- |
| CSAT thread count | Number of rated responses received in the period, filterable by rating. |
| CSAT - % positive | count(Positive ratings) / count(all ratings) x 100, for responses submitted in the selected period. |
Each thread has at most one CSAT response. The period filter applies to when the survey was submitted, not when the thread was created or resolved. Threads belonging to deleted or spam customers are excluded.
## Where to find CSAT data
Individual ratings and comments are visible in the thread sidebar. Aggregate trends and breakdowns are at Insights → Reporting → CSAT.
## Filtering and drill-down
CSAT supports the same drill-down dimensions as all other Insights metrics: Channel, Company, Group, Label, Priority, and Tier.
CSAT scores are not available per-agent in Team reporting.
CSAT is available on the Horizon and Frontier plans. Contact [sales@plain.com](mailto:sales@plain.com) to upgrade.
# Customer cards
Source: https://www.plain.com/docs/product/platform/customer-cards
Show data from your own systems next to a thread, and add actions your team can click.
[**Customer cards**](/docs/customer-cards) let you show information from your own systems and configure clickable actions that trigger a Plain workflow, without your team needing to leave the conversation or open a separate menu.
## How does data from my system get displayed in customer cards?
Customer cards:
* Pull from your backend, so you do not have to sync your customer data to Plain.
* The data is short-lived, so it isn't permanently stored in Plain beyond the time frame you set.
* Defined in JSON, so you do not write any styling.
* Automatically reloaded if a user is viewing a customer and the data expires.
## Workflow triggers from customer cards
When a thread is opened and your customer card API is called, you can include a `componentWorkflowButton` in your card response. Plain renders this as a button inside the card. When a team member clicks it, Plain runs the specified workflow in the context of that thread.
### Setting up workflow triggers:
This requires your engineering team to update your customer card API. Here's the two-step process:
**Step 1: Create the workflow in Plain.**
Go to **Settings → Workflows** and create the workflow you want the button to trigger. Your developer will need the workflow ID, which is visible in the workflow settings.
**Step 2: Add the button to your customer card API**
Your developer adds a `componentWorkflowButton` to your card API response, pointing it at the workflow ID. Your API can also include logic to only show the button in certain situations. For example, only when a customer's billing is in an error state.
## Use cases:
* **Billing operations**: resync a billing record, apply a credit, or trigger a refund
* **Account operations**: rename a team, allowlist a user, reset a rate limit
* **Escalation**: escalate a thread to the right team with a single click
* **Debugging**: trigger a diagnostics workflow tied to that customer's account
[**Jump to documentation →**](/docs/customer-cards)
## Examples:
## Try the examples
The [example cards repo](https://github.com/team-plain/example-customer-cards) is a working API you can point at your workspace. Each card is available at:
```plaintext theme={null}
https://example-customer-cards.plain.com/?cardKey=CARD_KEY
```
| **Card** | **Key** |
| ------------------- | --------------------- |
| Subscription status | `subscription-status` |
| Latest invoice | `latest-invoice` |
| Usage | `usage` |
| Sentry errors | `sentry` |
| Account details | `customer-details` |
| Last order | `last-order` |
| Workflow button | `workflow-card` |
To add one: go to **Settings → Customer Cards**, click **Add card**, enter the URL above with the relevant key, and open any thread to see it load.
# Customer groups
Source: https://www.plain.com/docs/product/platform/customer-groups
Organize and segment your customers with customer groups.
Customer groups let you segment your customers into named cohorts you define yourself. Unlike companies or tenants, which map to who a customer works for, groups are free-form: you create them for your own operational reasons, and a customer can belong to more than one at the same time.
The most common case is when you need a cross-cutting slice of your customer base that has nothing to do with company structure. A beta tester at Acme Corp and a beta tester at Initech belong to different companies but the same group.
## When to use them
Some examples of how teams use groups in practice:
* Beta programs, to flag customers trialing a feature before its general release
* VIP or strategic accounts that need faster response times or a dedicated agent
* Pilot cohorts in a structured onboarding or evaluation period
* Internal test users, so your own team's threads don't pollute your queue metrics
* Language or regional segments, to route threads to the right agent
* Churn-risk accounts flagged by your CSM team
## Setting up groups
Go to **Settings → Customer Groups**. Each group has a name (shown in the UI), a key (a short slug used to identify the group in the API, like `beta-testers`), and a color for the sidebar.
## Adding customers to a group
There are three ways.
**Manually from a thread:** open the thread, find the customer panel in the sidebar, and use the group picker.
**Via workflow rules:** add "Add customer to customer group" as a workflow action. You can then automatically group customers based on triggers, like tagging anyone who contacts you through a specific Slack channel, without writing any code.
**Via the API:** the `addCustomerToCustomerGroups` mutation assigns a customer to one or more groups in a single call. Use this to keep group membership in sync with an external system, like updating groups whenever a plan changes in a billing platform. See the [API docs](/docs/graphql/customers/customer-groups) for the full setup.
## Filtering threads by group
Each group shows up in your sidebar as a named filter with a live count of open threads. If you want to focus on all open threads from your beta cohort, it is one click.
## Deleting groups
You cannot delete a group that still has members. Remove all customers from the group first, then delete it from Settings.
# Customer waiting time
Source: https://www.plain.com/docs/product/platform/customer-waiting-time
How Plain sums every Todo window across a thread's lifetime to measure total customer waiting time.
Customer waiting time is the total time a thread spent in a Todo status, summed across the thread's entire lifetime. It differs from first response time in one way: it counts every stretch the thread was in Todo, not only the initial wait before the first reply.
## How it's calculated
Each time a thread enters Todo (for the first time or after being re-opened), a timer starts. When the thread leaves Todo (moved to Done or Snoozed), that window closes. Customer waiting time is the sum of all those windows.
Time in Snoozed or Waiting for Customer status does not count.
The metric is attributed to threads created in the selected period, regardless of when the individual waiting windows occurred. In Team reporting, a thread is credited to every user it was ever assigned to, not only the final assignee.
## Statistical views
**Median:** the midpoint waiting time across threads created in the selected period. Half waited less, half waited more.
**90th percentile:** 90% of threads had a lower total waiting time than this. High values suggest threads are sitting unattended across multiple back-and-forth cycles.
## How to use it
If customer waiting time is high relative to first response time, threads are likely returning to Todo repeatedly without reaching resolution. Comparing customer waiting time against resolution time shows what fraction of the total resolution window the customer spent actively waiting.
# Escalation paths
Source: https://www.plain.com/docs/product/platform/escalation-paths
Set up structured escalation levels and assign threads to them using the Workflow Builder.
**Escalation paths** allow your team to define a set of levels, so that if a team member is stuck when resolving a thread, they have a predefined path to escalate it to the next relevant person or team.
This ensures standardized handoffs and gives every thread a clear path to resolution.
## Configuration
Setting up escalation paths requires two steps:
1. Define the escalation path (name + levels) in Settings
2. Create a workflow in the Workflow Builder to assign threads to that path
### Adding an escalation path
1. Go to `Settings → Workflow → Escalation Paths`
2. Click "New Escalation Path"
3. Set a name and add your defined escalation levels
### Setting a thread to use an escalation path
1. Go to Workflows and create a new workflow or edit an existing one
2. Under **Actions**, pick **Set escalation path**
## Usage
### Escalating a thread
To escalate, click **More** at the bottom of the thread page, under the composer, then pick **Escalate**
### Changing or clearing the escalation path
To manually change which escalation path a thread uses, or to remove it entirely:
1. Press ⌘ + K to open the Command Palette
2. Search for **"Update Escalation Path"**
### Things to know
* When escalating, the currently assigned user is always unassigned
* The currently assigned team label is removed only if it is part of the escalation level being escalated from
* The current escalation level is evaluated at the moment of escalation. Manual user/team assignments can affect which level is considered "current"
## Example
At Plain, we use the following escalation path for product questions. It's assigned via a workflow that triggers on threads from `support@plain.com`:
1. Technical Support *(team)*
2. Engineering *(team)*
3. Staff Engineer *(user)*
4. Founders *(team)*
The workflow that sets this path uses a single condition step, "Support email equals [support@plain.com](mailto:support@plain.com)", followed by a **Set escalation path** action.
# First response time
Source: https://www.plain.com/docs/product/platform/first-response-time
How Plain calculates FRT: from the first inbound message to the first human reply.
First response time (FRT) is the time from the first inbound customer message to the first human reply on a thread. It is one of the metrics your SLA policies track.
## How it's calculated
FRT = time from the first inbound customer message to the first outbound human reply.
* The clock starts on the first inbound customer message, not on thread creation. These differ when a thread is created programmatically before the customer sends anything.
* Machine user and AI-generated replies do not stop the clock. Only a human agent's first reply ends the FRT measurement.
* Threads created by a Plain user are excluded entirely.
* For charting purposes, threads are grouped by when the first reply was sent, not when the thread was created.
Median and 90th percentile are quantile(0.5) and quantile(0.9) across all FRT values in the window.
## Statistical views
**Median:** the midpoint FRT across all threads in the selected period. Half of threads got a first reply faster than this, half slower. Less skewed by outliers than the average.
**90th percentile:** 90% of threads received a first reply within this time. Use it to see how the slowest 10% of threads are performing.
## Team member view
In Team reporting, FRT covers only threads where that team member sent the first reply.
## SLA tracking
A thread breaches its FRT SLA if no human reply arrives before the configured deadline. See [SLA compliance](/docs/product/platform/sla-compliance) for how the breach rate is calculated.
# Heatmaps
Source: https://www.plain.com/docs/product/platform/heatmaps
How Plain's heatmaps aggregate thread and message counts by hour and day of week.
Heatmaps show when threads arrive and when agents respond, broken down by hour of day (UTC) and day of week. Use them to spot coverage gaps and plan staffing.
## Available heatmaps
| Heatmap | What it shows | Where |
| --------------- | ---------------------------------------- | ------------------ |
| Threads created | New inbound threads by day and hour. | Main Insights view |
| Messages sent | Outbound agent messages by day and hour. | Team pages only |
## How each cell is calculated
Each cell shows the count for that hour-of-day and day-of-week combination, summed across every week in the selected period. A 4-week period means each cell aggregates up to 4 days' worth of data. Hour buckets run 0-23 UTC.
Thread counts exclude threads from deleted or spam customers and threads created by Plain users. Message counts cover outbound agent messages only; auto-replies and AI-generated messages are not included.
## How to read it
Darker cells mean more activity. Hover over any cell to see the exact count.
## How to use it
Set the period to at least 4 weeks to get a reliable pattern. Comparing the Threads created heatmap against Messages sent (from a team member's page) shows when threads are arriving but no agent is responding.
# Insights
Source: https://www.plain.com/docs/product/platform/insights
What Plain Insights covers and how to navigate it.
Insights shows you what's happening across your support operation. It covers the metrics side (volumes, response times, SLAs) as well as higher-level views like team performance, trending topics, and customer satisfaction.
## What's in insights
There are two main areas.
Metrics and charts cover the quantitative side:
* Support volume: how many threads you're handling over time
* First response time: how long customers wait for their first reply
* Next response time: how long you take to follow up on an ongoing thread
* Resolution time: how long it takes to close a request
* Customer waiting time: the total time a customer spends waiting across a whole conversation, not only before the first reply
* SLA compliance: the percentage of threads meeting your response time targets
* CSAT scores: customer satisfaction ratings from completed threads
* Heatmaps: support activity by day of week and hour
Reporting features give you broader views:
* Team reporting: per-agent and team-level metrics for managers
* Top issues: which product backlog items are generating the most threads
* Themes: AI-detected topic clusters across recent conversations
* Customer Surveys: configuring and sending post-thread satisfaction surveys
## Filtering
Every chart can be broken down by Channel, Company, Group, Label, Priority, or Tier. Hover over any company, group, label, or tier anywhere in Plain to see a volume snapshot inline.
## Data refresh
Threads in Todo updates in real time. Everything else refreshes hourly.
## Custom reporting
If you need metrics that aren't in Insights, or want to export data to a BI tool, reach out at [help@plain.com](mailto:help@plain.com).
# Keyboard shortcuts
Source: https://www.plain.com/docs/product/platform/keyboard-shortcuts
Every keyboard shortcut in Plain, grouped by where it applies.
Keyboard shortcuts let you work a thread without reaching for the mouse.
This is the list of available keyboard shortcuts in Plain:
## Global shortcuts
* ⌘ + K: Open the command palette
* ⌘ + K then **Copy link**: Copy a shareable link to your current page directly from the command palette
* ?: Open help panel with full list of available shortcuts
* T: Create task
## On thread queues
* F: Add filter
* /: Search for customers or threads
* V: Toggle display options
* ⇧ + Click: Select multiple threads for bulk actions
## When viewing a thread
* R: Focus composer to reply
* N: Add a note to a thread
* B: Ask Sidekick
* ⌘ + ⇧ + B: Toggle Sidekick position
* A: Assign the thread
* E: Mark a thread as `Done`
* Z: Pause for later
* W: Wait for customer
* S: Change status
* L: Add label
* P: Set priority
* D: Start discussion
* I: Add Thread link
* X: Escalate thread
* O: Open in Slack
* ⌘ + ⇧ + X: Copy thread URL
* J / K: Next/Previous thread
* ⌘ + .: Show/hide queue
* ⌘ + /: Show/hide sidebar
* esc: Go back to queue
* V: Toggles display options
## When the composer is focused
* ⌘ + ⏎: Send message
* ⌘ + ⇧ + ⏎: Send message and mark thread as done
* \[: Insert snippet
* esc: Exit composer
* ⌘ + U: Create snippet
## Reply formatting
* ⌘ + B: Bold
* ⌘ + I: Italic
* ⌘ + ⇧ + S: Strikethrough
* ⌘ + ⇧ + 7: Ordered list
* ⌘ + ⇧ + 8: Bullet list
* ⌘ + E: Code
* ⌘ + ⌥ + C: Code block
# Labels
Source: https://www.plain.com/docs/product/platform/labels
Categorize threads with labels, nest them into hierarchies, and apply them automatically with workflows.
Labels categorize threads so your team can see what each one is about at a glance. Use them to prioritize, route, and report on customer requests across the queue.
To manage labels, open the command palette with ⌘ + K and search for **Manage labels**, or go to **Settings → Labels**.
## Apply a label
1. Open the thread.
2. Click **Labels** in the thread sidebar.
3. Search for or select a label from the list.
## Organize labels
### Nested labels
Labels support up to **three levels** of hierarchy. When a label is applied, all of its parent labels are applied automatically. Use nesting to group related labels: for example, `Billing > Refund` or `Bug > Critical`.
### Team labels
In Plain, teams are defined through labels. A team label represents a team or functional group in your workspace, applying it signals ownership and enables team-based routing, filtering, and assignment.
Common examples: `Support`, `Payments`, `Onboarding`, `Enterprise`.
## Automate labels
### AI auto-labeling
Plain can apply labels automatically using AI. When creating or editing a label, turn on **Can be applied by Plain AI** to enable this.
### Workflows
Labels can be set to automatically apply based on conditions using [Workflows](/docs/product/workflows). For example, you could automatically apply a label like `Churn risk` whenever a thread mentions cancellation.
### API
Labels can also be applied programmatically using [Plain's GraphQL API](/docs/graphql/labels).
# Metrics reference
Source: https://www.plain.com/docs/product/platform/metrics
Every metric in Plain Insights, what it measures, and its team member variant.
Every metric in Plain Insights: what it measures, how it's calculated, and whether there's a team member variant. For a deeper explanation of any metric, see its dedicated article.
All core metrics can be filtered by Channel, Company, Group, Label, Priority, and Tier.
## Core metrics
| Metric | What it measures | Team member variant |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------- |
| Threads in Todo | Snapshot count of threads currently in Todo status. Updates in real time. | N/A |
| New threads created | Number of new threads opened in the selected period. | N/A |
| Threads reopened | Number of threads that moved back to Todo from Snoozed or Done. | N/A |
| Threads moved to Done | Number of threads that transitioned to Done. Auto-closed threads are not counted in the team member variant. | Threads this team member moved to Done. |
| First response time | Time from thread creation to first human reply. | Threads where this team member sent the first response. |
| SLA compliance: first response | % of threads that received a first human reply within the SLA target. | Filtered to threads where this team member sent the first response. |
| Next response time | Time between follow-up replies after the first. | Time between responses sent by this team member. |
| SLA compliance: next response | % of threads that received follow-up replies within the SLA target. | Filtered to threads where this team member sent the response. |
| Resolution time | Time from first customer message to thread marked Done. | Threads this team member resolved. |
| Customer waiting time | Total time a thread spent in Todo status. | Threads assigned to this team member. |
| CSAT thread count | Number of CSAT responses, filterable by rating. | Not available per agent. |
| CSAT: % positive responses | % of CSAT responses rated Positive. | Not available per agent. |
| Heatmap: threads created | Thread volume by day-of-week and hour-of-day (UTC). | N/A |
## Team-only metrics
These only appear on individual team member pages.
| Metric | What it measures |
| ---------------------- | ------------------------------------------------------------------ |
| Messages sent | Number of messages sent by the team member in the selected period. |
| Thread assignments | Number of times a thread was assigned to the team member. |
| Heatmap: messages sent | Messages sent broken down by day-of-week and hour-of-day (UTC). |
## Data refresh
Threads in Todo is a real-time snapshot. All other metrics refresh hourly.
# Next response time
Source: https://www.plain.com/docs/product/platform/next-response-time
How Plain calculates NRT: every consecutive reply gap across all threads in the period.
Next response time (NRT) is how long a customer waits for each follow-up reply after the first one. Where FRT covers the opening exchange, NRT covers everything after that.
## How it's calculated
NRT is the gap between every pair of consecutive outbound replies on a thread, not only the first and second. A thread with 5 replies contributes 4 individual gap measurements, each counted as a separate data point.
* Machine user replies are excluded from both ends of each gap.
* The window covers threads created in the selected period, not threads whose replies were sent in that period.
* Median and 90th percentile are computed across all individual gap measurements, not per-thread averages.
## Statistical views
**Median:** the midpoint gap across all follow-up reply pairs in the selected period.
**90th percentile:** 90% of follow-up gaps were shorter than this. Use it to identify the slowest-moving conversations.
## Team member view
In Team reporting, NRT covers reply gaps where that agent sent the follow-up reply. It is not filtered to same-agent pairs.
## SLA tracking
NRT has its own SLA target, separate from FRT. A thread breaches its NRT SLA if a human follow-up reply does not arrive within the configured window. See [SLA compliance](/docs/product/platform/sla-compliance) for breach rate details.
# Personal notifications
Source: https://www.plain.com/docs/product/platform/personal-notifications
Choose which threads and mentions notify you, and where those notifications go.
Personal notifications in Plain keep you informed about the threads you own, the conversations you're involved in, and the mentions directed at you, without the noise of workspace-wide activity.
You can receive personal notifications via **Slack DM**, **email**, and **in-app**. Each channel has its own settings, and you can mix and match to suit how you work.
## Getting to your notification settings
Click your **avatar** in the bottom-left corner of Plain, then select **Preferences**. In the left sidebar, under **Notifications**, you'll find separate sections for **Email notifications** and **Slack notifications**.
You can also press ⌘ + K (Mac) or Ctrl + K (Windows/Linux) to open the command palette, type **Personal preferences**, and go to the same place.
## Slack notifications
Slack notifications are delivered as direct messages from the Plain Slack App. That keeps your updates private, and out of a busy `#support` channel.
To receive Slack notifications, the Plain Slack App needs to be installed in your workspace and your Plain account must be connected to your Slack account.
**You can be notified when:**
* A thread is assigned to you
* A thread assigned to you receives a new reply from a customer
* A thread you're following gets new activity
* An SLA on a thread you own is about to breach
To configure, go to **Preferences → Notifications → Slack notifications** and toggle on the events you care about.
## Email notifications
Email notifications are sent to the address associated with your Plain account. They're useful if you're away from Slack or want a paper trail of key activity.
**You can be notified when:**
* A thread is assigned to you
* A customer replies to a thread you're assigned to
* You're mentioned in an internal note
To configure, go to **Preferences → Notifications → Email notifications** and enable the relevant triggers. You can also set a **notification delay**. Plain waits a short period before sending an email, so if you've already seen the activity in-app, you won't receive a redundant email.
## In-app notifications
In-app notifications appear via the **bell icon** in the bottom-left of Plain. Unlike Slack and email, in-app notifications are always active.They're the most immediate way to catch activity without leaving the app. Plain currently shows in-app notifications for:
* A thread being assigned to you by another team member
* Being @mentioned in an internal note
* An email you sent failing to deliver
Click the bell to see your notification feed. Unread notifications are indicated by a dot on the icon.
# Online, offline, and away mode
Source: https://www.plain.com/docs/product/platform/personal-status
Set yourself online, offline, or away, and control what happens to your threads.
Users can be in one of three statuses:
* **Online**: When you are online, the "assign to team" workflow will assign you threads as usual, and you will appear online to your team.
* **Offline**: When you are offline, the "assign to team" workflow will no longer assign threads to you, and you will appear offline to your team.
Designed for short-term offline periods like over night or lunch break and will not result in the user being unassigned from any threads.
* **Away**: When set to away, the "assign to team" workflow will not assign you threads. This is designed for longer term away periods, like going on holiday.
Away mode has an additional configuration option on the workspace level which controls how thread unassignment works for those set to "Away".
Admins choose the Away behavior in **Settings → Workflow**. Either you are unassigned from every thread the moment you go Away, or only from threads that receive a new message while you are away.
# Resolution time
Source: https://www.plain.com/docs/product/platform/resolution-time
How Plain calculates resolution time, including the 3-step fallback for determining the Done timestamp.
Resolution time is the elapsed time from the first inbound customer message to when the thread was moved to Done.
## How it's calculated
**Start point:** the first inbound customer message timestamp, not thread creation time. The two differ when a thread is created before the customer sends their first message.
**End point, in order of priority:**
1. If an agent manually moved the thread to Done, that timestamp is the resolution point.
2. If the most recent Done was set automatically (e.g. by a workflow), the calculation uses the last manually-set Done before that instead.
3. If no manual Done exists, it falls back to the last outbound message timestamp.
Only threads with a Done status, a non-null first inbound message, and a non-null last outbound message (where last outbound is later than first inbound) are included. Threads created by a Plain user are excluded. The window is when the thread moved to Done, not when it was created.
## Statistical views
**Median:** the midpoint resolution time across threads that moved to Done in the selected period. Half resolved faster, half slower.
**90th percentile:** 90% of threads resolved within this time. High values point to a tail of stalled or complex cases.
## Team member view
In Team reporting, resolution time covers threads that team member moved to Done.
You can configure auto-resolution in Settings → Workflow.
# SLA compliance
Source: https://www.plain.com/docs/product/platform/sla-compliance
How Plain calculates SLA compliance: Achieved vs Breached outcomes as a percentage.
SLA compliance is the percentage of threads where your team met its configured response time target. Each thread gets one outcome: Achieved or Breached. Threads still within their SLA window are not counted until they reach one of those two states.
## How it's calculated
Compliance % = count(Achieved) / count(Achieved + Breached) x 100
A thread reaches Achieved when a human reply arrives before the SLA deadline. It reaches Breached when the deadline passes with no human reply. Each thread is counted once, at the moment its outcome is determined. Re-opening a thread later does not change the original outcome.
Only human replies count. AI-generated replies do not satisfy SLA requirements.
## Metrics
| Metric | What it measures |
| ------------------------------- | --------------------------------------------------------------------------------------------------- |
| SLA compliance - First response | % of threads (with a terminal SLA outcome) that received a first human reply within the FRT target. |
| SLA compliance - Next response | % of threads that received every follow-up reply within the NRT target. |
## Team member view
FRT SLA compliance covers threads where that team member sent the first reply. NRT SLA compliance covers threads where that team member sent the follow-up reply. Threads are credited to every agent ever assigned to them.
## Configuring SLA targets
SLA targets are defined in [SLA policies](/docs/product/platform/slas) under **Settings → SLA policies**. A policy holds an FRT target, an NRT target, or both, and sets whether they pause outside business hours.
# SLA policies
Source: https://www.plain.com/docs/product/platform/slas
Define SLA targets as policies, choose whether they pause outside business hours, and apply them to threads with workflows.
A Service Level Agreement (SLA) policy is a named set of SLA targets that you apply to threads. It says how fast your team promises to reply, whether the clock pauses outside business hours, and how long before a deadline your team is warned. Manage your policies in **Settings → SLA policies**.
SLA policies are available on the Horizon pricing plan and above.
SLA policies replace the legacy SLAs configured on tiers. If your workspace still uses tier SLAs, see [Tier SLAs](/docs/product/platform/slas/tier-slas) instead.
## How SLA policies work
A policy only takes effect once it's applied to a thread, by a [workflow](/docs/product/workflows) or by a user. From that moment the thread tracks the policy's targets. A thread has one policy at a time, or none, and a thread without a policy has no SLA.
Because the policy is chosen per thread, you decide what each thread is promised. A workflow can apply an **Enterprise** policy to threads from your largest customers, an **Urgent** policy to any thread marked Urgent, and a slower policy to everything else.
SLAs only track threads that need a reply from your team. A thread that is Done, or Snoozed as **Waiting for customer**, has no active target.
## What a policy contains
Every policy has the same five parts:
* **Name**: shown on the thread, in filters, and in group headers. Names are unique within your workspace
* **Description**: an internal note. Only your team sees it
* **Targets**: a first response time, a next response time, or both
* **Notification before target**: an optional warning period for each target
* **Business hours**: whether the targets pause outside your business hours schedules
### Targets
A policy tracks up to two targets, one of each type:
* **First response time**: how long your team has to send the first reply after a thread is created. Tracking starts when the thread arrives
* **Next response time**: how long your team has to reply to each new customer message after the first reply. Tracking restarts with every customer message
Set each target in hours and minutes. A target applies to every thread the policy is on. Which threads that is depends on how the policy is [applied](#apply-a-policy-to-threads), so a policy has no conditions of its own.
### Notification before a target
Each target can send a warning before its deadline. When the time left falls below the period you set, the SLA status changes to **Imminent breach** and Plain sends a notification to the channels chosen in **Settings → Workspace notifications**. The warning period must be shorter than the target itself.
### Business hours
Each policy has one **Pause outside business hours** setting, and it covers every target in the policy:
* **Off**: targets tick down around the clock, including weekends and holidays
* **On**: targets tick down only while one of the [business hours](/docs/product/platform/business-hours) schedules you select is open. Outside those hours the timer pauses, and it resumes when the next schedule opens
With a 4 hour first response target and a Mon–Fri 09:00–17:00 schedule, a thread that arrives at 16:30 on a Friday is due at 12:30 on Monday. With the setting off, the same thread is due at 20:30 on Friday.
When you select more than one schedule, time counts whenever any of them is open. A policy covering an EMEA schedule and a North America schedule only pauses while both teams are offline.
## Create a policy
Go to **Settings → SLA policies** and click **New SLA policy**.
Enter a **Name**, for example `Enterprise EU`, and an optional **Description**.
Under **Business hours**, turn on **Pause outside business hours** and select one or more **Schedules**. Leave it off for targets to count around the clock.
Tick **First response time**, **Next response time**, or both, and enter each target in hours and minutes.
For each target, set **Send a notification before target** if you want an Imminent breach warning. Leave it empty to skip the warning.
Click **Create SLA policy**.
To create a variation of an existing policy, open its menu on the **SLA policies** page and click **Clone**. The copy opens prefilled for you to adjust.
## Edit or delete a policy
Editing a policy changes the targets for every thread on it, and the change reaches threads differently depending on what you edit:
* **Changing a target's time**: applies to targets that start after you save. A target already counting down keeps its existing deadline
* **Removing a target**: stops tracking that target on every thread the policy is applied to, including targets counting down right now
* **Changing the business hours setting or schedules**: applies to targets that start after you save
Deleting a policy removes it from every thread it was applied to and cancels their open targets. Delete a policy from its menu on the **SLA policies** page, or from the **Delete** option when editing it. This cannot be undone.
## Apply a policy to threads
Applying a policy is what starts the clock. Use a workflow so every matching thread gets the right policy without anyone thinking about it, and set or change the policy by hand when a thread needs an exception.
### With a workflow
Add a **Set SLA policy** [action](/docs/product/workflows/workflows-actions) to a workflow and pick the policy. The workflow's [trigger](/docs/product/workflows/workflows-triggers) decides when the policy is applied, and its [conditions](/docs/product/workflows/workflows-conditions) decide which policy a thread gets. The same action can also remove a policy: pick no policy, and the thread stops tracking SLAs.
#### Apply a policy by tier
The most common setup gives each customer segment its own policy. A thread's tier can change after it is created, for example when a company is put in a tier or [tier mapping](/docs/product/platform/tiers#tier-mapping) moves a tenant, so trigger on both events:
1. **Trigger**: **Automatic**, events: **Thread created** and **Tier updated**.
2. **Condition**: **Tier equals** Enterprise.
* **Yes**: **Set SLA policy** to **Enterprise**.
* **No**: **Set SLA policy** to **Standard**.
A new thread starts tracking the right targets from the moment it arrives. When its tier later changes, the workflow runs again and swaps it onto the matching policy. The same pattern works with the **Priority changed** and **Labels changed** events, for example an **Urgent** priority or a **Security** label that applies a tighter policy.
### From the thread
To set or change the policy on a single thread:
* Click the **SLA policy** row in the thread sidebar and pick a policy from the list. Choose **No SLA policy** to remove it
* Open the command palette with ⌘ + K and search for **Add SLA policy to thread**, **Change SLA policy on thread**, or **Remove SLA policy from thread**
## Follow a policy's targets on a thread
Once a policy is applied, each of its targets moves through a series of statuses as time passes and your team replies. The thread header and the queue show a countdown to the nearest deadline, and the status is available as a workflow trigger.
| Status | What it means |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| **Pending** | The target is counting down. The deadline hasn't passed and no warning has fired. |
| **Imminent breach** | The warning period has started. The deadline hasn't passed yet, but it's close. This only appears on targets with a notification before target. |
| **Breaching** | The deadline has passed and the reply still hasn't been sent. |
| **Achieved** | The reply was sent before the deadline. |
| **Breached** | The deadline passed before the reply, and the reply has since been sent. |
| **Canceled** | The target no longer applies. For example, the thread was marked as Done without a reply. |
Only replies from a user count. A reply from an [agent](/docs/product/agents/ari) doesn't achieve a target.
### Snoozed threads
What happens to a target when you snooze a thread depends on which snooze status you use:
| Snooze status | What happens to the target |
| ------------------------ | ---------------------------------------------------------------------------------------------------------- |
| **Waiting for customer** | The target is canceled. You've replied and are waiting on the customer, so nothing is due. |
| **Paused for later** | The target keeps counting down. You've set the thread aside, but the customer is still waiting on a reply. |
## Change or remove a policy on a thread
When a thread is moved from one policy to another, the old policy's targets that are still counting down are canceled and the new policy's targets start. The new targets measure from the same point the old ones did: the thread's creation for first response time, and the latest customer message for next response time. A thread that has waited 3 hours already has 3 hours counted against the new policy.
Moving a thread to another policy only touches targets still counting down. If your team has already sent the first reply, the new policy doesn't reopen a first response target, and whether that reply was on time stays as it was. Removing the policy from a thread cancels any target still counting down. Every move is recorded on the thread's timeline, with the policy names as they were at the time.
To change the targets in the policy itself rather than which policy a thread is on, see [Edit or delete a policy](#edit-or-delete-a-policy).
## Find threads by policy
The policy on a thread is available wherever you organize your queue:
* **Filter**: add an **SLA policy** filter to any view, or to My threads, to see the threads on one or more policies
* **Group**: in the **Display** menu, group a view by **SLA policy**. Threads without a policy appear under **No SLA policy**
* **Board**: with a view grouped by policy, drag a thread between columns to change its policy
* **Sort**: sort a view by **Closest to breaching SLA** to work the tightest deadlines first
## Manage SLA policies with the API
Policies can also be created, updated, deleted, and applied to threads with the [GraphQL API](/docs/graphql/sla-policies).
# Tier SLAs
Source: https://www.plain.com/docs/product/platform/slas/tier-slas
How SLAs attached to a tier work: the two SLA types, their priority and label conditions, statuses, snoozed threads, and business hours.
[SLA policies](/docs/product/platform/slas) supersede tier SLAs. Existing tier SLAs keep working, and you can still edit or delete them.
A Service Level Agreement (SLA) is a promise about how fast your team replies. Plain measures each thread against the SLA that applies to it, shows a countdown in the thread and the queue, and warns your team before the deadline passes. Only replies from your team stop the clock, and an SLA is only active while a thread needs a reply.
A tier SLA is an SLA attached to a [tier](/docs/product/platform/tiers). Every thread from a company or tenant in that tier is measured against it. Within a tier, you can set different SLAs per priority and label, so an Enterprise customer's Urgent thread can have a tighter SLA than a Free customer's Low priority thread. Manage tier SLAs in **Settings → Tiers & SLAs**.
## SLA types
A tier can hold two types of SLA:
* **First response time**: how long your team has to send the first reply to a new thread. The clock starts when the thread is created
* **Next response time**: how long your team has to reply after a customer responds. The clock restarts with every customer message
A tier needs a first response time SLA before you can add a next response time SLA, and you can't remove the first response time SLA while a next response time SLA exists.
Auto-responses don't count as a reply.
## Add an SLA to a tier
Tiers and SLAs are available on the Horizon pricing plan and above. To add an SLA:
Go to **Settings → Tiers & SLAs** and open the tier.
Click **Add SLA** and pick **First response time** or **Next response time**.
Enter the time allowed in hours and minutes. Turn on **Only during business hours** if the clock should pause outside your [working hours](#business-hours).
Click **Add priority** or **Add label** to apply the SLA only to some threads. Leave both empty to apply it to every thread in the tier. See [How an SLA is matched to a thread](#how-an-sla-is-matched-to-a-thread).
Under **Escalate to your team**, enter how long before the deadline Plain should notify you. See [Notify before breach](#notify-before-breach).
Click **Save changes**.
## How an SLA is matched to a thread
A thread's tier comes from its company or tenant. Plain looks at that tier's SLAs and applies the ones whose conditions the thread matches. An SLA with no conditions matches every thread in the tier. Conditions narrow it down:
* **Priority**: the SLA applies only when the thread's priority is one of the ones you add: **Urgent**, **High**, **Normal**, or **Low**
* **Labels**: the SLA applies only when the thread has any of the labels you add. Turn on **Require all labels** for it to apply only when the thread has every one of them
Plain rejects a new SLA if an existing SLA of the same type covers the same priority and matches the same label as it. Two SLAs on the same priority can coexist only when their label conditions are distinct, for example one for **Bug** threads and one for **Billing** threads.
A thread with both of those labels matches both SLAs. When that happens, an SLA with **Require all labels** on wins. Otherwise the SLA that was created first wins.
When a thread's priority or labels change, or its company or tenant moves to a different tier, Plain re-evaluates which SLA applies.
## Notify before breach
Under **Escalate to your team**, set how long before the deadline the SLA moves to **Imminent breach**. The warning period must be shorter than the response time.
When an SLA reaches **Imminent breach**, and again when it reaches **Breaching**, Plain posts a warning to the Slack or Discord channels that have **SLA warnings** turned on under **Settings → Notifications**.
## SLA statuses
Each SLA moves through a series of statuses as time passes and your team responds. You'll see these in the thread view and when setting up workflow triggers.
| Status | What it means |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Pending** | The SLA timer is counting down. The deadline hasn't passed and no warning has fired. |
| **Imminent breach** | The warning period is active. The deadline hasn't passed yet, but you're approaching it. This only appears if you've set a notify before breach time on the SLA. |
| **Breaching** | The deadline has passed and the SLA condition still isn't met. For example, no first reply has been sent inside the first response time. |
| **Achieved** | The SLA condition was met in time. For example, a first reply was sent before the deadline. |
| **Breached** | The SLA passed through Breaching without being met, but the underlying condition has since been resolved, for example a reply was eventually sent. |
| **Canceled** | The SLA no longer applies. For example, a thread was marked as Done without a reply being sent. |
## SLAs and snoozed threads
What happens to the SLA timer when you snooze a thread depends on which snooze status you use:
| Snooze status | SLA timer behavior |
| ------------------------ | --------------------------------------------------------------------------------------------------- |
| **Waiting for customer** | The timer is canceled. You've replied and are waiting on the customer, so no SLA applies. |
| **Paused for later** | The timer keeps running. You've paused the thread, but the customer is still waiting on a response. |
## Business hours
By default, SLA timers run 24/7. Turn on **Only during business hours** on an SLA to count time only while your team is working. Set your working schedule in **Settings → Business hours**.
With a 4 hour first response time and a Mon–Fri 09:00–17:00 schedule, a thread that arrives at 16:30 on a Friday is due at 12:30 on Monday. With the setting off, the same thread is due at 20:30 on Friday.
## Manage tier SLAs with the API
Tier SLAs can also be created, updated, and deleted with the [GraphQL API](/docs/graphql/tiers/service-level-agreements).
# Snippets
Source: https://www.plain.com/docs/product/platform/snippets
Save reusable replies with dynamic variables and insert them with a keystroke.
Snippets are reusable message templates that help your team reply faster, stay consistent, and avoid repetitive typing.
Manage your workspace snippets in **Settings → Snippets**.
## Use a snippet
1. Start typing a reply in any thread.
2. Press `[` to open the snippet menu.
3. Search by name or scroll to find the snippet you want.
4. Select it to insert it into your reply.
## Organize with groups
Snippets can be organized into groups to keep your library tidy as it grows. Groups appear in the snippet menu, so users can find the right template without scrolling.
To create a group, go to **Settings → Snippets** and click **New group**.
## Dynamic variables
Snippets support variables that automatically populate with thread or customer data when inserted.
| Variable | Inserts |
| -------------------------- | --------------------------- |
| `{{ customer.fullName }}` | The customer's full name |
| `{{ customer.shortName }}` | The customers first name |
| `{{ customer.email }}` | The customers email address |
| `{{ user.fullName }}` | Your full name |
| `{{ user.publicName }}` | Your public display name |
| `{{ thread.ref }}` | The thread reference number |
| `{{ thread.id }}` | The internal thread ID |
Use variables to personalize replies without extra manual effort.
# Speech to text
Source: https://www.plain.com/docs/product/platform/speech-to-text
Dictate a reply straight into the composer instead of typing it.
Speech to text lets you dictate messages directly into the composer. When you start recording, your voice is transcribed in real time and inserted as text.
**Where to enable it:**
Settings → Preferences → Speech to Text → toggle on *Enable speech to text*
* A microphone button will appear in the composer
* Click to start recording and click again to finish
Your browser will prompt for microphone permission the first time you record. You'll need to allow this for the feature to work.
Speech to text supports English only at this time.
# Support volume
Source: https://www.plain.com/docs/product/platform/support-volume
How Plain counts threads, status transitions, and queue size in the Support volume report.
Support volume shows how much work is coming into your workspace and whether that load is growing or shrinking. The metrics below count threads and status transitions, not messages.
## Metrics
| Metric | What it measures |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Threads in Todo | Running count of threads waiting for a reply. Plain adds 1 each time a thread enters Todo and subtracts 1 each time it leaves (to Done, Snoozed, or Ignored). The chart shows how that total changed over the selected period. |
| New threads created | Count of new threads opened in the selected period, grouped by day or hour. |
| Threads reopened | Count of status transitions where a thread moved back to Todo from Done or Snoozed. A thread that bounces back multiple times is counted each time. |
| Threads moved to Done | Count of status transitions where a thread moved to Done. A thread re-opened and closed again is counted each time it transitions to Done. |
None of these metrics include threads created by a Plain user (internal threads) or threads belonging to deleted or spam customers.
## How to use it
Compare "New threads created" against "Threads moved to Done" to see whether your queue is growing or shrinking. Drill down by Channel, Company, Group, Label, Priority, or Tier to find where volume is coming from. The heatmap shows which hours and days drive the most inbound traffic.
Threads in Todo updates in real time. All other volume metrics refresh hourly.
# Tasks
Source: https://www.plain.com/docs/product/platform/tasks
Track internal follow-ups in Plain, linked to a thread, company, or tenant.
Tasks let you track follow-ups and internal action items directly in Plain, without switching to an external tool. Attach them to threads, companies, or tenants to keep context in one place.
Tasks are available in full on all plans. You can also disable Tasks in your [workspace settings](http://app.plain.com/~/settings/tasks) if you prefer not to use them.
## Example use cases
* **Feature requests to investigate**: A customer asks about SSO for their enterprise plan. Create a task to research and follow up, linking it to multiple related threads.
* **Bug investigations**: A customer reports webhook delivery failures. Create a task assigned to the right team member, linked to the company.
* **Follow-up reminders**: A customer's CSV export is timing out for large datasets. Create a task to investigate and link it to the original thread.
## Creating tasks
You can create tasks in several ways:
### From anywhere in Plain
Press t to create a task from anywhere. If you're viewing a thread, company, or tenant page, the task will automatically pre-fill relevant context (linking the current thread or setting the company/tenant).
### From the tasks page
Go to the **Tasks** page and click **Create task** or press t.
### From a thread
In the thread sidebar, find the **Tasks** section and click the + button. You can either link an existing task from the picker or create a new task.
### Via Sidekick
You can also ask Sidekick to create tasks for you based on the context of the thread you're viewing. Tasks you create via Sidekick will be auto-linked to the current thread and assigned to yourself.
### Raised by Ari
When Ari can't answer a customer from your knowledge sources, Plain records a [knowledge gap](/docs/product/agents/knowledge-gaps) and creates one task for it, titled **Knowledge gap: …** and unassigned. Every thread with the same question is linked to that task. The task sidebar shows the gap under **Knowledge gap**, and **Handle with Sidekick** drafts the missing article for you. Mark the task done once the knowledge is published.
## Viewing tasks
### Tasks page
The main **Tasks** page shows all tasks in your workspace, organized into three tabs:
* **My tasks**: Tasks assigned to you that haven't been completed
* **All tasks**: All Todo tasks across the workspace
* **Done**: Completed tasks
Click any task to open its details in a sidebar.
### Company and tenant pages
Each company and tenant page includes a **Tasks** tab. This tab shows:
* Tasks explicitly linked to that company or tenant
* Tasks linked to that company or tenant via a linked thread (even if the task itself isn't explicitly linked to the company/tenant)
For example, if you create a task and link it to a thread from Acme Corp, that task will appear on Acme Corp's Tasks tab, even if you didn't directly assign the task to that company.
### Thread sidebar
The thread sidebar shows all tasks linked to the current thread under the **Tasks** section. You can mark tasks as done or change their assignee directly from this list.
## Deleting tasks
Open the task details sidebar and use the overflow menu (⋯) to delete a task. Deleted tasks are removed from all views.
**Deleting a task is final**. There is no way to recover deleted tasks.
# Team reporting
Source: https://www.plain.com/docs/product/platform/team-reporting
How to use team reporting to monitor performance across your team and individual team members.
Team reporting lets workspace owners and admins see how the team is performing overall, and drill into individual team member metrics.
## How to access
Go to Insights in the left-hand sidebar and click Team under Reporting.
Only workspace owners and admins can access team reporting.
## What you'll see
The team overview page shows volume trends, response times, and resolution times at a glance.
From there, open any team member's page to see:
* Threads handled and moved to Done
* First and next response times
* Resolution times
* Customer waiting times
* SLA compliance
* Messages sent
* Thread assignments
Each team member page also has a heatmap showing when they send messages, by day of week and hour (UTC).
## Comparing team members
On any team member's page, click Compare to bring one or more users into the same view and see their metrics side by side.
## Filtering
Team reports can be filtered by any custom fields configured on threads or tenants. Use the filter controls at the top of the page to narrow results.
Team reporting is available on Horizon and Frontier tiers. Contact [sales@plain.com](mailto:sales@plain.com) to get access.
# Tenants
Source: https://www.plain.com/docs/product/platform/tenants
Organize customers to mirror how your product is structured, and bring external data onto them with tenant fields.
A tenant mirrors how your own product groups its users, so a thread can be scoped to a whole account rather than one person.
In Plain, in addition to [**company support**](/docs/product/platform/companies), you can also organize your customers to mirror how your product is structured.
For example, if in your product all of your customers belong to a team/org/account/workspace then you would create a tenant per team/org/account/workspace.
Tenants are primarily useful if you are a larger support team building a headless support portal or if you have support SLAs tied to different tiers of customers.
Tenants can be [**created via the API**](/docs/graphql/tenants) or created automatically based on a customer's email domain. When a customer contacts you, Plain matches their email domain to an existing tenant or creates a new one with no manual API call needed.
## Viewing your tenants in Plain
Each Tenant has a Tenant page in Plain where you can add or see the following information:
* Tier
* External ID
* URL
* Support Volume (# of threads within the past 30 days)
* Individual customers
* Open threads
You can also favorite a Tenant so that it always appears in your lefthand sidebar.
## Tenant fields
Once you have tenants set up, Tenant Fields let you add more context to them.
Tenant Fields let you bring customer data from your external systems, like your CRM or your own database, directly into Plain. A Tenant represents a customer company or account, and Tenant Fields give you the context you already rely on elsewhere, right where you are working.
### Why it's useful
With Tenant Fields, every conversation has the right context: see customer details at a glance without switching tabs, filter queues and reports by data from external systems, and automate workflows based on customer data.
### How it works
Tenant Fields are a one way sync from your external systems into Plain. They are read only in the UI. You can toggle the visibility of a field by going to **Settings → Tenant Fields.**
### Supported field types
* **Boolean**: true or false
* **Text**: free form text
* **Number**: integers or decimals
* **Date and time**: a specific date and time
* **Dropdown (single select)**: choose one option from a predefined list
* **Multi select**: choose multiple options from a predefined list
# Themes
Source: https://www.plain.com/docs/product/platform/themes
How Themes uses AI to surface the topics driving your support volume.
Themes uses AI to group your recent support threads into topic clusters, so you can see what customers are asking about most without reading through every conversation.
## How it works
Every day at 5am UTC, Plain generates a new batch of themes based on the previous 7 days of conversations. Each theme is a cluster of related threads. Only the 5,000 most recent threads in that window are clustered.
You can sort by largest, smallest, most positive, or most negative, depending on what you want to look at first.
Clicking into a theme shows all the related threads, the customers and tiers involved, and lets you jump directly to individual conversations.
## Themes are snapshots
The threads in a theme are fixed at generation time and won't update afterward. If you're looking at an older theme, the most recent ones will give you a more current picture of what's coming in.
## How to turn themes on
1. Enable Plain AI in your workspace settings.
2. Themes will start appearing in Insights > Themes on the next working day.
3. Browse, sort, and click into any theme to see the related threads.
Themes are available on all plans once Plain AI is enabled.
# Assignment
Source: https://www.plain.com/docs/product/platform/threads/assignment
How threads get an owner in Plain - manually, automatically as you type, or load-balanced across a team.
Assigning threads in Plain keeps support **organized, accountable, and responsive**.
## How assignment works
When you begin typing in a thread, Plain automatically assigns it to you. You can also assign threads manually: press `A` while viewing a thread, use ⌘ + K, or select an assignee from the top-right of the thread view.
### Sticky assignment
By default, once you're assigned to a thread, Plain keeps you assigned. You can change this in **Settings → Workflow**.
### Online, offline, and away mode
Users can be in one of three statuses: **Online**, **Offline**, or **Away**. Configure this from your profile.
### Multiple assignees
You can add **multiple assignees** to a thread: one lead, plus optional co-assignees.
## Auto-assignment
You can enable auto-assignment in your team's settings to automatically distribute incoming threads across your team members so no one gets overloaded.
### How it works
When a thread carries the team label, auto-assignment collects every member of that team, filters out users who are Away or at capacity, and assigns the thread to the eligible user with the lowest current load.
### Set it up
1. **Create (or confirm) your team** - Go to **Settings → Labels** and add your team and its members.
2. **Enable Auto-assignment** - Toggle **Auto-assign threads for this team.**
3. Set a **max capacity**. This is the maximum number of Todo threads an agent can hold before they're skipped in the rotation.
4. **Save -** New matching threads will now auto-assign.
### How the algorithm works
When a thread needs to be assigned, Plain picks the best available agent by checking:
* **Availability**: agents with status Away, Break, or Offline are skipped
* **Capacity**: agents who've hit their max capacity are skipped
* **Least loaded**: among eligible agents, the one with the fewest assigned threads gets picked
### When assignments trigger
Auto-assignment fires automatically when:
* A new thread is created with a team label
* A team label is added to an existing thread
* A thread is marked Done or Snoozed (freeing up capacity → backfills other threads)
* A thread is reopened (goes back to Todo unassigned)
* An agent comes back online (their teams get backfilled)
### Things to know
* If no agents have capacity, the thread stays unassigned until someone frees up
* Auto-assignment only assigns threads with **Todo** status that have no existing assignee
When something needs escalating, [escalation paths](/docs/product/platform/escalation-paths) give your team a predefined route to the next level.
# Locking threads
Source: https://www.plain.com/docs/product/platform/threads/locking-threads
Permanently close a thread so nobody can reply to it.
Locking a thread permanently closes it. Once locked, no one on your team can reply to it.
If a customer replies to a locked thread, Plain automatically creates a new thread so the conversation can continue.
## Lock a thread
There are three ways to lock a thread:
* Open the thread, click the overflow menu in the thread header, and select **Lock thread**.
* Right-click the thread in the thread list and select **Lock thread**.
* Open the command palette with ⌘ + K, search for **Lock thread**, and select it.
## What happens when you lock a thread
* The thread status changes to **Done**.
* Replies are disabled for your team.
* The lock is permanent. Threads cannot be unlocked.
## Automate thread locking with workflows
You can lock threads automatically using workflows. Two options are available:
* **Condition:** Thread is locked
* **Action:** Lock thread
# Merging threads
Source: https://www.plain.com/docs/product/platform/threads/merging-threads
Bring duplicate threads together so your team tracks one conversation.
When a customer contacts you about the same issue across multiple threads, merging lets your team track and respond to everything from one place.
## How it works
When you merge two threads, you pick a **parent** thread and a **child** thread. The parent stays active. The child is set to **Ignored** status. Any new messages sent to the child thread automatically appear in the parent's timeline.
## Merging two threads
There are three ways to start a merge:
* Open the thread, click the overflow menu in the thread header, and select **Merge thread**.
* Open the command palette with ⌘ + K and search for **Merge thread**.
* Use the thread links panel on the thread page.
## Unmerging threads
1. Open either the parent or child thread.
2. Click the overflow menu in the thread header.
3. Select **Unmerge thread**.
## What happens after merging
| Area | Behavior |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Parent thread timeline** | New messages from the child thread appear as grouped "New activity in merged thread" entries, showing the child thread's reference number and title. |
| **Child thread status** | Automatically moves to "ignored." |
| **Child thread view** | Displays a "Thread is merged" card indicating the parent thread, with a link to navigate there. |
| **Replying** | A reply composer appears inline on the parent thread for each batch of merged messages, allowing you to respond to the child thread's customer without leaving the parent. |
| **Thread links** | Both threads show the merge relationship in their thread links section, with distinct icons indicating which is the parent and which is the child. |
## Supported channels
Merged thread messages work across all communication channels, including email, chat, Slack, Microsoft Teams, and Discord. When replying from the parent thread, the response is sent through the child thread's original channel.
## Things to know
* You can only merge threads created by the same customer/tenant/company
* A thread can only be merged as a child into one parent thread at a time.
* You cannot merge a thread into itself.
* Merging is available for internal threads only - it is not available for external issue tracker links (such as Linear or Jira).
# Priorities
Source: https://www.plain.com/docs/product/platform/threads/priorities
Order your queue by urgency, and set SLA targets per priority.
Priorities help your team focus on what matters most by organizing support threads by urgency. This is especially helpful when volume is high or service levels vary by customer tier.
## Priority levels
| Level | When to use |
| ------ | ------------------------------------------ |
| Urgent | Downtime, security threats, or data loss |
| High | Blocked workflows or time-sensitive issues |
| Medium | Standard requests |
| Low | Routine questions and follow-ups |
## Set priority
There are three ways to set a priority on a thread:
* Press `P` while viewing the thread.
* Open the command palette with ⌘ + K and search for **Set priority**.
* Select a priority from the dropdown in the thread header.
## Automate with workflows
Priorities can be set automatically using Workflows. For example, automatically marking a thread as **Urgent** when it contains specific keywords or comes from a high-tier customer.
# Similar threads
Source: https://www.plain.com/docs/product/platform/threads/similar-threads
Surface earlier threads about the same problem while you are working on one.
**Similar threads** surface context and previous solutions while you work a thread. They use AI to detect other threads that are closely related to the one you are viewing, so you cany gather extra information without searching manually.
When enabled, you'll see similar threads in the **thread sidebar**.
## How similar threads work
* **AI matching** - Plain compares the current thread's content with your historical threads to find those that share a similar theme.
* **Context at a glance** - Results are shown in the sidebar, alongside the thread you're working on.
* **Direct navigation** - Click a thread to open it in a new tab.
## Turning on similar threads
Similar threads are off by default and must be enabled in your workspace settings.
1. Go to **Settings** → **Plain AI**.
2. Toggle **Similar threads** to **On**.
3. Open any thread in Plain to see similar threads appear in the sidebar.
# Statuses
Source: https://www.plain.com/docs/product/platform/threads/statuses
Every thread status in Plain - Todo, Snoozed and Done - what each means and when to use it.
Every thread has a **status** to show you exactly where it is in your workflow at a glance. New threads are created with *Needs first response*; once you reply, they move to *Needs next response*. You can snooze a thread while waiting on the customer, move it to *Investigating* for deeper internal work, and mark it *Done* when resolved. Statuses drive your **SLA** timers. Plain tracks first and next response time commitments per thread and you can configure alerts for your team before a breach occurs.
[Read about SLAs →](/docs/product/platform/slas)
Every thread in Plain has a status that reflects where it sits in your support workflow. Statuses keep your queue organized and drive SLA timers so your team always knows what needs attention and what can wait.
There are three status groups: **Todo**, **Snoozed**, and **Done**.
What each status means, and when to use it.
## Todo
Todo threads are active and require action from your team.
### Needs first response
The thread has been created, but the customer hasn't received any reply yet. This is where **First Response Time SLAs** apply.
### Needs next response
The customer has replied, and it's your turn to respond. This is where **Next Response Time SLAs** apply.
### Investigating
Your team is working on the request internally. Use this status when a thread needs deeper technical investigation before you can reply.
### Close the loop
The thread is linked to a known issue that has now been resolved. Plain moves threads to this status automatically so you can follow up with the customer.
## Snoozed
Snoozed threads are temporarily paused and won't surface in your active queue.
### Waiting for customer
You've responded and are waiting on the customer to reply.
By default, Plain moves threads to this status after your reply. You can change this in **Settings → Workflow.**
### Paused for later
You've intentionally paused the thread.
## Done
Done threads are closed and no longer require action.
### Done
The issue is resolved, and you've marked the thread as complete. This status feeds into reporting, helps clear your queue, and provides clarity across your team. If the customer replies again, the thread will reopen automatically.
### Ignored
The thread needs no action from your team and has been explicitly ignored. Ignored threads are silenced. They won't trigger notifications, affect metrics, or move statuses. This is ideal for keeping your support workflow clean in multi-use Slack channels or integrations.
# Thread fields
Source: https://www.plain.com/docs/product/platform/threads/thread-fields
Store structured data on a thread, such as account IDs or deal values, and filter by it.
Thread fields let you store structured data directly on a thread, things like account IDs, deal values, renewal dates, or any custom information your team needs to track.
Once set, thread fields can be used to filter and sort your queue, trigger workflow rules, and keep agents consistent through required and read-only fields.
Configure thread fields in **Settings → Thread fields**.
## Field types
| Type | Description |
| -------- | ----------------------------------------------- |
| Boolean | A true or false toggle |
| Text | Free-form text input |
| Dropdown | Single select from a list of options you define |
| Number | A numeric value |
| Currency | A monetary value |
| Date | A date and time |
Number, Currency, and Date fields can be used as queue filters and for sorting.
## Additional configuration
* **Read-only fields:** Mark any field as read-only so agents can see the value but cannot edit it.
* **Conditional fields:** Fields can be nested so that one field only appears based on the value of another. Use this to build dynamic forms that adapt to what agents enter.
* **Label-based visibility:** A field can be configured to only appear when a thread has a specific label applied. Set this using the **Labels** option in the field settings. This is only available for top-level fields. It cannot be combined with the nested conditional field option.
* **Required fields:** Mark a field as required to ensure agents fill it in before marking a thread as Done.
* **AI auto-fill:** Plain can automatically fill thread fields using AI. To enable it, open the settings for a field and turn on **Autofill via AI**.
All field types work as conditions in the workflow builder, letting you route, prioritize, and automate based on any thread field value.
Thread fields can be created, updated, and read programmatically using [Plain's GraphQL API](/docs/graphql/threads/thread-fields).
## Agent context
The `"Include in Agent context"` toggle lets [Ari](/docs/product/agents/ari) use this field as additional context when handling a thread. It is off by default and set per field.
Fields can hold any data, including PII, so only enable this toggle for values you're comfortable sharing with Ari.
# What is a thread?
Source: https://www.plain.com/docs/product/platform/threads/what-is-a-thread
Every customer conversation in Plain lives in a thread. An orientation to the tools that move them through your workflow.
In Plain, every customer conversation lives in a **thread**. The tools in this section determine what happens to a thread: how it moves through your workflow, who is responsible for it, how urgently it needs attention, and how your team tracks it from arrival to resolution.
## Structuring your queue
Plain gives you several tools to help bring order to chaos.
* **Priorities**: (Urgent, High, Medium, Low) flag which threads need attention first.
* **Tiers** let you segment your customers based on your internal (or external) criteria. Such as pricing plans like Enterprise, Pro or Free. You can then attach different SLA targets to each.
* **Labels** can categorize threads by type and enable team based assignment and routing.
* **Thread fields** go further still, letting you store structured custom data like renewal dates, deal values and account IDs with the thread. That your team can then use to filter, sort, and build workflows on.
[Priorities →](/docs/product/platform/threads/priorities) [Tiers →](/docs/product/platform/tiers) [Labels →](/docs/product/platform/labels) [Thread fields →](/docs/product/platform/threads/thread-fields)
## Keeping work moving
**Tasks** let you track internal follow-ups without leaving Plain. Press T to create one on any thread. **Auto-responses** keep customers informed the moment a thread arrives, configurable by channel, business hours, or label. When two threads cover the same issue, **merging** them into a single parent avoids duplicated effort. And when a conversation is genuinely closed, **locking** it prevents further replies while automatically creating a fresh thread if the customer writes back.
[Tasks →](/docs/product/platform/tasks) [Auto-responses →](/docs/product/platform/auto-responses) [Merging threads →](/docs/product/platform/threads/merging-threads) [Locking threads →](/docs/product/platform/threads/locking-threads)
# Tiers
Source: https://www.plain.com/docs/product/platform/tiers
Segment customers into tiers, manually or automatically from a tenant field, and route their threads by tier.
Tiers let you segment your customers, like **Enterprise**, **Pro**, or **Free**, so your team can deliver differentiated support to each group.
Manage your tiers in **Settings → Tiers**.
Tiers are available on the **Horizon pricing plan** and above.
## Assign a tier
Tiers can be assigned to **Companies** or **Tenants**. Once assigned, the tier shows on every thread from that company or tenant, and workflows can be configured to route threads by tier.
To assign a tier:
Open the page for the company or tenant you want to put in a tier.
Click the **Tier** field in the sidebar and pick a tier from the list.
## Tier SLAs
A tier can carry SLAs, so every thread from a company or tenant in the tier is measured against the same response time commitments. SLA policies supersede tier SLAs, but existing ones keep working. See [Tier SLAs](/docs/product/platform/slas/tier-slas) for how they work and how to manage them.
## Tier mapping
Tier mapping lets Plain automatically assign tenants to tiers based on the value of a tenant field. Rather than assigning tiers manually, you define which field drives tier assignment and Plain keeps every tenant's tier in sync as their field value changes.
### How it works
When you map a tenant field to tiers, Plain creates a tier for each option in that field. Every tenant is then assigned the tier that matches their current field value. If a tenant has no value for that field, they receive no tier assignment.
Machine tiers behave like any other tier: they can have a default thread priority and can be used to filter your queue.
### Prerequisites
Only **single-select (dropdown) tenant fields** are eligible for tier mapping. Free-form text, number, boolean, and multi-select fields are not supported, because Plain needs a fixed, bounded set of values to create tiers from.
### Setting it up
Go to [**Settings → Tenant fields**](https://app.plain.com/~/settings/tenant-fields/) and find the **Tier mapping** card.
Select the field you want to map from the dropdown. Only eligible fields appear.
Click **Save**. Plain starts mapping tiers from that field.
Only one field can be mapped to tiers at a time.
#### What happens when you save
* Plain creates a new tier per option in the field, named after each option value
* All existing tenants with that field set are automatically assigned to their matching tier
* Tenants added or updated after setup are assigned in real time as their field value changes
### Keeping tiers in sync
Plain automatically keeps machine tiers in sync when the field schema changes:
* **New option added**: a new machine tier is created; any tenants with that value are assigned to it
* **Option removed**: the corresponding machine tier is deleted and affected tenants lose that tier assignment
* **Mapping removed or changed**: all machine tiers from the previous mapping are deleted and tenants are unassigned.
Changing the mapped field first removes the old mapping (including its tiers), then sets up the new one.
# Top issues
Source: https://www.plain.com/docs/product/platform/top-issues
How Top Issues surfaces which product backlog items are generating the most customer threads.
Top Issues shows which Linear and Jira issues are generating the most customer threads. It's useful when you want data on what to prioritize, rather than going by gut feel.
## How to access
Go to Insights in the left-hand sidebar and click Top issues.
## What you'll see
Issues from your tracker (Linear or Jira) are ranked by the number of linked support threads. For each issue you can see:
* The number of linked threads
* A breakdown by tier, so you can weigh impact by customer segment
* The companies asking about it
## Linking threads to issues
From any thread, open the thread detail panel and link it to a Linear or Jira issue. That thread then counts toward the issue's total in Top Issues.
## When to use it
Top Issues is useful for sprint planning, escalation decisions, and making the case to the product team for which fixes matter most.
# Translation
Source: https://www.plain.com/docs/product/platform/translation
Detect non-English messages in the timeline and translate them automatically.
Message translation automatically detects non-English messages in the timeline and translates them to English, without leaving Plain or sending content to any external service.
**Where to enable it:**
Settings → Preferences → Translation → toggle on *Enable message translation*
* When a message in a supported language is detected, a *Translate* button appears inline
* Click it to see the English translation alongside the original.
* Click *Hide translation* to go back to the original.
**Supported languages:**
Arabic, Chinese, Dutch, Finnish, French, German, Hindi, Italian, Japanese, Portuguese, Russian, Spanish, Swedish, and Vietnamese.
# Favorites
Source: https://www.plain.com/docs/product/platform/views/favorites
Pin any view, customer, or page to the top of your own sidebar.
Favorites pin a view, customer, or page to the top of your sidebar. They are personal to you, so what you pin does not change what anyone else sees.
You can favorite:
* **Saved views**, including ones other people created
* Individual **company**, **tenant**, **label**, **group**, or **tier** pages
New saved views are favorited automatically. Click the star icon to add or remove a favorite.
# My threads
Source: https://www.plain.com/docs/product/platform/views/my-threads
Your personal queue: everything assigned to you, plus any filters you add on top.
The **My threads** view shows all threads assigned to you with a **Todo** status. You can extend it with additional filters to surface any other threads relevant to you, not only your assigned ones.
## Set up your personal queue
1. Go to **Settings → Members**
2. Click your name
3. Configure your queue filters
Your queue always includes **Assigned to me**, so you never miss a thread assigned directly to you. You can add as many additional filters as you need on top of that.
Only you can edit your own queue filters. Administrators and owners can edit everyone's.
## Queue filters
A personal queue consists of any number of thread filters. The personal queue will always contain "Assigned to me" so that you don't miss any threads that are assigned to you, regardless of if they match any of the following filters or not. The remaining filters are then joined together to generate your resulting list.
For example, if your team is organized by customer or by tier, you can narrow your personal queue to "Threads I'm assigned to and threads from Acme LLC", or to "Threads I'm assigned to and Premium Support Customers".
# Saved views
Source: https://www.plain.com/docs/product/platform/views/saved-views
Shared workspace queues, with filters, sorting, grouping and board or table layouts.
A saved view is a shared queue: a set of filters, sorting, and grouping that everyone in the workspace can open.
**Saved Views** let you build customized support queue views shared across your **entire workspace**: ideal for teams to track what matters most.
**Use cases:**
* Triage by Tier, or channel
* Dashboards for Sales or Success teams
* Monitor breaching SLAs
* Shared queue ownership
**Display options:**
* **Board View**: great for visual triage and drag-and-drop workflows
* **Table View**: ideal for scanning high volumes of threads
**To create a Saved View:**
1. Go to **Views** in the sidebar
2. Click **Create view**
3. Set your filters, sort/group preferences, layout, and fields
4. Save and favorite the view
## Filters
Saved views support nested filter logic, so a view can be as broad or as narrow as you need.
**Available filters:**
* Assignee, Participant, Label, Status
* Company, Tenant, Tier, Priority
* Group, Channel, Thread field
* Created at, CSAT sentiment, tenant field
## Filter operators
Each filter supports:
* **is**: include threads that match the value
* **is not**: exclude threads that match the value
Example:
* Label **is not** `spam`
## Combining filters
You can choose how filter blocks are combined:
* **Match all**: all filter blocks must match (combined with AND)
* **Match any**: any filter block can match (combined with OR)
This lets you control how strict or broad a saved view should be.
## Repeating filter blocks
You can add the same filter block multiple times. This is useful when you want to apply different conditions to the same field.
Example logic:
* Threads with label `security` **AND** threads with either `bug` or `feature request`
This would be represented as:
* One label block containing `bug` and `feature request`
* A second label block containing `security`
* The blocks combined with **AND**
**Sorting options:**
* Status changed (oldest/newest)
* Closest to SLA breach
* Priority or most recent message
**Grouping options:**
* Priority, Status, Company, Label
* Tier, Channel, Assignee, Group
Saved Views are shared across your workspace. They are not currently personal or private.
# Working hours
Source: https://www.plain.com/docs/product/platform/working-hours
Set the hours you work each day and let Plain switch your status automatically.
**Working Hours** allow you to set your working hours for each day and automate the online and offline transition. You will automatically switch to online at the beginning of each working day and revert to offline at the end of the day. To configure this, click on your avatar in the bottom left corner, then go to **Preferences > Working Hours**.
If you're going on holiday or stepping away from support, you can activate **Away Mode** by clicking on your profile picture at the bottom of the sidebar. While it's on, any thread that returns to **Todo** won't be reassigned to you, even if you were the previous owner.
This prevents support from bottlenecking while you're offline and keeps the team's workflow running smoothly.
# Security at Plain
Source: https://www.plain.com/docs/product/security
How Plain stores and protects your data, which certifications it holds, and who to contact about security.
This section collects the security commitments Plain makes about your data: where it is stored, who can reach it, what happens when AI features are on, and how to report a vulnerability. If your team needs something that isn't here, email [security@plain.com](mailto:security@plain.com).
## Certifications and compliance
Plain is SOC 2 Type II certified. Independent audits verify how Plain manages security, availability, and confidentiality.
Plain's systems are built in line with the General Data Protection Regulation (GDPR) and the UK Data Protection Act. A [Data Processing Agreement (DPA)](https://www.plain.com/legal/dpa) is available for companies that need one.
To request the SOC 2 report or other security documentation, go to [trust.plain.com](https://trust.plain.com). Current system health is at [status.plain.com](https://status.plain.com).
## How data is stored
All data is encrypted in transit and at rest. Plain runs on Amazon Web Services in the `eu-west-2` (London) region. Backups run on a regular schedule and are encrypted at rest.
Engineers, systems, and roles get the least privilege needed to do their work. Every change to infrastructure, permissions, and code goes through code review. Administrator privileges are used only during serious incidents; routine maintenance runs through roles with fine-grained permissions.
## How the API is protected
Every API request requires authentication. Requests Plain makes to your endpoints can be verified two ways:
* **[Request signing](/docs/request-signing)**: outbound requests to your webhook targets and customer card endpoints carry an HMAC-SHA256 signature made with a shared secret.
* **[mTLS](/docs/mtls)**: outbound requests present a client certificate you can verify against Plain's CA certificate.
## Third-party vendors
Plain reviews every third-party vendor before using it, and reviews what data each vendor can reach on an ongoing basis. The full list of sub-processors is in the [DPA](https://www.plain.com/legal/dpa).
The AI providers Plain uses are covered separately in [AI and your data](/docs/product/security/ai-and-your-data), including Plain's zero data retention commitments.
## Your privacy rights
The [Privacy Policy](https://www.plain.com/legal/privacy-policy) and the [DPA](https://www.plain.com/legal/dpa) set out what data Plain collects and how it is used. You can request, access, or delete your data at any time, and Plain never shares your data without consent.
## When workspaces are deleted
A workspace with no activity for 6 months and no active billing subscription is deleted automatically. Workspace owners get an email first, and deletion goes ahead 14 days later unless an owner replies to cancel it.
To request a manual deletion, email [support@plain.com](mailto:support@plain.com). Plain support will ask you to confirm your identity before proceeding.
Workspace deletion is final. Deleted workspaces and their data cannot be recovered.
## Reporting a security issue
Email [security@plain.com](mailto:security@plain.com). Keep the report concise, include steps to reproduce, and add a proof of concept if you can. Valid reports are acknowledged within 48 hours.
If you are testing Plain as a security researcher, read the [responsible disclosure policy](/docs/product/security/responsible-disclosure) first. It sets out the rules research must stay inside, what Plain commits to, and when a bounty is paid.
# AI and your data
Source: https://www.plain.com/docs/product/security/ai-and-your-data
Which AI providers Plain sends data to, what they can do with it, and how to turn AI features off.
When Plain's AI features are on, thread content is sent to OpenAI and Anthropic to generate a response. Neither provider uses your data to train its models, and you can turn every AI feature off from your workspace settings.
## Which providers Plain uses
Plain uses OpenAI and Anthropic to power its AI features, which include [Ari](/docs/product/agents/ari), [Sidekick](/docs/product/agents/sidekick), thread summaries, [suggested responses](/docs/product/agents/ari/suggested-responses), [AI triage](/docs/product/platform/ai-triage), and [AI slash commands](/docs/product/platform/ai-slash-commands).
Both providers are listed as sub-processors in Plain's [Data Processing Agreement (DPA)](https://www.plain.com/legal/dpa), which sets out the full terms Plain processes data under.
## Zero data retention
Plain has opted into [OpenAI's zero data retention program](https://developers.openai.com/api/docs/guides/your-data#zero_data_retention). Data sent to OpenAI is not stored once the API call completes, and OpenAI does not use it to train its models.
Anthropic does not use data sent through its API to train its models either.
## Turning AI features off
AI features are on by default. A workspace admin can turn them off at any time:
Go to **Settings → Plain AI → Configuration**.
Each feature can be disabled on its own, or you can disable AI entirely.
Changes take effect immediately. Once a feature is disabled, no thread content from it reaches any AI provider.
# Responsible disclosure policy
Source: https://www.plain.com/docs/product/security/responsible-disclosure
How to report a security vulnerability to Plain, what researchers must and must not do, and what Plain commits to.
If you think you have found a security issue in Plain, email [security@plain.com](mailto:security@plain.com). This page sets out how to report it, the rules research must stay inside, and what Plain commits to in return.
## Reporting an issue
Keep your report concise, add steps to reproduce, and include a proof of concept if possible.
We acknowledge valid reports within 48 hours of receipt. Avoid following up more than once every 72 hours, so the team can focus on fixing the issue.
## Bounties
We pay a bounty to security researchers who have followed this policy and found a confirmed high-severity vulnerability. Amounts are decided case by case.
## What you must not do
* Break any applicable law or regulation
* Access unnecessary, excessive, or significant amounts of data
* Modify data in Plain systems or services
* Use high-intensity invasive or destructive scanning tools to find vulnerabilities
* Attempt or report any form of denial of service, for example overwhelming a service with a high volume of requests
* Disrupt Plain services or systems
* Submit reports detailing non-exploitable vulnerabilities, or reports arguing that a service does not fully align with "best practice", for example missing security headers
* Submit reports detailing TLS configuration weaknesses, for example "weak" cipher suite support or the presence of TLS 1.0 support
* Communicate any vulnerability or associated detail by any means other than those described in this policy
* Social engineer, phish, or physically attack Plain staff or infrastructure
* Demand financial compensation in order to disclose a vulnerability, **or threaten public disclosure of a vulnerability unless payment is made**
## What you must do
* Comply with data protection rules, and do not violate the privacy of any data Plain holds. Do not share or redistribute data retrieved from Plain systems or services, and do not leave it unsecured
* Securely delete all data retrieved during your research as soon as it is no longer needed, or within 1 month of the vulnerability being resolved, whichever comes first, or as otherwise required by data protection law
## What Plain commits to
If you follow this policy when reporting an issue, we commit to:
* Not pursuing or supporting any legal action related to your research
* Working with you to understand and resolve the issue, including an initial confirmation of your report within 48 hours of submission
# Slack data retention
Source: https://www.plain.com/docs/product/security/slack-data-retention
What the Plain Slack app accesses, what it stores, and how long it keeps that data.
This page is for IT and security teams reviewing Plain's [Slack integration](/docs/product/channels/slack). It covers what the app can reach, what Plain stores, and when that data is deleted.
## How the integration works
During setup, an admin installs the Plain app into your Slack workspace and authorizes the scopes it requests. Plain reads only from channels where the bot has been invited. No data is collected from channels the app has not been added to.
## What Plain stores
For each channel where the bot has been invited, Plain stores:
* Message content, including text, attachments, and files
* Message metadata, including timestamp, sender, and thread structure
* Channel membership, meaning which users are in connected channels
* Channel metadata, including name, ID, and type
## What Plain does not store
Plain never stores:
* Data from any channel the Plain bot has not been invited to
* Slack workspace-level administrative data
* Direct messages
## Retention rules
Two rules govern how long Slack data stays in Plain:
* **Deleted messages**: if a message is deleted in Slack, Plain deletes the message content. A minimal audit trail is kept, recording that a message existed and was deleted, without the content.
* **Data Plain doesn't need**: any additional data Plain receives from Slack's APIs that isn't needed to operate is deleted automatically after 14 days.
## AI features
AI features that run on Slack threads, such as suggested responses and thread summaries, send thread content to Plain's AI providers. [AI and your data](/docs/product/security/ai-and-your-data) covers which providers those are, Plain's zero data retention commitments, and how a workspace admin turns the features off.
# What is Plain?
Source: https://www.plain.com/docs/product/what-is-plain
Plain is a support platform for B2B teams, built API-first so you can change how it works rather than work around it.
Plain is a support platform for B2B teams. Customer conversations arrive from email, Slack, Microsoft Teams, chat, Discord, or your own product, and become [threads](/docs/product/platform/threads/what-is-a-thread) in a single queue that your team and your AI agents both work from.
It is built API-first. Everything the app can do, the [GraphQL API](/docs/graphql/introduction) can do, so you can automate a workflow, build a support portal inside your own product, or run your own agent against the same data your team uses.
## Our mission
Support is becoming more technical. AI is enabling companies to build anything themselves, and the speed of software change is accelerating faster than the tools teams use to support it. We believe the companies that win are the ones that treat support as infrastructure they build on top of, not a tool whose roadmap they don't control.
Plain exists so that every B2B team can build support their way.
## What Plain does
* **Channels.** Email, [Slack](/docs/product/channels/slack), [Microsoft Teams](/docs/product/channels/microsoft-teams), [chat](/docs/product/channels/chat), [Discord](/docs/product/channels/discord), and [contact forms](/docs/product/channels/contact-forms) all land in the same queue. You can also bring a channel Plain does not support yet with [custom channels](/docs/custom-channels).
* **AI agents.** [Ari](/docs/product/agents/ari) replies to customers. [Sidekick](/docs/product/agents/sidekick) helps your team from inside Plain or Slack. You can also [bring your own agent](/docs/product/agents/bring-your-own-agent), which runs with the same identity and permissions as anyone else.
* **The queue.** [Tiers](/docs/product/platform/tiers), [SLA policies](/docs/product/platform/slas), [labels](/docs/product/platform/labels), [priorities](/docs/product/platform/threads/priorities), [assignment](/docs/product/platform/threads/assignment), [business hours](/docs/product/platform/business-hours), and [escalation paths](/docs/product/platform/escalation-paths) decide what your team sees first.
* **Automation.** [Workflows](/docs/product/workflows) run on triggers and conditions, so triage, routing, and follow-ups happen without anyone watching the queue.
* **Customer context.** [Companies](/docs/product/platform/companies), [tenants](/docs/product/platform/tenants), and [customer cards](/docs/product/platform/customer-cards) pull data from your own systems into the thread your team is reading.
* **Self-serve.** A [Help Center](/docs/product/help-center) with a knowledge base, Ask AI, and a customer inbox.
## Who Plain is for
Plain suits B2B teams whose support needs outgrow a fixed feature set:
* **Technical teams** who want to build on an API rather than file feature requests.
* **Fast-growing companies** whose volume and process change faster than a vendor ships.
* **Teams adopting AI** who want to choose and swap agents rather than take whichever one is bundled.
* **Support teams working with engineering**, where threads need to link to issues, incidents, and code.
## Where to start
* Set up a channel, starting with [email](/docs/product/channels/email) or [Slack](/docs/product/channels/slack).
* Read how [threads](/docs/product/platform/threads/what-is-a-thread) work, since everything else attaches to them.
* If you are building against the API, start with the [data model](/docs/graphql/introduction).
# Workflows
Source: https://www.plain.com/docs/product/workflows
Build multi-step automations with branching logic, wait states, and conditional actions.
Workflows let you automate multi-step support processes: route threads, follow up after delays, branch based on conditions, and chain actions together without code. Use them for anything that needs more than a single if-then rule.
## What you can do
* Run multi-step automations when specific thread events happen
* Take different paths based on thread state, labels, tier, channel, or other properties
* Pause for minutes, hours, or days before continuing
* Cancel a pause early if something changes (e.g. the customer replies)
* Trigger workflows manually on a thread or automatically from events
* Set the order several workflows run in when they match the same event
## How workflows work
Every workflow starts with a **trigger**. You can trigger a workflow manually on a specific thread, or set it to run automatically when something happens (like a thread being created or a message being added).
After the trigger fires, the workflow runs through **steps**. There are three types:
* **Condition steps** check whether something is true or false and branch accordingly. For example, check if a thread has a specific label and take different paths based on the result.
* **Action steps** do something like assigning a thread, setting priority, or sending a message, then continue to the next step.
* **Wait steps** pause the workflow for a set duration. Each wait step has two outcomes: **completed** (the wait finished) or **canceled** (a cancel condition was met, e.g. the customer replied).
## Run order
One event can trigger several workflows. When it does, they run one after another, in the order they are sorted on the **Workflows** page.
The order matters when one workflow depends on what another did. A workflow that routes a thread by its label needs the workflow that applies the label to run first.
To change the order, go to **Workflows** and drag a workflow up or down the list. The new order applies to the next event. Runs already underway keep the order they started with.
Two exceptions:
* A workflow that pauses at a wait step lets the next one start. See [wait for](/docs/product/workflows/workflows-wait-for).
* Workflows started by two different events are separate runs, and neither waits for the other. See [triggers](/docs/product/workflows/workflows-triggers).
## Create a workflow
Go to **Workflows** in the sidebar, click **New workflow**, and give it a name.
Choose **Manual** or **Automatic**, then select the events that should start the workflow.
Click **+** on the canvas to add condition, action, or wait steps, then drag between them to define the flow.
Condition and wait steps have two branches, Yes/No and Completed/Canceled, which you connect to different next steps.
Click **Publish**. The workflow starts running against new threads.
## Duplicate a step
To reuse a step without rebuilding it, duplicate it. Two ways to do this:
* **Right-click** any node on the canvas and select **Duplicate**.
* **Hold Option (Mac) or Alt (Windows)** while dragging a node to place a copy at the new position. The original stays in place.
The copy keeps the step's full configuration and branch structure. Connections to other nodes are not carried over: wire the copy into your workflow after placing it.
## Limits
* Steps can only move forward. No loops
* Wait durations: 1 second to 1 year
# Detect and ignore spam with an AI prompt condition
Source: https://www.plain.com/docs/product/workflows/example-workflows-ai-filter-spam
Learn how to use the AI prompt condition in workflows to automatically identify spam threads and set them to ignored.
The AI prompt condition in workflows can act on the content of a thread, not only its metadata, and it's well-suited to detecting spam. This article explains how the condition works, how to configure it for spam detection, and where its boundaries are.
## How the AI prompt condition works
When a workflow reaches an AI prompt condition, Plain assembles a context snapshot of the thread and passes it to the model alongside your prompt. The context includes:
* The full thread message history (up to the 600 most recent messages, each up to 3,000 characters)
* Thread metadata: title, description, status, priority, and labels
* Customer details: name and email address
* Channel information (e.g. email, Slack, API)
* Assignees
* Thread fields and their configured schemas
* Attachment count
The model evaluates your prompt against this context and returns a boolean: **match** or **no match**. Your workflow branches accordingly: if the condition matches, the configured action fires.
For spam detection, you would pair this condition with the **Set Status → Ignored** action, which closes the thread with an ignored status detail.
## Setting up a spam detection workflow
The AI condition prompt is a free-text field (up to 2,500 characters). A well-written prompt is the most important part of making this reliable.
**Example prompt:**
> This thread is spam or should be silently ignored. Examples include: unsolicited sales or partnership outreach, automated phishing or credential-harvesting messages, irrelevant mass marketing unrelated to our product, test messages with no genuine support intent, or messages that appear to be sent to the wrong company entirely. If you are uncertain whether this is spam, treat it as spam and return true.
The last sentence is important: by default, the model returns **false** (no match) when uncertain. For spam detection, you want the opposite behavior. Instructing the model to err on the side of flagging uncertain cases catches more spam, at the cost of more false positives.
Your workflow structure would be:
1. **Trigger:** Thread created (or Message added, if you want to catch spam that arrives as a follow-up)
2. **Condition:** AI prompt condition with your spam criteria
3. **Action:** Set Status → Ignored
## What this approach handles well
* **Text-based outreach spam:** Sales emails, partnership pitches, automated marketing blasts: anything where the message content reveals the intent.
* **Misdirected messages:** Threads clearly sent to the wrong company or product.
* **Phishing and credential requests:** Messages asking for login details, wire transfers, or similar.
* **Pattern-based spam:** If your spam follows recognizable patterns, you can describe them explicitly in the prompt and the model will apply them consistently.
## Limitations to be aware of
### Attachment content is not analyzed
The model receives the *count* of attachments on a thread, but not the attachment content itself. If spam arrives as an image-only message, a common way to evade text-based filters, the model has almost no signal to work with. This is the most significant gap for a spam use case.
### No external reputation signals
The AI condition works exclusively with data Plain has stored about the thread and customer. There is no access to email headers, sender IP reputation, domain blocklists, or DMARC/SPF results. If you need header-level or network-level filtering, that would need to happen upstream at your email provider before the thread reaches Plain.
### Already-flagged spam customers are handled separately
If a customer has already been marked as spam in Plain, workflows do not run for their new threads. They are caught earlier in the pipeline and their threads are automatically closed as ignored. The AI workflow condition is therefore most useful for catching new spam from customers not yet identified.
### Consistency and determinism
LLM-based evaluation is not fully deterministic. The model settings are tuned to reduce variance, but edge cases, for example, an aggressive but technically legitimate sales email, may be evaluated differently across runs. Writing a precise prompt with concrete examples of what does and does not count as spam for your use case reduces this variance.
### Plain AI must be enabled
The AI prompt condition requires Plain AI to be active for your workspace. If it is disabled, all AI conditions evaluate to false and the workflow will not trigger.
## Recommended approach
For most use cases, an AI prompt workflow condition is a solid first layer of spam defence for content-identifiable spam. We recommend:
1. Start with a conservative prompt targeting obvious spam (outbound sales, phishing, clearly misdirected messages) and monitor results over the first week.
2. Review threads that are being incorrectly ignored (false positives) and refine the prompt to exclude those patterns.
3. If you have specific spam patterns unique to your product or audience, describe them explicitly. The more concrete examples you provide, the more consistent the model's behavior will be.
4. For spam that relies on attachments or header signals, consider handling that filtering upstream before it reaches Plain.
If you have questions about prompt structure or want help drafting your initial condition, [get in touch](https://plain.com): we're happy to help.
# Triage security emails with an AI prompt condition
Source: https://www.plain.com/docs/product/workflows/example-workflows-ai-triage-security
A worked workflow that uses an AI condition to separate vulnerability reports from general mail.
Route threads from your security@ email address using AI to separate vulnerability reports from general inquiries.
## Setup
1. **Trigger**: Set to **Automatic**, event: **Thread created**.
2. **Step 1 (Condition)**: **Support email equals** [security@yourcompany.com](mailto:security@yourcompany.com).
* **No**: End the workflow.
* **Yes**: Continue.
3. **Step 2 (Condition)**: **AI prompt match** with: "This is a security vulnerability report or disclosure".
* **Yes**: Continue to Step 3.
* **No**: Skip to Step 5.
4. **Step 3 (Action)**: **Set priority** to **Urgent**.
5. **Step 4 (Action)**: **Assign to user**. Select your security lead. End this branch.
6. **Step 5 (Action)**: **Apply labels**. Select **Security - General**.
7. **Step 6 (Action)**: **Assign to team**. Select your security team.
# Follow up with inactive customers
Source: https://www.plain.com/docs/product/workflows/example-workflows-inactive-customer-follow-up
A worked workflow that chases a customer who has not replied, then closes the thread.
After your team replies, wait 24 hours. If the customer hasn't responded, send a follow-up. If still no response after 48 more hours, close the thread.
## Setup
1. **Trigger**: Set to **Automatic**, event: **Message added**.
2. **Step 1 (Condition)**: Use an **AI prompt match** with: "a support agent has replied to the customer". Or use **Any user message contains** with keywords.
* **No**: End the workflow.
* **Yes**: Continue.
3. **Step 2 (Wait)**: Duration: **24 hours**. Cancel condition: **Any customer message contains** common reply words (e.g. "thanks", "yes", "no", "help").
* **Canceled**: End the workflow. The customer replied.
* **Completed**: Continue.
4. **Step 3 (Action)**: **Send message**. Enter: "Checking in on this. Are you still seeing the problem?"
5. **Step 4 (Wait)**: Duration: **48 hours**. Same cancel condition as Step 2.
* **Canceled**: End the workflow. The customer replied.
* **Completed**: Continue.
6. **Step 5 (Action)**: **Set status** to **Done** (manually set).
7. **Step 6 (Action)**: **Add note**. Enter: "Auto-closed: no customer response after follow-up".
# Route enterprise threads to account owners
Source: https://www.plain.com/docs/product/workflows/example-workflows-route-enterprise-threads-to-account-owners
A worked workflow that routes enterprise threads to the account owner.
When a thread is created, check if the customer is enterprise tier. If yes, assign it to their account owner. If no account owner is set, fall back to the enterprise support team.
## Setup
1. **Trigger**: Set to **Automatic**, event: **Thread created**.
2. **Step 1 (Condition)**: **Tier equals** Enterprise.
* **No**: End the workflow. Default routing handles it.
* **Yes**: Continue.
3. **Step 2 (Action)**: **Assign to company account owner**.
4. **Step 3 (Condition)**: **Assigned to** checks if someone is now assigned. This confirms the account owner assignment worked.
* **Yes**: End the workflow. The account owner is assigned.
* **No**: Continue.
5. **Step 4 (Action)**: **Assign to team**. Select your enterprise support team label.
6. **Step 5 (Action)**: **Apply labels**. Select **Enterprise**.
7. **Step 6 (Action)**: **Set priority** to **High**.
# Actions
Source: https://www.plain.com/docs/product/workflows/workflows-actions
Every action a workflow can take, from assigning a thread to calling your own endpoint.
Actions are the steps in your workflow that do something: assign a thread, update a property, send a message, or call an external service.
## Assignment
* **Assign to user** - Assigns the thread to a specific user or machine user.
* **Assign to company account owner** - Assigns the thread to the account owner of the customer's company.
* **Assign to team** - Distributes the thread using round-robin assignment across team members.
* **Unassign thread** - Removes all assignees from the thread.
## Thread properties
* **Set priority** - Sets the thread priority to: Urgent, High, Normal, or Low.
* **Set tier** - Moves the thread to a specific tier.
* **Set status** - Changes the thread status.
* **Apply labels** - Adds one or more labels to the thread.
* **Lock thread** - Locks the thread.
## Messages and notes
* **Send message** - Sends a message to the customer.
* **Add note** - Adds an internal note to the thread, only visible to your team.
## Integrations
### Send HTTP request
Sends an HTTPS POST request to any external endpoint. You configure the URL, headers, and a JSON body. The request runs in the background after the workflow step completes.
Use this action to connect your workflows to any service that accepts webhooks or API calls:
* Post to a Slack channel with a [Slack incoming webhook](https://api.slack.com/messaging/webhooks)
* Create a Linear issue through the [Linear API](https://developers.linear.app/docs/graphql/working-with-the-graphql-api)
* Trigger a PagerDuty incident with their [Events API](https://developer.pagerduty.com/docs/events-api-v2/overview/)
* Send a notification to Discord with a [Discord webhook](https://discord.com/developers/docs/resources/webhook)
* Call your own internal API to run custom logic
The request body supports variable interpolation. Available variables:
* `{{ customer.email }}`, `{{ customer.fullName }}`, `{{ customer.shortName }}`
* `{{ thread.ref }}`, `{{ thread.id }}`, `{{ thread.externalId }}`, `{{ thread.title }}`, `{{ thread.description }}`, `{{ thread.url }}`
* `{{ workspace.id }}`
Example body:
```json theme={null}
{
"threadId": "{{ thread.id }}",
"customer": "{{ customer.email }}",
"title": "{{ thread.title }}"
}
```
This action uses the same security measures and retry policy as Plain webhooks.
Actions run in sequence. If an action fails, the workflow stops and is marked as failed. Put your most important actions first.
# Conditions
Source: https://www.plain.com/docs/product/workflows/workflows-conditions
Branch a workflow on what a thread looks like, including with an AI prompt condition.
If/Else conditions are decision points inside the workflow that check something about the thread and split the flow into a **Yes** path and a **No** path.
## Combining conditions
You can combine multiple conditions using: **All of (AND)** - every condition must be true, **Any of (OR)** - at least one condition must be true, or **Not** - inverts a condition.
Start small. Build a workflow with two or three steps, test it on a few threads, then add to it. To change a published workflow, unpublish it, edit, and republish.
## Message contains
There are three message contains variants, each scoping which side of the conversation is checked:
* **Customer message contains**: matches only messages sent by the customer. Use this when you want to react to what the customer wrote, for example detecting keywords in their request or checking for a follow-up reply.
* **User message contains**: matches only messages sent by a member of your support team. Use this to detect when an agent has replied with a particular phrase or to trigger logic based on your team's response content.
* **Any message contains**: matches messages from either side of the conversation. Use this when the keyword could appear in any message regardless of who sent it.
When using any of these conditions, there are a few things worth knowing:
* **Case-insensitive**: matching is case-insensitive, so "Urgent", "urgent", and "URGENT" all match.
* **Multiple keywords (OR matching)**: comma-separated values (e.g. "urgent, asap, critical") match if the message contains *any* of those words.
* **Whole word matching**: by default keywords match inside other words, so "LEA" matches "please". Switch the condition to **Whole word** to match complete words only.
* **Bot replies are skipped**: autoresponder and agent replies (e.g. Ari) are ignored when picking the newest message, so a bot replying first doesn't hide the customer's message.
## AI prompts
The AI prompt condition evaluates your written condition against the thread context Plain provides, then determines whether the thread should take the `Yes` branch or `No` branch in your workflow.
### When to use it
When a workflow reaches an AI prompt condition, Plain assembles a context snapshot of the thread and passes it to the model alongside your prompt. The context includes:
* The full thread message history (up to the 600 most recent messages, each up to 3,000 characters)
* Thread metadata: title, description, status, priority, and labels
* Customer details: name and email address
* Channel information (e.g. email, Slack, API)
* Assignees
* Thread fields and their configured schemas
* Attachment count
The model evaluates your prompt against this context and returns a boolean: **match** or **no match**. If the condition matches, the configured action fires.
### How to write good conditions
Write your conditions using **"Match if…"** to describe what should take the **Yes** branch, and **"Don't match if/for…"** to clarify what should take the **No** branch.
Example: *Match if the customer explicitly requests a refund. Don't match for general billing questions or payment issues without refund mentions.*
### Examples
* *Match if the customer is asking to schedule a call or demo. Don't match for async questions with no scheduling intent.*
* *Match if the latest customer message is only a thank you or acknowledgment with no new question.*
* *Match if the customer explicitly asks to delete their account or data under privacy/GDPR.*
### Common mistakes
| Problem | Example | Solution |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Too broad, many unrelated outcomes | *Match for refunds, or bugs, or billing, or angry customers, or urgent issues; otherwise route to sales.* | Split into separate conditions, one per decision |
| Asking it to take action | *If match, assign to Tier 2; if no match, send a CSAT; if maybe, add label "Review".* | Each AI condition makes one match/no-match decision. Use [workflow actions](/docs/product/workflows/workflows-actions) to carry out an action after the prompt step. |
| Details that are not in the thread context | *Match if the customer's company in our CRM is in the manufacturing sector.* | AI can only use the thread context Plain provides. External data isn't available. |
| Contradictory or impossible to follow | *Match only if the customer never mentioned pricing, but also asked for a quote.* | Use consistent rules |
| Purely standard rules | *Match if the thread has label "Billing"* | Use standard conditions, like *contains label* in this example |
| Over-reliance on negation | *Don't match if it's not a duplicate of something that isn't a duplicate unless it's not spam.* | Use positive statements |
| Unbounded "use your judgment" asks | *Use your best judgment to decide if this thread is important.* | Give specific criteria. AI can't guess what "important" means for your workflow. |
# Triggers
Source: https://www.plain.com/docs/product/workflows/workflows-triggers
What can start a workflow, from a new thread to a schedule.
Triggers control when a workflow starts. Conditions are decision points inside the workflow that check something about the thread and split the flow into two paths.
## Manual
You run the workflow yourself. Open a thread, click **Run workflow**, and select which workflow to run. Useful for on-demand processes like escalation procedures.
## Automatic
The workflow runs automatically when a thread event happens. You choose which events during setup:
* Thread created
* Message added
* Labels changed
* Tier updated
* Thread escalated
* Priority changed
* Status changed
* Thread field created
* Thread field updated
You can select multiple events for one workflow. For example, trigger on both **Thread created** and **Message added** to catch new threads and follow-up messages.
## One run per event
Every event starts a separate run. A workflow set to both **Thread created** and **Message added** runs twice on a thread that arrives with a message on it, once for each event.
[Run order](/docs/product/workflows#run-order) applies to one event at a time. Workflows on different events run independently.
To keep two workflows in a fixed order, put them on the same trigger.
# Wait for
Source: https://www.plain.com/docs/product/workflows/workflows-wait-for
Pause a workflow for a set time, or until the customer replies.
Wait steps pause your workflow for a set duration before continuing. Use them to give customers time to respond, build escalation timers, or schedule follow-ups.
## How wait steps work
A wait step pauses the workflow for a duration you set, from 1 second up to 1 year.
Each wait step has two branches:
* **Completed**: the full duration passed.
* **Canceled**: a cancel condition was met before the duration ended.
## Waits and run order
While a workflow waits, the next workflow in the [run order](/docs/product/workflows#run-order) starts. A one-hour wait in the first workflow does not delay the rest by an hour.
A **start a Sidekick discussion** step behaves the same way: the workflow parks while Sidekick works, and the next workflow starts. See [Sidekick in workflows](/docs/product/agents/sidekick/in-workflows).
Put the steps that later workflows depend on before the wait, not after it.
## Cancel conditions
You can add a cancel condition to any wait step. This lets you end the wait early when something changes on the thread.
## Limits
* Minimum duration: 1 second
* Maximum duration: 1 year (31,536,000 seconds)
## When to use wait steps
* **Follow-ups**: send a message, wait 24 hours, then check if the customer replied.
* **Escalation timers**: wait 5 minutes after an urgent thread is created.
* **Business hours**: acknowledge a thread outside business hours, wait until morning, then assign to the team.
# Request signing
Source: https://www.plain.com/docs/request-signing
Verify that a webhook or customer card request came from Plain using its HMAC signature.
We sign outbound requests we make to your target URLs with a HMAC signature using a shared secret key. This allows you to verify that the request was made by Plain and not a third party.
## How to verify
Your workspace has a global HMAC secret, this secret can be viewed and (re)generated by workspace admins in **Settings** → **Request signing**.
If you have a HMAC secret set up, when you receive a request from Plain you will see a header `Plain-Request-Signature` with the HMAC signature.
You can verify this signature by hashing the request body with your HMAC secret and comparing it to the signature in the header.
**The signature is a HMAC-SHA256 hash of the request body, encoded as a hexadecimal string.**
### Node example
```js theme={null}
const crypto = require('crypto');
// You may need to stringify the request body if you are using a library that parses it to a javascript object
const requestBody = JSON.stringify(request.body);
const incomingSignature = request.headers['Plain-Request-Signature'];
const expectedSignature = crypto
.createHmac('sha-256', '')
.update(requestBody)
.digest('hex');
if (incomingSignature !== expectedSignature) {
return response.status(403).send('Forbidden');
}
```
# UI components
Source: https://www.plain.com/docs/ui-components
Describe UI declaratively when creating threads, events, and customer cards.
UI components are a way of describing some UI when creating threads or [events](https://help.plain.com/article/events) or building [customer cards](/docs/customer-cards).
For example - this is a button that links to Stripe.
```json theme={null}
{
"componentLinkButton": {
"linkButtonUrl": "http://stripe.com/",
"linkButtonLabel": "View in Stripe"
}
}
```
and it looks like this:
In the GraphQL API schema there are two separate unions for Custom Timeline Entry Components and Customer Card
Components, but both unions share the same types therefore they can be treated as the same.
For TypeScript, the [UI components SDK](/docs/ui-components/sdk) provides typed helpers for building these.
To see UI components in action you can experiment with them in the [UI components playground](https://app.plain.com/developer/ui-components-playground/)
# Badge
Source: https://www.plain.com/docs/ui-components/badge
Useful for statuses or when you need to attract attention to something.
A badge renders a short colored label, for a status or anything else worth drawing the eye to.
A badge has the following properties:
* `badgeLabel`: the text that should be displayed on the badge
* `badgeColor`: one of `GREY`, `GREEN`, `YELLOW`, `RED`, `BLUE`
For example:
# Container
Source: https://www.plain.com/docs/ui-components/container
Useful when you need to create a bit of structure.
A container groups other components together and draws a border around them.
A container has the following properties:
* `containerContent` (min 1): an array of components.
Allowed components within a Container are:
* [Badge](/docs/ui-components/badge)
* [CopyButton](/docs/ui-components/copy-button)
* [Divider](/docs/ui-components/divider)
* [LinkButton](/docs/ui-components/link-button)
* [Row](/docs/ui-components/row)
* [Spacer](/docs/ui-components/spacer)
* [Text](/docs/ui-components/text)
* [PlainText](/docs/ui-components/plain-text)
For example:
# CopyButton
Source: https://www.plain.com/docs/ui-components/copy-button
Useful if you have any IDs or other details you want to copy for use in messages or outside of Plain.
A copy button puts a value on the user's clipboard when clicked, such as an ID they need elsewhere.
A copy button has the following properties:
* `copyButtonTooltipLabel` (optional): the text that should be displayed on hover. Defaults to the value if not
provided.
* `copyButtonValue`: the value that should be copied to the user's clipboard after clicking the button
For example:
# Divider
Source: https://www.plain.com/docs/ui-components/divider
Useful when you need a bit of structure.
A divider draws a horizontal line between components, with configurable spacing around it.
A divider has the following properties:
* `dividerSpacingSize` (optional): the spacing the divider should have before and after the component. One of `XS`, `S`,
`M`, `L`, `XL`. Defaults to `S`.
For example:
# LinkButton
Source: https://www.plain.com/docs/ui-components/link-button
Links out to an external tool, such as your own admin panel or payment provider.
A link button opens a URL in a new tab, so you can send someone straight to your own tooling.
A link button has the following properties:
* `linkButtonLabel`: the text of the button
* `linkButtonUrl`: the URL the button should open in a new tab
For example:
# PlainText
Source: https://www.plain.com/docs/ui-components/plain-text
Useful when you want to show any text that should not have any formatting (is not Markdown). If you want markdown please use [Text](/ui-components/text).
Plain text renders text exactly as given, with no Markdown parsing. For formatted text, use [Text](/docs/ui-components/text) instead.
The plain text component has the following properties:
* `plainText`: the plain text
* `plainTextSize` (optional): one of `S`, `M`, `L`, defaults to `M`
* `plainTextColor` (optional): one of `NORMAL`, `MUTED`, `SUCCESS`, `WARNING`, `ERROR`, defaults to `NORMAL`
For example:
# Row
Source: https://www.plain.com/docs/ui-components/row
Useful when you need to show two things next to each-other.
A row lays out two groups of components side by side, one aligned left and one aligned right.
The row component has the following properties:
* `rowMainContent` (min 1): an array of row components
* `rowAsideContent` (min 1): an array of row components
The following components can be used in a row:
* [Badge](/docs/ui-components/badge)
* [CopyButton](/docs/ui-components/copy-button)
* [Divider](/docs/ui-components/divider)
* [LinkButton](/docs/ui-components/link-button)
* [Spacer](/docs/ui-components/spacer)
* [Text](/docs/ui-components/text)
* [PlainText](/docs/ui-components/plain-text)
For example:
# UI components SDK
Source: https://www.plain.com/docs/ui-components/sdk
Helper functions for building Plain UI components with full type safety.
The `@team-plain/ui-components` package provides typed helper functions for building `ComponentInput` objects. Instead of constructing JSON by hand, you get a concise, type-safe API.
## Installation
```bash theme={null}
npm install @team-plain/ui-components @team-plain/graphql
```
Requires `@team-plain/graphql` as a peer dependency.
## Usage
```ts theme={null}
import { uiComponent } from "@team-plain/ui-components";
```
## Available components
| Builder | Description |
| --------------------------------------------------- | --------------------------------------- |
| `uiComponent.text({ text, size?, color? })` | Rich text with optional size and color |
| `uiComponent.plainText({ text })` | Plain unformatted text |
| `uiComponent.badge({ label, color? })` | Colored badge |
| `uiComponent.divider()` | Horizontal divider |
| `uiComponent.spacer({ size })` | Vertical spacing |
| `uiComponent.linkButton({ label, url })` | Button that opens a URL |
| `uiComponent.copyButton({ value, tooltip? })` | Button that copies a value to clipboard |
| `uiComponent.workflowButton({ label, workflowId })` | Button that triggers a workflow |
| `uiComponent.container({ content })` | Groups components together |
| `uiComponent.row({ mainContent, asideContent })` | Two-column layout |
For details on each component's properties and how they render, see the [UI components reference](/docs/ui-components).
## Example
```ts theme={null}
import { uiComponent } from "@team-plain/ui-components";
const components = [
uiComponent.row({
mainContent: [uiComponent.text({ text: "Customer Plan", size: "L" })],
asideContent: [uiComponent.badge({ label: "Pro", color: "GREEN" })],
}),
uiComponent.divider(),
uiComponent.text({ text: "Signed up 3 days ago", color: "MUTED" }),
uiComponent.spacer({ size: "M" }),
uiComponent.container({
content: [
uiComponent.linkButton({
label: "View in Stripe",
url: "https://dashboard.stripe.com/...",
}),
uiComponent.copyButton({
value: "cus_abc123",
tooltip: "Copy Stripe ID",
}),
],
}),
];
```
## Resources
* [UI components reference](/docs/ui-components): component properties and visual examples
* [UI components playground](https://app.plain.com/developer/ui-components-playground/): build and preview components interactively
* [GitHub repository](https://github.com/team-plain/sdk/tree/main/packages/ui-components)
# Spacer
Source: https://www.plain.com/docs/ui-components/spacer
Adds vertical space between components, in one of five fixed sizes.
A spacer adds vertical space between components.
A spacer has the following property:
* `spacerSize`: the amount of space the component should take up. One of `XS`, `S`, `M`, `L`, `XL`.
For example:
# Text
Source: https://www.plain.com/docs/ui-components/text
Renders a line of text, with optional size, color, and formatting.
Text renders a line of text, and accepts a subset of Markdown for bold, italic, and links.
The text component has the following properties:
* `text`: the text. Can include a subset of markdown (bold, italic, and links).
* `textSize` (optional): one of `S`, `M`, `L`, defaults to `M`
* `textColor` (optional): one of `NORMAL`, `MUTED`, `SUCCESS`, `WARNING`, `ERROR`, defaults to `NORMAL`
For example:
# WorkflowButton
Source: https://www.plain.com/docs/ui-components/workflow-button
Lets users trigger a workflow directly from a customer card.
A workflow button runs one of your workflows when a user clicks it.
A workflow button has the following properties:
* `workflowButtonLabel`: the text of the button
* `workflowButtonWorkflowIdentifier`: an object containing either:
* `workflowId`: the ID of the workflow to trigger
When clicked, the button will trigger the specified workflow in the context of the current thread. The button shows a loading state while the workflow is being triggered and displays a tooltip with the latest execution status, including when it was run and how long it took.
For example:
# Webhooks
Source: https://www.plain.com/docs/webhooks
Get notified when something happens in your workspace, and react to it in your own systems.
Webhooks allow you to get notified about events happening in your Plain workspace. You can react to these events in many ways, such as:
* Assigning threads to users based on business requirements (urgency, customer value, recurrency, etc.)
* Creating an AI-powered auto-responder
* Categorizing threads by adding labels based on the their content
* Triggering internal incidents (by identifying patterns in inbound messages)
* Tracking metrics from your customer support team
For TypeScript, the [Webhooks SDK](/docs/webhooks/sdk) handles parsing and signature verification with full types.
## Receiving events from Plain
Events happening in your workspace ('Plain events') are delivered as Webhook requests.
In order to receive webhook requests, you need a **publicly available HTTPS** endpoint. Plain makes
an `HTTP POST` request to this endpoint whenever an event you are interested in occurs.
Once your endpoint is ready, you may create a *webhook target* in Plain. A webhook target tells Plain what events you
are interested in and where to send those events.
You can create it by going to **Settings** → **Webhooks**, then clicking on '+ Add webhook target'
Then, you need to choose a name (e.g. 'Customer notifications'), the URL of your webhook endpoint, the events you want
to receive and whether you want to enable it straight away.
You can create up to **25 webhook targets** per workspace.
Plain events may contain Personally Identifiable Information (PII). If you want to test webhooks
with a production workspace, take the necessary precautions to avoid leaking PII to untrusted
parties.
We have created a repository where you will find instructions on how to create a webhook endpoint
using different programming languages. You can find it
[here](https://github.com/team-plain/webhooks-resources/tree/main/servers).
## Security
Webhook requests are always sent through HTTPS.
If you want, you can include basic authentication credentials in your webhook target's URL (`https://username:password@example.com`) which will then be sent along the webhook request in an `Authorization` header:
```plaintext theme={null}
Authorization: Basic cGxhaW46cm9ja3M=
```
Plain also supports [request signing](/docs/request-signing) and [mTLS](/docs/mtls) to verify that the request was made by Plain and not a third party.
## Delivery semantics
Plain guarantees **at-least-once** delivery of webhook requests. As such, you should make sure your webhook endpoint is idempotent. The `id` field in the [webhook request body](#body) can be used as an idempotency key.
## Handling webhook requests
Plain considers a webhook request to be successfully delivered if your endpoint returns a **2xx** HTTP status code. The contents of the response body are ignored.
Any other HTTP status code will be considered a failure, **including redirects**, which are explicitly forbidden.
## Retry policy
When a webhook request fails, Plain keeps retrying it during the **\~5 days** after the first request. The delay between
retries is set by the following table:
| Retry # | Delay | Approximate time since first attempt |
| ------- | ----- | ------------------------------------ |
| 1 | 10s | 10s |
| 2 | 30s | 40s |
| 3 | 5m | 6m |
| 4 | 30m | 36m |
| 5 | 1h | 1.5h |
| 6 | 3h | 4.5h |
| 7 | 6h | 10.5h |
| 8 | 12h | 22.5h |
| 9 | 1d | 2d |
| 10 | 1d | 3d |
| 11 | 1d | 4d |
| 12 | 1d | 5d |
Plain keeps track of all the webhook delivery attempts and their results. Each webhook request
includes [some metadata](#webhook-metadata) that you can use in order to know which delivery attempt it is currently
being processed.
## The webhook request
Webhook requests are sent as an `HTTP POST` request to the webhook target URL.
### Headers
* `Accept`: `application/json`
* `Content-Type`: `application/json`
* `User-Agent`: `Plain-Webhooks/1.0 (plain.com; help@plain.com)`
* `Plain-Workspace-Id`: The ID of the workspace where the Plain event originated
* `Plain-Webhook-Target-Id`: The ID of the webhook target this webhook request is being sent to
* `Plain-Webhook-Target-Version`: The [version](/docs/webhooks/versions.mdx) of the webhook target this webhook request is being sent to
* `Plain-Webhook-Delivery-Attempt-Id`: The ID of the delivery attempt. It will be different on every delivery attempt
* `Plain-Webhook-Delivery-Attempt-Number`: The current delivery attempt number (starts at 1)
* `Plain-Webhook-Delivery-Attempt-Timestamp`: The time at which the delivery attempt was made. In UTC and formatted as
ISO8601. E.g. `1989-10-28T17:30:00.000Z`
* `Plain-Event-Type`: The Plain event's type
* `Plain-Event-Id`: The ID of the Plain event. It remains the same across all of the delivery attempts
An additional `Authorization` header is sent if the webhook target URL contains authentication credentials.
### Body
The request body is a `JSON` object with the fields below.
The JSON schema for Plain the webhook request body can be found [here](https://core-api.uk.plain.com/webhooks/schema/latest.json).
| Field | Type | Description |
| ----------------- | -------- | -------------------------------------------------------------------------------------------------------- |
| `id` | `string` | The ID of the Plain event. It remains the same across all of the delivery attempts |
| `type` | `string` | The Plain event's type |
| `webhookMetadata` | `object` | Metadata associated with the webhook request. See [Webhook Metadata](#webhook-metadata) for more details |
| `timestamp` | `string` | The Plain event's timestamp. In UTC and formatted as ISO8601. E.g. `1989-10-28T17:30:00.000Z` |
| `workspaceId` | `string` | The ID of the workspace where the Plain event originated |
| `payload` | `object` | The Plain event's payload [(Example)](/docs/webhooks/thread-created); |
### Webhook metadata
All the following fields are also sent as [HTTP headers](#headers).
| Field | Type | Description |
| --------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `webhookTargetId` | `string` | The ID of the webhook target this webhook request is being sent to. This is the ID that you will find under **Settings → Webhooks** in the Support App |
| `webhookTargetVersion` | `string` | The [version](/docs/webhooks/versions.mdx) of the webhook target this webhook request is being sent to. |
| `webhookDeliveryAttemptId` | `string` | The ID of the delivery attempt. It will be different on every delivery attempt |
| `webhookDeliveryAttemptNumber` | `string` | The current delivery attempt number (starts at 1) |
| `webhookDeliveryAttemptTimestamp` | `string` | The time at which the delivery attempt was made. In UTC and formatted as ISO8601. E.g. `1989-10-28T17:30:00.000Z` |
# Customer changed
Source: https://www.plain.com/docs/webhooks/customer-changed
Fired when a customer is created or updated, with the previous state on updates.
This event is fired when a customer is created or updated. `changeType` is `ADDED` or `UPDATED`; `previousCustomer` is null on `ADDED`. Deletions are not reported here, see [Customer deleted](/docs/webhooks/customer-deleted).
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Customer created
Source: https://www.plain.com/docs/webhooks/customer-created
Fired when a new customer is created in your workspace. Includes the payload schema and an example.
This event is fired when a new customer is created in your workspace.
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Customer deleted
Source: https://www.plain.com/docs/webhooks/customer-deleted
Fired when a customer is deleted from your workspace. Includes the payload schema and an example.
This event is fired when a customer is deleted from your workspace.
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Customer group changed
Source: https://www.plain.com/docs/webhooks/customer-group-changed
Fired when a customer group is created, updated or deleted.
This event is fired when a customer group itself changes. For changes to which customers belong to a group, see [Customer group membership changed](/docs/webhooks/customer-group-membership-changed).
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Customer group membership changed
Source: https://www.plain.com/docs/webhooks/customer-group-membership-changed
Fired when a customer is added to or removed from a customer group.
This event is fired whenever a customer is added or removed from a customer group.
The `changeType` field allows you to know what kind of change has occurred. It can be one of the following:
* `ADDED`: a customer group membership was added
* `REMOVED`: a customer group membership was removed
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Customer updated
Source: https://www.plain.com/docs/webhooks/customer-updated
Fired when a customer is updated, including which changes trigger it.
This event is fired when a customer is updated in your workspace. You can expect this event:
* when a customer is marked as spam
* when a customer is un-marked as spam
* when the details of a customer are updated
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Discussion created
Source: https://www.plain.com/docs/webhooks/discussion-created
Fired when a discussion is started on a thread.
This event is fired when a [discussion](/docs/graphql/discussions) is started, on a thread or on its own.
Read `discussion.type` before acting. `AGENT_SESSION` is a Sidekick conversation, which is the only type a [custom agent](/docs/agents/discussions) answers; `SLACK` and `EMAIL` discussions are conversations with people. Read `discussion.agent` to see which machine user the conversation was opened against. It is `null` on a discussion Plain's own Sidekick runs.
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Discussion message created
Source: https://www.plain.com/docs/webhooks/discussion-message-created
Fired when a message is posted in a discussion.
This event is fired when a message is posted in a [discussion](/docs/graphql/discussions), including the messages your own agent posts.
`message.type` is the field to read, and its sense is the opposite of what you might expect on a Sidekick conversation: a person's turn is `OUTBOUND` and your agent's own replies return as `INBOUND`. An agent that answers every message it receives, without checking the type, answers itself forever. [Interacting in discussions](/docs/agents/discussions#decide-whether-to-answer) has the full filter.
`message.workspaceFiles` holds the files attached to the message, and is present from webhook version `2026-09-02`.
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Tool call approval requested
Source: https://www.plain.com/docs/webhooks/discussion-tool-call-approval-requested
Fired when a custom agent asks a user to approve one of its tool calls.
This event is fired when a [custom agent](/docs/agents/tool-calls#ask-for-approval) calls `requestDiscussionToolCallApproval` to stop in front of a tool call and wait for a user to decide it. Plain shows that user an approval card with **Approve** and **Deny** controls.
`toolCallId` names the call the approval gates, and it is the agent's own id for that call rather than one Plain issues. `justification` is the text the agent gave, and the user reads it on the card. Requesting the approval also moves the discussion to `TOOL_CALL_APPROVAL_PENDING`, which is why `discussion.agentStatus` reads that way in the example below.
Plain's own Sidekick approvals do not produce this event. It is sent only for a discussion a custom agent drives.
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Tool call approval resolved
Source: https://www.plain.com/docs/webhooks/discussion-tool-call-approval-resolved
Fired when a user approves or denies a custom agent tool call.
This event is fired when a user decides an approval a [custom agent](/docs/agents/tool-calls#ask-for-approval) asked for. Subscribe to it so your agent resumes on a decision instead of polling the discussion.
`status` is `APPROVED` or `DENIED`, and `reviewerNote` is what the user typed, or `null`. On `DENIED`, Plain has already failed the gated tool call and set `reviewerNote` as its error, so do not report an error on that call yourself: feed the note back to your model and try again under a new `toolCallId`.
`discussion.agentStatus` tells you where the discussion is now. It stays `TOOL_CALL_APPROVAL_PENDING` while another approval is open, and otherwise it is `IN_PROGRESS` if any tool call is still `PENDING` or `IDLE` if none is. An approval a user has approved leaves its own call `PENDING` for you to run, so a lone approval reads `IN_PROGRESS` and a lone denial reads `IDLE`.
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Discussion turn stop requested
Source: https://www.plain.com/docs/webhooks/discussion-turn-stop-requested
Fired when someone requests that an agent stop its current turn.
This event fires when someone requests that an agent stop its current turn in a discussion. Use it to stop generating a response while keeping the discussion available for the next message.
`requestedBy` identifies who requested the stop, and `requestedAt` records when they requested it. The `discussion` object identifies the agent session to stop.
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Webhooks SDK
Source: https://www.plain.com/docs/webhooks/sdk
Webhook parsing and signature verification for Plain webhooks.
The `@team-plain/webhooks` package provides typed webhook parsing and HMAC-SHA256 signature verification. It is a standalone package with no dependency on `@team-plain/graphql`.
## Installation
```bash theme={null}
npm install @team-plain/webhooks
```
## Verify and parse (recommended)
`verifyPlainWebhook` validates the HMAC-SHA256 signature, checks the timestamp to prevent replay attacks, and parses the payload against the webhook JSON schema.
```ts theme={null}
import { verifyPlainWebhook } from "@team-plain/webhooks";
const result = verifyPlainWebhook(
rawBody, // raw request body string
req.headers["plain-request-signature"], // signature header
process.env.PLAIN_WEBHOOK_SECRET, // your webhook signing secret
);
if (result.error) {
console.error(result.error.message);
} else {
const event = result.data;
console.log(event.eventType, event.payload);
}
```
The optional fourth argument `tolerance` (default: `300` seconds) controls the maximum allowed age of the webhook timestamp.
## Parse only (no signature verification)
`parsePlainWebhook` validates the payload against the webhook JSON schema without checking the signature. Useful for development or when verification is handled elsewhere.
```ts theme={null}
import { parsePlainWebhook } from "@team-plain/webhooks";
const result = parsePlainWebhook(rawBody);
if (result.error) {
console.error(result.error.message);
} else {
const event = result.data;
console.log(event.eventType, event.payload);
}
```
## Error types
All functions return a `Result`, either `{ data: T }` or `{ error: Error }`.
| Error class | When |
| ---------------------------------------- | ------------------------------------------------------------------------ |
| `PlainWebhookSignatureVerificationError` | Invalid signature, missing headers, or expired timestamp |
| `PlainWebhookPayloadError` | Payload fails JSON schema validation |
| `PlainWebhookVersionMismatchError` | Payload version doesn't match the schema version bundled in this package |
## Typed event payloads
All webhook payload types are exported for use in your handlers:
```ts theme={null}
import type {
ThreadCreatedPublicEventPayload,
CustomerCreatedPublicEventPayload,
// ...
} from "@team-plain/webhooks";
```
See the full list of [webhook events](/docs/webhooks/thread-created) for all available event types.
## Resources
* [Webhooks overview](/docs/webhooks): setup, security, delivery semantics, and retry policy
* [Request signing](/docs/request-signing): how Plain signs webhook requests
* [GitHub repository](https://github.com/team-plain/sdk/tree/main/packages/webhooks)
# Task created
Source: https://www.plain.com/docs/webhooks/task-created
Fired when a new task is created in your workspace.
This event is fired when a new task is created.
Subscribe to this event to keep an external system, such as a CRM or project tracker, in sync with tasks created in Plain.
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Task deleted
Source: https://www.plain.com/docs/webhooks/task-deleted
Fired when a task in your workspace is deleted.
This event is fired when a task is deleted.
The payload contains `previousTask`, the deleted row, with `deletedAt` and `deletedBy` populated.
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Task status transitioned
Source: https://www.plain.com/docs/webhooks/task-status-transitioned
Fired when a task's status changes, for example moving to done or canceled.
This event is fired when a task's status changes (for example, moving to `DONE` or `CANCELLED`).
A status change also fires [`task.task_updated`](/docs/webhooks/task-updated).
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Task updated
Source: https://www.plain.com/docs/webhooks/task-updated
Fired when a task's title, description, priority, company, tenant, or assignment changes.
This event is fired when a task's title, description, priority, company, tenant, or assignment changes.
A status change fires both this event and [`task.task_status_transitioned`](/docs/webhooks/task-status-transitioned).
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Thread assignment transitioned
Source: https://www.plain.com/docs/webhooks/thread-assignment-transitioned
Fired when a thread's assignee changes or the thread is unassigned.
This event is fired when the assignee of a thread changes or a thread is unassigned.
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Chat received
Source: https://www.plain.com/docs/webhooks/thread-chat-received
Fired when a customer sends a chat message, with the payload schema and an example.
This event is fired when a chat message from a customer is received.
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Chat sent
Source: https://www.plain.com/docs/webhooks/thread-chat-sent
Fired when a chat message is sent to a customer on a thread.
This event is fired when a chat message is sent to a customer in a thread.
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Thread created
Source: https://www.plain.com/docs/webhooks/thread-created
Fired when a new thread is created in your workspace, whatever channel it arrived on.
This event is fired when a new thread is created in your workspace.
You can subscribe to this event if you want to build an [autoresponder](/docs/graphql/threads/autoresponders).
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Discord message received
Source: https://www.plain.com/docs/webhooks/thread-discord-message-received
Fired when a customer sends a Discord message on a thread.
This event is fired when a Discord message is received from a customer.
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Discord message sent
Source: https://www.plain.com/docs/webhooks/thread-discord-message-sent
Fired when a Discord message is sent on a thread.
This event is fired when a Discord message is sent from a thread.
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Discord message updated
Source: https://www.plain.com/docs/webhooks/thread-discord-message-updated
Fired when a Discord message on a thread is edited or deleted.
This event is fired when a Discord message in a thread is edited or deleted on Discord.
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Email received
Source: https://www.plain.com/docs/webhooks/thread-email-received
Fired when an inbound email arrives in your workspace.
This event is fired when an email is received in your workspace.
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Email sent
Source: https://www.plain.com/docs/webhooks/thread-email-sent
Fired when an email is sent from your workspace.
This event is fired when an email is sent in your workspace.
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Thread field created
Source: https://www.plain.com/docs/webhooks/thread-field-created
Fired when a thread field is set on a thread for the first time.
This event is fired when a new thread field is created in your workspace.
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Thread field deleted
Source: https://www.plain.com/docs/webhooks/thread-field-deleted
Fired when a thread field is removed from a thread.
This event is fired when a thread field is deleted in your workspace.
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Thread field updated
Source: https://www.plain.com/docs/webhooks/thread-field-updated
Fired when the value of a thread field changes.
This event is fired when a thread field is updated in your workspace.
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Thread labels changed
Source: https://www.plain.com/docs/webhooks/thread-labels-changed
Fired when labels are added to or removed from a thread.
This event is fired when labels are added to or removed from a thread.
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Thread locked
Source: https://www.plain.com/docs/webhooks/thread-locked
Fired when a thread is locked and can no longer receive new customer messages.
This event is fired when a thread is locked. A locked thread cannot receive new customer messages.
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Microsoft Teams message received
Source: https://www.plain.com/docs/webhooks/thread-ms-teams-message-received
Fired when a customer sends a Microsoft Teams message on a thread.
This event is fired when a Microsoft Teams message is received from a customer.
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Microsoft Teams message sent
Source: https://www.plain.com/docs/webhooks/thread-ms-teams-message-sent
Fired when a Microsoft Teams message is sent on a thread.
This event is fired when a Microsoft Teams message is sent from a thread.
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Note created
Source: https://www.plain.com/docs/webhooks/thread-note-created
Fired when an internal note is added to a thread.
This event is fired when a note is created in a thread.
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Note mention created
Source: https://www.plain.com/docs/webhooks/thread-note-mention-created
Fired when a note on a thread mentions a machine user.
This event is fired when a note mentions a machine user, such as an [agent](/docs/agents) you built. It also fires when a user edits a note and adds a mention. `mentions` lists the machine users mentioned. Mentions of users stay as `<@u_…>` tokens in `note.markdown` and don't fire this event.
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Thread priority changed
Source: https://www.plain.com/docs/webhooks/thread-priority-changed
Fired when the priority of a thread changes.
This event is fired when the priority of a thread changes.
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Thread SLA status transitioned
Source: https://www.plain.com/docs/webhooks/thread-service-level-agreement-status-transitioned
Fired when the status of an SLA attached to a thread changes, including breaches.
This event is fired when the status of an SLA linked to a thread changes.
As part of the `serviceLevelAgreementStatusDetail` field threads can have a status with the following values:
| Status | Description |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PENDING` | When the timer on the SLA is counting down but has not met the `IMMINENT_BREACH` threshold |
| `IMMINENT_BREACH` | For SLAs where an alert has been set up to notify the team before it breaches. The SLA will be in this status after the alert period and before the SLA breaches |
| `BREACHING` | Applies to SLAs while their conditions are not met e.g if a thread with a first response time (FRT) SLA has not been replied to after the time period specified |
| `ACHIEVED` | A thread where the SLA conditions were met e.g a thread was replied to within the FRT SLA period |
| `BREACHED` | A thread where the SLA conditions were not met (and so entered `BREACHING`) but action has been taken that would have resolved the SLA e.g a thread breached the FRT SLA, but then first reply was sent |
| `CANCELLED` | An SLA which no longer applies e.g if a thread is marked as done with no reply the SLA is canceled since we don't want it to affect metrics |
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Slack message received
Source: https://www.plain.com/docs/webhooks/thread-slack-message-received
Fired when a Slack message arrives in a connected channel.
This event is fired when a Slack message is received in your workspace.
If the message is edited in Slack, this webhook will not fire again.
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Slack message sent
Source: https://www.plain.com/docs/webhooks/thread-slack-message-sent
Fired when a Slack message is sent from your workspace.
This event is fired when a Slack message is sent in your workspace.
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Slack message updated
Source: https://www.plain.com/docs/webhooks/thread-slack-message-updated
Fired when a Slack message on a thread is edited, deleted or reacted to.
This event is fired when a Slack message in a thread is edited, deleted, or has a reaction added or removed. `changeType` tells you which happened. `reactionChange` is only populated when `changeType` is `REACTIONS_CHANGED`, and is null otherwise.
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Thread status transitioned
Source: https://www.plain.com/docs/webhooks/thread-status-transitioned
Fired when a thread moves between Todo, Snoozed, Done, or Ignored.
This event is fired when the status of a thread changes.
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Thread tenant updated
Source: https://www.plain.com/docs/webhooks/thread-tenant-updated
Fired when the tenant a thread belongs to is changed, added or removed.
This event is fired when the tenant a thread belongs to changes. Both the previous and the new tenant are included, and either can be null.
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Timeline entry changed
Source: https://www.plain.com/docs/webhooks/timeline-entry-changed
Fired when a timeline entry is added, updated or removed.
This event is fired when an entry on a customer or thread timeline changes. `changeType` is `ADDED`, `UPDATED` or `REMOVED`; `previousTimelineEntry` is null on `ADDED`.
## Schema
[**View JSON Schema →**](https://core-api.uk.plain.com/webhooks/schema/latest.json)
Example:
# Webhook versions
Source: https://www.plain.com/docs/webhooks/versions
Every webhook target is pinned to a schema version. This page lists each version and what changed.
Every [webhook target](/docs/webhooks#receiving-events-from-plain) in Plain is associated with a specific version. The webhook version defines the schema of the payload that Plain sends to your endpoint. By specifying a version, you ensure that the payload format remains consistent, even as Plain evolves and introduces changes to the webhook schema.
**Benefits of Versioning**:
* **Consistency**: Your endpoint always receives payloads in the same format.
* **Control**: You decide when to adopt new schema changes.
* **Stability**: Prevents unexpected breaking changes due to schema updates.
## Available versions
We recommend always using the latest version of the webhook payload schema to benefit from new features and improvements. Below are the currently available versions:
### `2026-09-18` (Latest)
* Added `FIRST_RESOLUTION_TIME` and `TOTAL_RESOLUTION_TIME` to the `serviceLevelAgreement` definition, alongside the existing `FIRST_RESPONSE_TIME` and `NEXT_RESPONSE_TIME`. Each carries the same SLA fields plus its own target, `firstResolutionTimeMinutes` and `totalResolutionTimeMinutes`.
* [View JSON Schema](https://core-api.uk.plain.com/webhooks/schema/2026-09-18.json)
* Added [`discussion.turn_stop_requested`](/docs/webhooks/discussion-turn-stop-requested), sent when someone requests that an agent stop its current turn. The payload contains `discussion`, `requestedBy`, and `requestedAt`.
* [View JSON Schema](https://core-api.uk.plain.com/webhooks/schema/2026-09-18.json)
### `2026-09-11`
* **What's New**:
* Added the `task.task_created` event. It is sent when a [task](/docs/graphql/tasks) is created. The payload contains `task`.
* Added the `task.task_updated` event. It is sent when a task's title, description, priority, company, tenant, or assignment changes. The payload contains `task` and `previousTask`.
* Added the `task.task_status_transitioned` event. It is sent when a task's status changes. The payload contains `task` and `previousTask`. A status change fires both `task.task_updated` and `task.task_status_transitioned`.
* Added the `task.task_deleted` event. It is sent when a task is deleted. The payload contains `previousTask`, the deleted row, with `deletedAt` and `deletedBy` populated.
* Added the `task`, `taskAssignee`, and `taskSourceLink` schema definitions to support the events above.
* [View JSON Schema](https://core-api.uk.plain.com/webhooks/schema/2026-09-11.json)
### `2026-09-06`
* **What's New**:
* Added the `discussion.tool_call_approval_requested` event. It is sent when a tool call in a [discussion](/docs/graphql/discussions) needs approval before it runs. The payload contains `discussion`, `approvalId`, `toolCallId`, `justification`, `requestedBy` and `requestedAt`.
* Added the `discussion.tool_call_approval_resolved` event. It is sent when a user approves or denies that request. The payload contains `discussion`, `approvalId`, `toolCallId`, `status`, `justification`, `reviewerNote`, `resolvedBy` and `resolvedAt`.
* Added `agentStatus` to the `discussion` object on `discussion.message_created`. It is one of `IDLE`, `IN_PROGRESS`, `TOOL_CALL_APPROVAL_PENDING` or `UNKNOWN`.
* No fields were changed or removed, so upgrading from `2026-09-02` requires no changes to an existing consumer.
* [View JSON Schema](https://core-api.uk.plain.com/webhooks/schema/2026-09-06.json)
### `2026-09-02`
* **What's New**:
* Added `workspaceFiles` to the `message` object on `discussion.message_created`. Each entry contains `id`, `fileName`, `fileSizeBytes`, `fileMimeType` and `fileExtension`.
* No fields were changed or removed, so upgrading from `2026-08-31` requires no changes to an existing consumer.
* [View JSON Schema](https://core-api.uk.plain.com/webhooks/schema/2026-09-02.json)
### `2026-08-31`
* **What's New**:
* Added the `discussion.discussion_created` event. It is sent when a [discussion](/docs/graphql/discussions) is started on a thread. The payload contains `discussion`.
* [View JSON Schema](https://core-api.uk.plain.com/webhooks/schema/2026-08-31.json)
### `2026-08-25`
* **What's New**:
* Added the `thread.thread_locked` event. It is sent when a thread is [locked](/docs/product/platform/threads/locking-threads) so nobody can reply to it. The payload contains `thread` and `previousThread`.
* [View JSON Schema](https://core-api.uk.plain.com/webhooks/schema/2026-08-25.json)
### `2026-08-19`
* **What's New**:
* Added the `discussion.message_created` event. It is sent when a message is posted in a [discussion](/docs/graphql/discussions). The payload contains `discussion` and `message`.
* [View JSON Schema](https://core-api.uk.plain.com/webhooks/schema/2026-08-19.json)
### `2026-07-14`
* **What's New**:
* Added the `thread.note_mention_created` event. It is sent when a note mentions a machine user (e.g. an [agent](/docs/agents)). The payload contains `thread`, `note` and `mentions`, where `mentions` is an array of `machineUser`.
* Added `attio` and `email_domain` to the `tenant.source` enum on `thread.tenant_updated`.
* [View JSON Schema](https://core-api.uk.plain.com/webhooks/schema/2026-07-14.json)
### `2026-07-07`
* **What's New**:
* Added `attio` and `email_domain` to the `tenant.source` enum on `thread.tenant_updated`.
* [View JSON Schema](https://core-api.uk.plain.com/webhooks/schema/2026-07-07.json)
### `2026-06-15`
* **What's New**:
* Added `WAITING_INDEFINITELY` to the `statusDetail.type` enum on `thread`. This is emitted when a thread is snoozed without a duration (i.e. snoozed indefinitely).
* `componentUser.user` may now also be a `machineUser` (in addition to a `user`) when a [UI component](/docs/ui-components) references a machine user (e.g. an [agent](/docs/agents)). Update consumers to handle the additional shape: human users continue to use the existing `user` payload.
* [View JSON Schema](https://core-api.uk.plain.com/webhooks/schema/2026-06-15.json)
### `2026-05-06`
* **What's New**:
* Added `dateTime` and `user` [UI component](/docs/ui-components) types to custom timeline entries.
* [View JSON Schema](https://core-api.uk.plain.com/webhooks/schema/2026-05-06.json)
### `2026-04-21`
* **What's New**:
* Added the `thread.thread_tenant_updated` event. It is sent when a thread's tenant changes. The payload contains `thread` and `previousThread`.
* Added `reactionChange` on `thread.slack_message_updated`.
* [View JSON Schema](https://core-api.uk.plain.com/webhooks/schema/2026-04-21.json)
### `2026-03-13`
* **What's New**:
* Added the `workflowButton` [UI component](/docs/ui-components) type to custom timeline entries.
* [View JSON Schema](https://core-api.uk.plain.com/webhooks/schema/2026-03-13.json)
### `2026-02-27`
* **What's New**:
* Added `changeType` on `thread.slack_message_updated`.
* [View JSON Schema](https://core-api.uk.plain.com/webhooks/schema/2026-02-27.json)
### `2026-02-13`
* **What's New**:
* Added `dateValue` field to `threadField`.
* Added `DATE` and `CURRENCY` to the `threadField.type` enum.
* [View JSON Schema](https://core-api.uk.plain.com/webhooks/schema/2026-02-13.json)
### `2026-02-11`
* **What's New**:
* Added `numberValue` field to `threadField`.
* Added `NUMBER` to the `threadField.type` enum.
* [View JSON Schema](https://core-api.uk.plain.com/webhooks/schema/2026-02-11.json)
### `2025-08-06`
* **What's New**:
* Added `additionalAssignees` to `thread`.
* Added `externalId` and `isExcludedFromAi` fields to `labelType`.
* [View JSON Schema](https://core-api.uk.plain.com/webhooks/schema/2025-08-06.json)
### `2025-07-30`
* **What's New**:
* `threadField.createdBy` and `threadField.updatedBy` changed from `internalActor` to `actor` (now supports customer and other actor types).
* [View JSON Schema](https://core-api.uk.plain.com/webhooks/schema/2025-07-30.json)
### `2024-09-18`
Our first official versioned webhook payload schema.
* **What's New**:
* Introduction of webhook versioning.
* Improved forward-compatibility schema definitions for payloads.
* Microsoft Teams events.
* New thread status details.
* [View JSON Schema](https://core-api.uk.plain.com/webhooks/schema/2024-09-18.json)
### `unversioned`
The legacy webhook payload schema before versioning was implemented.
* [View JSON Schema](https://core-api.uk.plain.com/webhooks/schema/unversioned.json)
## How to upgrade to the latest version
Upgrading to the latest webhook version involves updating your code to handle the new schema and changing your webhook target settings in Plain.
### Step 1: Update your code
Modify your code to handle both the old and new webhook payload versions during the transition period. This ensures uninterrupted processing of events.
Deploy this updated code, and fast follow with Step 2.
### Step 2: Update the webhook target in Plain
After deploying your updated code, change the version of your webhook target in Plain to the new version. This ensures that all future webhook events are sent using the latest schema.
### Step 3: Revert temporary code changes
Once you have confirmed that your application is successfully processing events with the new version, you can remove the code that handles both old and new versions. Your code can now exclusively handle the latest webhook payload schema.
Ensure that your webhook handling code is **idempotent** and can gracefully handle **duplicate
events**. Plain's webhook delivery is **at least once**, meaning the same event might be delivered
multiple times. Refer to our [delivery
semantics](/docs/webhooks#delivery-semantics) for more
information.
## Identifying the webhook version in received payloads
If you receive a webhook payload and are unsure which version it is using, you can identify the version by checking:
* **Headers**: The `Plain-Webhook-Target-Version` header indicates the version of the webhook target for which this request is intended.
* **Payload Metadata**: Within the [webhook metadata](/docs/webhooks#webhook-metadata) in the payload body, the `webhookTargetVersion` field specifies the version of the webhook target for this request.
This information helps you determine how to parse and handle the webhook payload according to its schema version.
## Best practices and recommendations
* **Monitor Logs**: After upgrading, monitor your logs and error tracking systems for any issues related to webhook processing.
* **Stay Informed**: Keep an eye on our documentation and [change log](https://www.plain.com/changelog) for future updates or changes to the webhook schema.
If you have any questions or need assistance, please reach out to us at **[help@plain.com](mailto:help@plain.com)**.