From 0450c4690502eecb347a02c165b6117b33b764f4 Mon Sep 17 00:00:00 2001 From: danciaclara Date: Thu, 9 Apr 2026 15:52:11 +0530 Subject: [PATCH 1/5] Plane Runner --- docs/.vitepress/config.ts | 4 + docs/automations/plane-runner.md | 586 +++++++++++++++++++++++++++++++ 2 files changed, 590 insertions(+) create mode 100644 docs/automations/plane-runner.md diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 5dd0e68..c1a48d2 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -480,6 +480,10 @@ export default defineConfig({ text: "Automations", link: "/automations/custom-automations", }, + { + text: "Plane Runner", + link: "/automations/plane-runner", + }, ], }, { diff --git a/docs/automations/plane-runner.md b/docs/automations/plane-runner.md new file mode 100644 index 0000000..40d00cb --- /dev/null +++ b/docs/automations/plane-runner.md @@ -0,0 +1,586 @@ +--- +title: Plane Runner +description: Write custom scripts and reusable functions in JavaScript or TypeScript to automate workflows, enforce rules, and extend Plane's behavior. +--- + +# Plane Runner + +Plane Runner is a secure, sandboxed execution engine that lets you write custom logic in JavaScript or TypeScript. You can use it to automate actions when events happen, run scheduled jobs, and enforce rules during workflow transitions. + +Runner provides two building blocks — **Scripts** and **Functions** — that work together to extend Plane's behavior beyond what [built-in automations](/automations/custom-automations) offer. + +## Activate Plane Runner + +> **Role**: Workspace Admin + +1. Go to **Workspace Settings > Plane Runner**. +2. Click **Activate now** under the Scripts card. +3. On the authorization screen, review the permissions Runner requires — read and write access to all workspace resources and your user profile. +4. Click **Accept**. + +Once activated, you'll see the **Scripts** and **Functions** tabs in the Plane Runner settings page. + +![Plane Runner activated](https://media.docs.plane.so/automations/activate-runner.webp#hero) + +## Building blocks + +### Scripts + +A script is a piece of executable code that runs in response to a trigger. Scripts are the primary unit of custom logic in Plane Runner. + +Each script is written in JavaScript or TypeScript, runs in an isolated sandbox, and has access to the Plane SDK, reusable functions, and environment variables. When triggered, a script receives contextual data about what caused it to run and returns a result indicating success or failure. + +Scripts can be workspace-scoped (created by your team) or system-provided (built-in, read-only templates). + +### Functions + +A function is a reusable, named piece of logic with defined parameters and return types. Functions work like utility libraries — you write them once and call them from any script. + +Functions can also be workspace-scoped or system-provided. + +**The key difference**: scripts are *triggered* by events, schedules, or transitions. Functions are *called* from within scripts. + +## How scripts are triggered + +Scripts run in response to one of three trigger types. The trigger determines what data the script receives. + +### Event-based automations + +Event-based automations run a script when something happens in your workspace — a work item's state changes, a new item is created, a label is added, and so on. The script receives the full event payload including the entity that changed, previous attributes, and the automation context. + +```typescript +export async function main( + input: AutomationEventInput, + variables: Record +) { + // input.event — the Plane event that triggered this + // input.context.automation_id — the automation rule ID + // input.context.automation_run_id — this specific run's ID + // variables — key-value pairs configured on the automation +} +``` + +### Scheduled automations (cron) + +Scheduled automations run a script on a recurring basis, such as daily or weekly. Since there's no triggering event, the script only receives variables. + +```typescript +export async function main( + variables: Record +) { + // variables — key-value pairs configured on the schedule +} +``` + +### Workflow transitions + +[Workflow transitions](/workflows-and-approvals/workflows#transition-flows) invoke scripts during state changes. They serve two purposes: + +**Pre-conditions** run before a transition to validate whether it should proceed. If the script returns an error or throws, the transition is blocked and the user sees the error message. + +**Post-operations** run after a transition completes to perform follow-up actions like notifying a channel or creating related items. + +```typescript +export async function main( + input: WorkflowTransitionEventInput, + variables: Record +) { + // input.event — the Plane event for the transition + // input.context.workflow_transition_id — the transition ID + // input.context.rule_id — the workflow rule ID + // variables — key-value pairs configured on the rule +} +``` + +## Create a script + +> **Role**: Workspace Admin + +1. Go to **Workspace Settings > Plane Runner > Scripts**. +2. Click **New Script**. +3. Enter a title for the script. +4. Select a **Script Type** from the dropdown — **Automation**, **Workflow Transition**, or **Cron Trigger**. +5. Expand the **Variables** section to define any key-value parameters the script needs. These values are provided when the script is attached to an automation rule or workflow. +6. Write your script in the code editor. The editor includes IntelliSense — type `Plane.` to see available API methods. You can also click the **Functions** button to browse available functions. +7. Click **Save**. + +### Script structure + +Every script must export an `async function main(...)`. This is the entry point that Runner calls when the script is triggered. + +```typescript +export async function main( + input: AutomationEventInput, + variables: Record +) { + const projectId = input.event.project_id; + const workItemId = input.event.entity_id; + + // Your logic here... + + return { success: true, message: "Done!" }; +} +``` + +You can use TypeScript syntax freely — type annotations, interfaces, generics, enums, `as const`, optional chaining, and more are all supported. + +### The event object + +For event-based and workflow scripts, the `input.event` object contains: + +| Field | Type | Description | +| --- | --- | --- | +| `event_id` | `string` | Unique event identifier | +| `event_type` | `string` | Type of event (e.g., `work_item.updated`) | +| `entity_type` | `string` | Entity type (e.g., `work_item`) | +| `entity_id` | `string` | ID of the affected entity | +| `project_id` | `string` | Project where the event occurred | +| `workspace_id` | `string` | Workspace ID | +| `initiator_id` | `string` | User who initiated the action | +| `timestamp` | `number` | Unix timestamp of the event | +| `payload.data` | `object` | Current state of the entity | +| `payload.previous_attributes` | `object` | Fields that changed (previous values) | + +### Variables + +Variables are key-value string pairs that parameterize your script. Define them on the script and provide values when attaching the script to an automation rule, schedule, or workflow. + +To define variables on a script, expand the **Variables** section when creating or editing: + +```json +[ + { + "key": "sourceProjectId", + "description": "Project ID to watch for changes", + "required": true + }, + { + "key": "slackWebhook", + "description": "Slack webhook URL for notifications", + "required": false + } +] +``` + +Access variables in your code: + +```typescript +export async function main( + input: AutomationEventInput, + variables: Record +) { + const projectId = variables.sourceProjectId; + const webhook = variables.slackWebhook; +} +``` + +### Returning data + +Scripts should return an object. The return value is stored in the execution record and can be inspected later. + +```typescript +// Success +return { updated: true, parentId: "abc-123" }; + +// Skipped (nothing to do) +return { skipped: true, reason: "Work item has no parent" }; +``` + +If your script throws an error, the execution is marked as `errored` and the error details are captured. + +## Create a function + +> **Role**: Workspace Admin + +1. Go to **Workspace Settings > Plane Runner > Functions**. +2. Click **New Function**. +3. Enter a name and description. +4. Select a **Category** from the dropdown — HTTP, Notifications, Data, Utils, or Custom. +5. Expand **Parameters** to define input parameters. For each parameter, specify a name, type, description, and whether it's required. +6. Enter a **Return Type** describing what the function returns (e.g., `{ success: boolean }`). +7. Write the function code in the **Function Code** editor. +8. Click **Create Function**. + +The function is now available to call from any script using `Functions.yourFunctionName({ ... })`. A usage example is auto-generated at the bottom of the form. + +### Using functions in scripts + +Functions are available via the `Functions` global. Call them by name with a single parameter object: + +```typescript +export async function main( + input: AutomationEventInput, + variables: Record +) { + // Call a system function + const siblings = await Functions.getSiblings({ + projectId: input.event.project_id, + workItemId: input.event.entity_id + }); + + // Call a custom workspace function + const result = await Functions.calculatePriority({ + severity: "high", + isBlocking: true + }); + + // Call another system function + await Functions.addComment({ + projectId: input.event.project_id, + workItemId: input.event.entity_id, + comment: `Priority set to ${result.priority}` + }); +} +``` + +Runner automatically detects which `Functions.*` calls your script makes during the build phase. Only the required functions are loaded at execution time — you don't need to import or configure anything. + +## Test a script + +You can test a script without saving it or attaching it to an automation. Expand the **Test** section at the bottom of the script editor, provide test input data and variables, and run the script. + +Testing validates the code for security violations, builds it (detecting function dependencies), executes it with the provided input, and returns the result or error details. Test executions are recorded with `trigger_type: "test"` so you can review output, timing, and errors separately from production runs. + +## Available globals + +Every script runs in a sandboxed environment with these pre-injected globals. + +### Plane SDK (`Plane`) + +A pre-initialized client for the Plane API. Use it to read and modify work items, projects, states, labels, and more. + +```typescript +// Retrieve a work item +const item = await Plane.workItems.retrieve( + workspaceSlug, projectId, workItemId +); + +// Update a work item +await Plane.workItems.update(workspaceSlug, projectId, workItemId, { + state: newStateId, + priority: "high" +}); + +// Create a work item +const newItem = await Plane.workItems.create(workspaceSlug, projectId, { + name: "Auto-created item", + description_html: "

Created by automation

", + priority: "medium" +}); + +// List states +const states = await Plane.states.list(workspaceSlug, projectId); + +// List labels +const labels = await Plane.labels.list(workspaceSlug, projectId); + +// Create a label +const label = await Plane.labels.create(workspaceSlug, projectId, { + name: "auto-tagged", + color: "#e53e3e" +}); + +// Add a comment +await Plane.workItems.comments.create( + workspaceSlug, projectId, workItemId, { + comment_html: "

Auto-comment

" + } +); + +// Create a relation +await Plane.workItems.relations.create( + workspaceSlug, projectId, workItemId, { + relation_type: "relates_to", + issues: [otherItemId] + } +); + +// Advanced search +const results = await Plane.workItems.advancedSearch(workspaceSlug, { + filters: { and: [{ parent_id: parentId }] }, + project_id: projectId +}); +``` + +The `workspaceSlug` global is also pre-injected and always available. + +### Functions library (`Functions`) + +The collection of reusable functions, both system-provided and workspace-scoped. See [Built-in system functions](#built-in-system-functions) for what's available out of the box. + +### Environment variables (`ENV`) + +A `Record` containing environment variables configured on the script. Use these for secrets, API keys, or configuration that shouldn't be hardcoded. + +```typescript +const apiKey = ENV.EXTERNAL_API_KEY; +const webhookUrl = ENV.SLACK_WEBHOOK; +``` + +### Fetch (HTTP requests) + +A sandboxed `fetch` function for making HTTP requests. It works like the standard Fetch API but is restricted to domains explicitly allowed on the script. See [Domain restrictions](#domain-restrictions). + +```typescript +const response = await fetch("https://api.example.com/data", { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${ENV.API_TOKEN}` + }, + body: JSON.stringify({ key: "value" }) +}); + +const data = await response.json(); +``` + +## Built-in system functions + +These functions are available in every workspace out of the box: + +| Function | Category | Description | +| --- | --- | --- | +| `Functions.httpRequest({ url, method?, headers?, body? })` | HTTP | Makes an HTTP request. Returns `{ status, headers, data }`. Throws on non-2xx responses. | +| `Functions.postToSlack({ webhookUrl, text?, blocks? })` | Notifications | Posts a message to a Slack incoming webhook. Supports plain text and Block Kit. | +| `Functions.getChildren({ projectId, workItemId })` | Data | Returns all child work items of a given parent. | +| `Functions.getSiblings({ projectId, workItemId })` | Data | Returns sibling work items (same parent, excluding self). | +| `Functions.addComment({ projectId, workItemId, comment })` | Data | Adds a comment to a work item. Accepts plain text or HTML. | +| `Functions.addLabel({ projectId, workItemId, labelName, color? })` | Data | Finds a label by name (or creates it) and attaches it to a work item. | + +## Built-in system scripts + +Plane ships with ready-to-use system scripts that cover common automation patterns. These are read-only templates that you can attach to automations or workflows directly. + +**Mark parent as done if all children are done** — when a work item completes, checks all siblings. If every sibling is also completed, automatically marks the parent as done. + +**Create linked work item in another project** — when a work item in a source project reaches a specified state, creates a copy in a destination project with a `relates_to` link. + +System scripts appear alongside your custom scripts in the Scripts tab. + +## Security model + +Runner executes scripts in a secure, isolated sandbox. + +### What's allowed + +Standard JavaScript globals are available: `JSON`, `Math`, `Date`, `Array`, `Object`, `String`, `Number`, `Boolean`, `RegExp`, `Map`, `Set`, `WeakMap`, `WeakSet`, `Promise`, `Symbol`, `Proxy`, `Reflect`, `URL`, `URLSearchParams`, `atob`, `btoa`, `encodeURI`, `decodeURI`, `encodeURIComponent`, `decodeURIComponent`, `setTimeout`, `clearTimeout`, `setInterval`, `clearInterval` (function callbacks only), `TextEncoder`, `TextDecoder`, and standard error types. + +Runner-specific globals are also available: `Plane`, `Functions`, `ENV`, `fetch`, `workspaceSlug`, and `console`. + +### What's blocked + +| Category | Blocked | Reason | +| --- | --- | --- | +| Module system | `require`, `module`, `exports`, `import()` | No filesystem or module access | +| Node.js APIs | `fs`, `child_process`, `http`, `net`, `os`, `path`, `cluster`, `vm`, `worker_threads` | No system access | +| Code execution | `eval()`, `Function()`, `new Function()` | Dynamic code execution risk | +| Process control | `process.exit()`, `process.kill()`, `process.env` | No process access | +| Prototype manipulation | `__proto__`, `constructor`, `prototype` | Sandbox escape prevention | +| Infinite loops | `while(true)`, `for(;;)` | Resource protection | +| Implicit eval | `setTimeout("string")`, `setInterval("string")` | Must use function callbacks | + +### Domain restrictions + +External HTTP requests via `fetch` or `Functions.httpRequest` are restricted to domains you explicitly allow. Configure the `allowed_domains` list on your script. Any fetch to a domain not on the list throws a `"Domain not allowed"` error. + +## Execution limits + +| Limit | Default | Description | +| --- | --- | --- | +| Execution timeout | 10 seconds | Maximum time a script can run | +| Initialization timeout | 5 seconds | Maximum time for script setup | +| Memory limit | 128 MB | Maximum memory per execution | + +If a script exceeds any limit, the execution is terminated and marked as `errored`. + +## Examples + +### Auto-close parent when all children are done + +When a work item moves to a completed state, check if all its siblings are also completed. If so, automatically mark the parent as done. + +```typescript +export async function main( + input: AutomationEventInput, + variables: Record +) { + const projectId = input.event.project_id; + const workItemId = input.event.entity_id; + + const item = await Plane.workItems.retrieve( + workspaceSlug, projectId, workItemId + ); + if (!item.parent) return { skipped: true, reason: "No parent" }; + + const statesResult = await Plane.states.list(workspaceSlug, projectId); + const states = statesResult.results || statesResult; + const stateGroupMap: Record = {}; + for (const s of states) { + stateGroupMap[s.id] = s.group; + } + + if (stateGroupMap[item.state] !== "completed") { + return { skipped: true, reason: "Current item not in completed state" }; + } + + const siblings = await Functions.getSiblings({ projectId, workItemId }); + const allDone = siblings.every( + (s: any) => stateGroupMap[s.state_id] === "completed" + ); + + if (!allDone) { + return { skipped: true, reason: "Not all siblings are completed" }; + } + + const completedState = states.find((s: any) => s.group === "completed"); + await Plane.workItems.update(workspaceSlug, projectId, item.parent, { + state: completedState.id, + }); + + return { updated: true, parentId: item.parent, stateId: completedState.id }; +} +``` + +**Variables**: None required. +**Functions used**: `getSiblings` + +### Create a linked work item in another project + +When a work item in a source project moves to a specific state, create a copy in a destination project and link them. + +```typescript +export async function main( + input: AutomationEventInput, + variables: Record +) { + const sourceProjectId = variables.sourceProjectId; + const sourceProjectStateName = variables.sourceProjectStateName; + const destinationProjectId = variables.destinationProjectId; + const projectId = input.event.project_id; + const workItemId = input.event.entity_id; + + if (projectId !== sourceProjectId) { + return { skipped: true, reason: "Not in source project" }; + } + + const statesResult = await Plane.states.list( + workspaceSlug, sourceProjectId + ); + const states = statesResult.results || statesResult; + const targetState = states.find( + (s: any) => s.name.toLowerCase() === sourceProjectStateName.toLowerCase() + ); + if (!targetState) { + return { skipped: true, reason: "State not found: " + sourceProjectStateName }; + } + + const item = await Plane.workItems.retrieve( + workspaceSlug, sourceProjectId, workItemId + ); + if (item.state !== targetState.id) { + return { skipped: true, reason: "Work item not in target state" }; + } + + const newItem = await Plane.workItems.create( + workspaceSlug, destinationProjectId, { + name: item.name, + description_html: item.description_html, + priority: item.priority, + } + ); + + await Plane.workItems.relations.create( + workspaceSlug, sourceProjectId, workItemId, { + relation_type: "relates_to", + issues: [newItem.id], + } + ); + + return { created: true, newWorkItemId: newItem.id, destinationProjectId }; +} +``` + +**Variables**: + +| Key | Required | Description | +| --- | --- | --- | +| `sourceProjectId` | Yes | Project ID to watch for state changes | +| `sourceProjectStateName` | Yes | State name that triggers the copy (e.g., "Ready for QA") | +| `destinationProjectId` | Yes | Project ID where the new work item is created | + +### Post to Slack on state change + +Notify a Slack channel whenever a work item moves to "In Review". + +```typescript +export async function main( + input: AutomationEventInput, + variables: Record +) { + const projectId = input.event.project_id; + const workItemId = input.event.entity_id; + + const item = await Plane.workItems.retrieve( + workspaceSlug, projectId, workItemId + ); + + const statesResult = await Plane.states.list(workspaceSlug, projectId); + const states = statesResult.results || statesResult; + const currentState = states.find((s: any) => s.id === item.state); + + if (currentState?.name !== "In Review") { + return { skipped: true, reason: "Not in 'In Review' state" }; + } + + await Functions.postToSlack({ + webhookUrl: variables.slackWebhook, + text: `Work item "${item.name}" is now In Review\nProject: ${projectId}` + }); + + return { notified: true, itemName: item.name }; +} +``` + +**Variables**: + +| Key | Required | Description | +| --- | --- | --- | +| `slackWebhook` | Yes | Slack incoming webhook URL | + +**Allowed domains**: `hooks.slack.com` + +### Enforce a workflow pre-condition + +Block a transition from "To Do" to "In Progress" unless the work item has an assignee and an estimate. + +```typescript +export async function main( + input: WorkflowTransitionEventInput, + variables: Record +) { + const projectId = input.event.project_id; + const workItemId = input.event.entity_id; + + const item = await Plane.workItems.retrieve( + workspaceSlug, projectId, workItemId + ); + + const errors: string[] = []; + + if (!item.assignees || item.assignees.length === 0) { + errors.push("Work item must have at least one assignee"); + } + + if (!item.estimate_point) { + errors.push("Work item must have an estimate"); + } + + if (errors.length > 0) { + throw new Error("Transition blocked: " + errors.join("; ")); + } + + return { allowed: true }; +} +``` + +Attach this script as a **pre-condition** on the "To Do → In Progress" workflow transition. If the script throws, the transition is blocked and the user sees the error message. \ No newline at end of file From f42925358ffc69ac8e836961bddfc6eca2fcd071 Mon Sep 17 00:00:00 2001 From: danciaclara Date: Sun, 12 Apr 2026 19:54:38 +0530 Subject: [PATCH 2/5] formatting fixes --- docs/.vitepress/config.ts | 2 +- docs/automations/plane-runner.md | 227 ++++++++++++------------------- 2 files changed, 89 insertions(+), 140 deletions(-) diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index c1a48d2..3fea191 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -480,7 +480,7 @@ export default defineConfig({ text: "Automations", link: "/automations/custom-automations", }, - { + { text: "Plane Runner", link: "/automations/plane-runner", }, diff --git a/docs/automations/plane-runner.md b/docs/automations/plane-runner.md index 40d00cb..aebb625 100644 --- a/docs/automations/plane-runner.md +++ b/docs/automations/plane-runner.md @@ -38,7 +38,7 @@ A function is a reusable, named piece of logic with defined parameters and retur Functions can also be workspace-scoped or system-provided. -**The key difference**: scripts are *triggered* by events, schedules, or transitions. Functions are *called* from within scripts. +**The key difference**: scripts are _triggered_ by events, schedules, or transitions. Functions are _called_ from within scripts. ## How scripts are triggered @@ -49,10 +49,7 @@ Scripts run in response to one of three trigger types. The trigger determines wh Event-based automations run a script when something happens in your workspace — a work item's state changes, a new item is created, a label is added, and so on. The script receives the full event payload including the entity that changed, previous attributes, and the automation context. ```typescript -export async function main( - input: AutomationEventInput, - variables: Record -) { +export async function main(input: AutomationEventInput, variables: Record) { // input.event — the Plane event that triggered this // input.context.automation_id — the automation rule ID // input.context.automation_run_id — this specific run's ID @@ -65,9 +62,7 @@ export async function main( Scheduled automations run a script on a recurring basis, such as daily or weekly. Since there's no triggering event, the script only receives variables. ```typescript -export async function main( - variables: Record -) { +export async function main(variables: Record) { // variables — key-value pairs configured on the schedule } ``` @@ -81,10 +76,7 @@ export async function main( **Post-operations** run after a transition completes to perform follow-up actions like notifying a channel or creating related items. ```typescript -export async function main( - input: WorkflowTransitionEventInput, - variables: Record -) { +export async function main(input: WorkflowTransitionEventInput, variables: Record) { // input.event — the Plane event for the transition // input.context.workflow_transition_id — the transition ID // input.context.rule_id — the workflow rule ID @@ -109,10 +101,7 @@ export async function main( Every script must export an `async function main(...)`. This is the entry point that Runner calls when the script is triggered. ```typescript -export async function main( - input: AutomationEventInput, - variables: Record -) { +export async function main(input: AutomationEventInput, variables: Record) { const projectId = input.event.project_id; const workItemId = input.event.entity_id; @@ -128,18 +117,18 @@ You can use TypeScript syntax freely — type annotations, interfaces, generics, For event-based and workflow scripts, the `input.event` object contains: -| Field | Type | Description | -| --- | --- | --- | -| `event_id` | `string` | Unique event identifier | -| `event_type` | `string` | Type of event (e.g., `work_item.updated`) | -| `entity_type` | `string` | Entity type (e.g., `work_item`) | -| `entity_id` | `string` | ID of the affected entity | -| `project_id` | `string` | Project where the event occurred | -| `workspace_id` | `string` | Workspace ID | -| `initiator_id` | `string` | User who initiated the action | -| `timestamp` | `number` | Unix timestamp of the event | -| `payload.data` | `object` | Current state of the entity | -| `payload.previous_attributes` | `object` | Fields that changed (previous values) | +| Field | Type | Description | +| ----------------------------- | -------- | ----------------------------------------- | +| `event_id` | `string` | Unique event identifier | +| `event_type` | `string` | Type of event (e.g., `work_item.updated`) | +| `entity_type` | `string` | Entity type (e.g., `work_item`) | +| `entity_id` | `string` | ID of the affected entity | +| `project_id` | `string` | Project where the event occurred | +| `workspace_id` | `string` | Workspace ID | +| `initiator_id` | `string` | User who initiated the action | +| `timestamp` | `number` | Unix timestamp of the event | +| `payload.data` | `object` | Current state of the entity | +| `payload.previous_attributes` | `object` | Fields that changed (previous values) | ### Variables @@ -165,10 +154,7 @@ To define variables on a script, expand the **Variables** section when creating Access variables in your code: ```typescript -export async function main( - input: AutomationEventInput, - variables: Record -) { +export async function main(input: AutomationEventInput, variables: Record) { const projectId = variables.sourceProjectId; const webhook = variables.slackWebhook; } @@ -208,27 +194,24 @@ The function is now available to call from any script using `Functions.yourFunct Functions are available via the `Functions` global. Call them by name with a single parameter object: ```typescript -export async function main( - input: AutomationEventInput, - variables: Record -) { +export async function main(input: AutomationEventInput, variables: Record) { // Call a system function const siblings = await Functions.getSiblings({ projectId: input.event.project_id, - workItemId: input.event.entity_id + workItemId: input.event.entity_id, }); // Call a custom workspace function const result = await Functions.calculatePriority({ severity: "high", - isBlocking: true + isBlocking: true, }); // Call another system function await Functions.addComment({ projectId: input.event.project_id, workItemId: input.event.entity_id, - comment: `Priority set to ${result.priority}` + comment: `Priority set to ${result.priority}`, }); } ``` @@ -251,21 +234,19 @@ A pre-initialized client for the Plane API. Use it to read and modify work items ```typescript // Retrieve a work item -const item = await Plane.workItems.retrieve( - workspaceSlug, projectId, workItemId -); +const item = await Plane.workItems.retrieve(workspaceSlug, projectId, workItemId); // Update a work item await Plane.workItems.update(workspaceSlug, projectId, workItemId, { state: newStateId, - priority: "high" + priority: "high", }); // Create a work item const newItem = await Plane.workItems.create(workspaceSlug, projectId, { name: "Auto-created item", description_html: "

Created by automation

", - priority: "medium" + priority: "medium", }); // List states @@ -277,28 +258,24 @@ const labels = await Plane.labels.list(workspaceSlug, projectId); // Create a label const label = await Plane.labels.create(workspaceSlug, projectId, { name: "auto-tagged", - color: "#e53e3e" + color: "#e53e3e", }); // Add a comment -await Plane.workItems.comments.create( - workspaceSlug, projectId, workItemId, { - comment_html: "

Auto-comment

" - } -); +await Plane.workItems.comments.create(workspaceSlug, projectId, workItemId, { + comment_html: "

Auto-comment

", +}); // Create a relation -await Plane.workItems.relations.create( - workspaceSlug, projectId, workItemId, { - relation_type: "relates_to", - issues: [otherItemId] - } -); +await Plane.workItems.relations.create(workspaceSlug, projectId, workItemId, { + relation_type: "relates_to", + issues: [otherItemId], +}); // Advanced search const results = await Plane.workItems.advancedSearch(workspaceSlug, { filters: { and: [{ parent_id: parentId }] }, - project_id: projectId + project_id: projectId, }); ``` @@ -326,9 +303,9 @@ const response = await fetch("https://api.example.com/data", { method: "POST", headers: { "Content-Type": "application/json", - "Authorization": `Bearer ${ENV.API_TOKEN}` + Authorization: `Bearer ${ENV.API_TOKEN}`, }, - body: JSON.stringify({ key: "value" }) + body: JSON.stringify({ key: "value" }), }); const data = await response.json(); @@ -338,14 +315,14 @@ const data = await response.json(); These functions are available in every workspace out of the box: -| Function | Category | Description | -| --- | --- | --- | -| `Functions.httpRequest({ url, method?, headers?, body? })` | HTTP | Makes an HTTP request. Returns `{ status, headers, data }`. Throws on non-2xx responses. | -| `Functions.postToSlack({ webhookUrl, text?, blocks? })` | Notifications | Posts a message to a Slack incoming webhook. Supports plain text and Block Kit. | -| `Functions.getChildren({ projectId, workItemId })` | Data | Returns all child work items of a given parent. | -| `Functions.getSiblings({ projectId, workItemId })` | Data | Returns sibling work items (same parent, excluding self). | -| `Functions.addComment({ projectId, workItemId, comment })` | Data | Adds a comment to a work item. Accepts plain text or HTML. | -| `Functions.addLabel({ projectId, workItemId, labelName, color? })` | Data | Finds a label by name (or creates it) and attaches it to a work item. | +| Function | Category | Description | +| ------------------------------------------------------------------ | ------------- | ---------------------------------------------------------------------------------------- | +| `Functions.httpRequest({ url, method?, headers?, body? })` | HTTP | Makes an HTTP request. Returns `{ status, headers, data }`. Throws on non-2xx responses. | +| `Functions.postToSlack({ webhookUrl, text?, blocks? })` | Notifications | Posts a message to a Slack incoming webhook. Supports plain text and Block Kit. | +| `Functions.getChildren({ projectId, workItemId })` | Data | Returns all child work items of a given parent. | +| `Functions.getSiblings({ projectId, workItemId })` | Data | Returns sibling work items (same parent, excluding self). | +| `Functions.addComment({ projectId, workItemId, comment })` | Data | Adds a comment to a work item. Accepts plain text or HTML. | +| `Functions.addLabel({ projectId, workItemId, labelName, color? })` | Data | Finds a label by name (or creates it) and attaches it to a work item. | ## Built-in system scripts @@ -369,15 +346,15 @@ Runner-specific globals are also available: `Plane`, `Functions`, `ENV`, `fetch` ### What's blocked -| Category | Blocked | Reason | -| --- | --- | --- | -| Module system | `require`, `module`, `exports`, `import()` | No filesystem or module access | -| Node.js APIs | `fs`, `child_process`, `http`, `net`, `os`, `path`, `cluster`, `vm`, `worker_threads` | No system access | -| Code execution | `eval()`, `Function()`, `new Function()` | Dynamic code execution risk | -| Process control | `process.exit()`, `process.kill()`, `process.env` | No process access | -| Prototype manipulation | `__proto__`, `constructor`, `prototype` | Sandbox escape prevention | -| Infinite loops | `while(true)`, `for(;;)` | Resource protection | -| Implicit eval | `setTimeout("string")`, `setInterval("string")` | Must use function callbacks | +| Category | Blocked | Reason | +| ---------------------- | ------------------------------------------------------------------------------------- | ------------------------------ | +| Module system | `require`, `module`, `exports`, `import()` | No filesystem or module access | +| Node.js APIs | `fs`, `child_process`, `http`, `net`, `os`, `path`, `cluster`, `vm`, `worker_threads` | No system access | +| Code execution | `eval()`, `Function()`, `new Function()` | Dynamic code execution risk | +| Process control | `process.exit()`, `process.kill()`, `process.env` | No process access | +| Prototype manipulation | `__proto__`, `constructor`, `prototype` | Sandbox escape prevention | +| Infinite loops | `while(true)`, `for(;;)` | Resource protection | +| Implicit eval | `setTimeout("string")`, `setInterval("string")` | Must use function callbacks | ### Domain restrictions @@ -385,11 +362,11 @@ External HTTP requests via `fetch` or `Functions.httpRequest` are restricted to ## Execution limits -| Limit | Default | Description | -| --- | --- | --- | -| Execution timeout | 10 seconds | Maximum time a script can run | -| Initialization timeout | 5 seconds | Maximum time for script setup | -| Memory limit | 128 MB | Maximum memory per execution | +| Limit | Default | Description | +| ---------------------- | ---------- | ----------------------------- | +| Execution timeout | 10 seconds | Maximum time a script can run | +| Initialization timeout | 5 seconds | Maximum time for script setup | +| Memory limit | 128 MB | Maximum memory per execution | If a script exceeds any limit, the execution is terminated and marked as `errored`. @@ -400,16 +377,11 @@ If a script exceeds any limit, the execution is terminated and marked as `errore When a work item moves to a completed state, check if all its siblings are also completed. If so, automatically mark the parent as done. ```typescript -export async function main( - input: AutomationEventInput, - variables: Record -) { +export async function main(input: AutomationEventInput, variables: Record) { const projectId = input.event.project_id; const workItemId = input.event.entity_id; - const item = await Plane.workItems.retrieve( - workspaceSlug, projectId, workItemId - ); + const item = await Plane.workItems.retrieve(workspaceSlug, projectId, workItemId); if (!item.parent) return { skipped: true, reason: "No parent" }; const statesResult = await Plane.states.list(workspaceSlug, projectId); @@ -424,9 +396,7 @@ export async function main( } const siblings = await Functions.getSiblings({ projectId, workItemId }); - const allDone = siblings.every( - (s: any) => stateGroupMap[s.state_id] === "completed" - ); + const allDone = siblings.every((s: any) => stateGroupMap[s.state_id] === "completed"); if (!allDone) { return { skipped: true, reason: "Not all siblings are completed" }; @@ -449,10 +419,7 @@ export async function main( When a work item in a source project moves to a specific state, create a copy in a destination project and link them. ```typescript -export async function main( - input: AutomationEventInput, - variables: Record -) { +export async function main(input: AutomationEventInput, variables: Record) { const sourceProjectId = variables.sourceProjectId; const sourceProjectStateName = variables.sourceProjectStateName; const destinationProjectId = variables.destinationProjectId; @@ -463,38 +430,30 @@ export async function main( return { skipped: true, reason: "Not in source project" }; } - const statesResult = await Plane.states.list( - workspaceSlug, sourceProjectId - ); + const statesResult = await Plane.states.list(workspaceSlug, sourceProjectId); const states = statesResult.results || statesResult; const targetState = states.find( - (s: any) => s.name.toLowerCase() === sourceProjectStateName.toLowerCase() + (s: any) => s.name.toLowerCase() === sourceProjectStateName.toLowerCase(), ); if (!targetState) { return { skipped: true, reason: "State not found: " + sourceProjectStateName }; } - const item = await Plane.workItems.retrieve( - workspaceSlug, sourceProjectId, workItemId - ); + const item = await Plane.workItems.retrieve(workspaceSlug, sourceProjectId, workItemId); if (item.state !== targetState.id) { return { skipped: true, reason: "Work item not in target state" }; } - const newItem = await Plane.workItems.create( - workspaceSlug, destinationProjectId, { - name: item.name, - description_html: item.description_html, - priority: item.priority, - } - ); + const newItem = await Plane.workItems.create(workspaceSlug, destinationProjectId, { + name: item.name, + description_html: item.description_html, + priority: item.priority, + }); - await Plane.workItems.relations.create( - workspaceSlug, sourceProjectId, workItemId, { - relation_type: "relates_to", - issues: [newItem.id], - } - ); + await Plane.workItems.relations.create(workspaceSlug, sourceProjectId, workItemId, { + relation_type: "relates_to", + issues: [newItem.id], + }); return { created: true, newWorkItemId: newItem.id, destinationProjectId }; } @@ -502,27 +461,22 @@ export async function main( **Variables**: -| Key | Required | Description | -| --- | --- | --- | -| `sourceProjectId` | Yes | Project ID to watch for state changes | -| `sourceProjectStateName` | Yes | State name that triggers the copy (e.g., "Ready for QA") | -| `destinationProjectId` | Yes | Project ID where the new work item is created | +| Key | Required | Description | +| ------------------------ | -------- | -------------------------------------------------------- | +| `sourceProjectId` | Yes | Project ID to watch for state changes | +| `sourceProjectStateName` | Yes | State name that triggers the copy (e.g., "Ready for QA") | +| `destinationProjectId` | Yes | Project ID where the new work item is created | ### Post to Slack on state change Notify a Slack channel whenever a work item moves to "In Review". ```typescript -export async function main( - input: AutomationEventInput, - variables: Record -) { +export async function main(input: AutomationEventInput, variables: Record) { const projectId = input.event.project_id; const workItemId = input.event.entity_id; - const item = await Plane.workItems.retrieve( - workspaceSlug, projectId, workItemId - ); + const item = await Plane.workItems.retrieve(workspaceSlug, projectId, workItemId); const statesResult = await Plane.states.list(workspaceSlug, projectId); const states = statesResult.results || statesResult; @@ -534,7 +488,7 @@ export async function main( await Functions.postToSlack({ webhookUrl: variables.slackWebhook, - text: `Work item "${item.name}" is now In Review\nProject: ${projectId}` + text: `Work item "${item.name}" is now In Review\nProject: ${projectId}`, }); return { notified: true, itemName: item.name }; @@ -543,9 +497,9 @@ export async function main( **Variables**: -| Key | Required | Description | -| --- | --- | --- | -| `slackWebhook` | Yes | Slack incoming webhook URL | +| Key | Required | Description | +| -------------- | -------- | -------------------------- | +| `slackWebhook` | Yes | Slack incoming webhook URL | **Allowed domains**: `hooks.slack.com` @@ -554,16 +508,11 @@ export async function main( Block a transition from "To Do" to "In Progress" unless the work item has an assignee and an estimate. ```typescript -export async function main( - input: WorkflowTransitionEventInput, - variables: Record -) { +export async function main(input: WorkflowTransitionEventInput, variables: Record) { const projectId = input.event.project_id; const workItemId = input.event.entity_id; - const item = await Plane.workItems.retrieve( - workspaceSlug, projectId, workItemId - ); + const item = await Plane.workItems.retrieve(workspaceSlug, projectId, workItemId); const errors: string[] = []; @@ -583,4 +532,4 @@ export async function main( } ``` -Attach this script as a **pre-condition** on the "To Do → In Progress" workflow transition. If the script throws, the transition is blocked and the user sees the error message. \ No newline at end of file +Attach this script as a **pre-condition** on the "To Do → In Progress" workflow transition. If the script throws, the transition is blocked and the user sees the error message. From ac227770aa3b4c3226ff42118d805d5ecd6e803b Mon Sep 17 00:00:00 2001 From: danciaclara Date: Wed, 22 Apr 2026 15:23:01 +0530 Subject: [PATCH 3/5] Added pre and post workflow conditions --- docs/automations/plane-runner.md | 16 +++++-- docs/workflows-and-approvals/workflows.md | 57 +++++++++++++++++++++++ 2 files changed, 70 insertions(+), 3 deletions(-) diff --git a/docs/automations/plane-runner.md b/docs/automations/plane-runner.md index aebb625..50f8873 100644 --- a/docs/automations/plane-runner.md +++ b/docs/automations/plane-runner.md @@ -69,11 +69,11 @@ export async function main(variables: Record) { ### Workflow transitions -[Workflow transitions](/workflows-and-approvals/workflows#transition-flows) invoke scripts during state changes. They serve two purposes: +Run a script during state changes defined in [Workflows](/workflows-and-approvals/workflows). Workflow transition scripts serve two purposes: -**Pre-conditions** run before a transition to validate whether it should proceed. If the script returns an error or throws, the transition is blocked and the user sees the error message. +**Pre-validation** scripts run before a transition to check whether it should proceed. If the script returns an error or throws, the transition is blocked and the user sees the error message. These are attached to the **Conditions > Pre validation** section of a workflow transition flow. -**Post-operations** run after a transition completes to perform follow-up actions like notifying a channel or creating related items. +**Post-action** scripts run after a transition completes to perform follow-up work like notifying a channel, creating related items, or updating properties. These are attached to the **Conditions > Post actions** section. ```typescript export async function main(input: WorkflowTransitionEventInput, variables: Record) { @@ -84,6 +84,16 @@ export async function main(input: WorkflowTransitionEventInput, variables: Recor } ``` +Pre-validation scripts follow these return rules: + +- `return { success: true }` — allow the transition to proceed. +- `return { success: false }` — block the transition. +- `throw new Error("reason")` — block the transition with a specific message shown to the user. + +Post-action scripts don't block transitions. If they fail, the transition still stands and the error is logged. + +For details on attaching scripts to workflow transitions, see [Transition conditions](/workflows-and-approvals/workflows#transition-conditions). + ## Create a script > **Role**: Workspace Admin diff --git a/docs/workflows-and-approvals/workflows.md b/docs/workflows-and-approvals/workflows.md index c4dfb08..8cd0a4c 100644 --- a/docs/workflows-and-approvals/workflows.md +++ b/docs/workflows-and-approvals/workflows.md @@ -69,6 +69,7 @@ A transition flow defines a permitted state change. When you add one, you config - **via** - shows **Transition** to indicate the flow type. - **move to** - the destination state. Click to select from available project states on the right pane. - **by** - who can make this transition. Defaults to **All**, meaning any project member. To restrict it, open the Members panel and select specific individuals. +- **with** - optional conditions that run before or after the transition. See [Transition conditions](#transition-conditions) below. ![Transition flow](https://media.docs.plane.so/workflows/transition-flow.webp#hero) @@ -82,11 +83,67 @@ An approval flow adds a gate: the work item won't move forward until designated - **on approve, move to** - the state the item moves to when approved. - **on reject, move to** - the state the item falls back to when rejected. - **by** - who can approve or reject. Defaults to **All**, but you can restrict it to specific members. +- **with** — optional conditions that run before or after the approval decision. See [Transition conditions](#transition-conditions) below. ![Approval flow](https://media.docs.plane.so/workflows/approval-flow.webp#hero) For example, you might add an approval flow on "Testing" so that moving to "Ready for Release" requires sign-off from a QA lead. If rejected, the item moves back to "In Development" for further work. +## Transition conditions + +Transition conditions let you attach custom logic to any transition or approval flow. They use [Plane Runner](/automations/plane-runner) scripts to validate whether a transition should proceed and to perform follow-up actions after it completes. + +When you click the **Conditions** option in the "with" column of a flow, a panel opens on the right with two sections: + +### Pre-validation + +Pre-validation scripts run **before** a transition happens. They verify that the work item is ready to move — for example, checking that all required fields are filled, that an estimate exists, or that a linked document has been approved. + +If a pre-validation script returns `{ success: false }` or throws an error, the transition is blocked and the user sees an error message explaining why. If all pre-validation scripts return `{ success: true }`, the transition proceeds. + +You can chain multiple pre-validation scripts — they run in the numbered order shown (#1, #2, etc.). All must pass for the transition to proceed. + +**To add a pre-validation script:** + +1. In the Conditions panel, under **Pre validation**, click **+ Select script**. +2. Choose an existing Runner script of type "Workflow Transition" from the list. +3. Or click **New Script** to create one inline — the script editor opens with the type pre-set to "Workflow Transition" and a template showing the return rules: + - `return { success: true }` — allow the transition. + - `return { success: false }` — block the transition. + - `throw new Error("reason")` — block the transition with a specific message shown to the user. +4. Click **Save and use** to save the script and attach it to the condition in one step. +5. Click **+ Add more** to chain additional pre-validation scripts. + +### Post actions + +Post-action scripts run **after** a transition completes successfully. They handle follow-up work — posting a Slack notification, creating a linked work item in another project, adding a comment, updating a custom property, or calling an external API. + +Post-action scripts don't block the transition (it has already happened). If a post-action script fails, the transition still stands, but the error is logged in the script's execution history. + +**To add a post-action script:** + +1. Under **Post actions**, click **+ Select script**. +2. Choose an existing Runner script or create one inline. +3. Click **+ Add more** to chain additional post-action scripts. + +### Example: enforcing quality gates + +A common pattern is combining pre-validation and post-actions on a single transition: + +**Transition: "In Development" → "In Review"** + +Pre-validation: +- Script #1: Verify the work item has at least one assignee and an estimate. +- Script #2: Verify all sub-work items are completed. + +Post actions: +- Script #1: Post a message to Slack notifying the review channel. +- Script #2: Add a "Ready for Review" label to the work item. + +If either pre-validation script fails, the developer sees an error message and the work item stays in "In Development." If both pass, the transition happens and the post-action scripts fire in sequence. + +For details on writing Runner scripts, including the full SDK reference, available globals, and security model, see [Plane Runner](/automations/plane-runner). + ### Activate the workflow Once you've defined all the states and flows you need, toggle the workflow on from the Workflows settings page to make it active. You can toggle it off at any time to pause enforcement without losing your configuration. From a7886f21eca823d68e279bf303f7e99e02da1629 Mon Sep 17 00:00:00 2001 From: danciaclara Date: Wed, 22 Apr 2026 15:24:37 +0530 Subject: [PATCH 4/5] formatting fixes --- docs/workflows-and-approvals/workflows.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/workflows-and-approvals/workflows.md b/docs/workflows-and-approvals/workflows.md index 8cd0a4c..a15cead 100644 --- a/docs/workflows-and-approvals/workflows.md +++ b/docs/workflows-and-approvals/workflows.md @@ -133,10 +133,12 @@ A common pattern is combining pre-validation and post-actions on a single transi **Transition: "In Development" → "In Review"** Pre-validation: + - Script #1: Verify the work item has at least one assignee and an estimate. - Script #2: Verify all sub-work items are completed. Post actions: + - Script #1: Post a message to Slack notifying the review channel. - Script #2: Add a "Ready for Review" label to the work item. From d189108a988eb9b5e8a97ee2be64356b18d65d9b Mon Sep 17 00:00:00 2001 From: danciaclara Date: Wed, 22 Apr 2026 15:40:13 +0530 Subject: [PATCH 5/5] Added script in Automations page --- docs/automations/custom-automations.md | 86 ++++++++++++-------------- 1 file changed, 41 insertions(+), 45 deletions(-) diff --git a/docs/automations/custom-automations.md b/docs/automations/custom-automations.md index 861763b..b34c3a1 100644 --- a/docs/automations/custom-automations.md +++ b/docs/automations/custom-automations.md @@ -5,65 +5,65 @@ description: Automate repetitive project tasks with trigger-based workflows. Set # Custom automations -Automations let you streamline your project management workflow by automatically performing actions based on specific triggers and conditions. This powerful feature eliminates repetitive manual tasks, ensures consistency in your processes, and helps your team maintain focus on high-value work by letting the system handle routine operations. +Custom automations let you streamline your project management workflow by automatically performing actions based on specific triggers and conditions. This powerful feature eliminates repetitive manual tasks, ensures consistency in your processes, and helps your team maintain focus on high-value work by letting the system handle routine operations. Think of automations as your digital assistant that watches for specific events in your projects and responds according to rules you define. Whether it's updating work item statuses, assigning team members, adding labels, or posting comments, automations work behind the scenes to keep your projects moving smoothly. ![Create automations](https://media.docs.plane.so/automations/create-automation.webp#hero) -## What automations do +For built-in automations, see [Automations](/automations/overview). + +## How custom automations work Automations follow a simple but powerful logic: When [trigger] happens, if [conditions] are met, then perform [actions]. This trigger-condition-action framework allows you to create sophisticated workflows that adapt to your team's specific needs. -### Key components +Every custom automation has three components. + +**Triggers** are events that start the automation. A single event fires the automation, which then evaluates conditions before executing actions. -- **Triggers** - Events that start the automation (work item created, updated, state changed, assignee changed, comment created) +| Trigger | Fires when… | +| ----------------- | ----------------------------------------------- | +| Work item created | A new work item is added to the project | +| Work item updated | Any property on a work item changes | +| State changed | A work item moves to a different workflow state | +| Assignee changed | A work item's assignee is added or removed | +| Comment created | Someone adds a comment to a work item | -- **Conditions** - Optional filters that must be met for the automation to proceed (specific state, type, label, assignee, creator, or priority) +**Conditions** are optional filters that must be satisfied for the automation to proceed. If you add multiple conditions, all of them must be met (AND logic). -- **Actions** - What the automation does when triggered (add comments, change properties) +| Condition | Filters on… | +| ---------- | ------------------------------------ | +| State | A specific workflow state | +| Type | A specific work item type | +| Label | One or more project labels | +| Assignees | Specific team members | +| Created by | The person who created the work item | +| Priority | A specific priority level | -You can create complex workflows by adding multiple conditions and multiple actions to a single trigger, giving you fine-grained control over when and how automations execute. +**Actions** are what the automation does when fired. Multiple actions execute in sequence. + +| Action | What it does | +| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Add comment | Posts an automatic comment on the work item | +| Change property | Updates a work item property: state, priority, assignee, labels, start date, or due date | +| Run Script | Executes a [Plane Runner](/automations/plane-runner) script, giving you full programmatic control — call external APIs, create linked work items, enforce custom business logic, or anything else you can write in JavaScript or TypeScript | ## Set up automations ![Configure trigger and action](https://media.docs.plane.so/automations/configure-trigger-and-action.webp#hero) -1. Navigate to your Project Settings. - -2. Select **Automations** on the left pane. -3. Click **Create automation** to start building your workflow. - - Give your automation a descriptive name and description. - - Save the configuration. -4. Click **Add trigger** in the trigger section. -5. Choose from available trigger types: - - Work item created - - Work item updated - - State changed - - Assignee changed - - Comment created - -6. Click **Add condition** to specify when the automation should run. Select condition types such as: - - State (specific workflow status) - - Type (work item type) - - Label (project tags) - - Assignees (specific team members) - - Created by - - Priority - -7. Click **Add action** to specify what the automation should do. Choose from available actions: - - Add comment - - Change property (State, Priority, Assignee, Labels, Start Date, Due Date) +1. Navigate to **Project Settings → Automations**. +2. In the **Custom automations** section, click **Create automation**. +3. Give your automation a descriptive name and an optional description, then save. +4. Click **Add trigger** and choose the event that should start the automation. +5. Optionally click **Add condition** to narrow when the automation runs. You can add multiple conditions — the automation only fires when all conditions are met. +6. Click **Add action** to define what happens. Choose from **Add comment**, **Change property**, or **Run Script** . You can add multiple actions — they execute in sequence. +7. Click **Enable** in the top right corner to activate the automation. ::: tip You can add multiple conditions to create more specific rules and multiple actions to perform several operations in sequence on a single trigger. ::: -8. Click **Enable** on the top right corner of the screen to to start turn on the automation on your project. - ## Manage automations ![Manage automations](https://media.docs.plane.so/automations/manage-automations.webp#hero) @@ -95,17 +95,13 @@ Toggle automations on or off as needed without deleting them. This is useful for ## Common use cases -- **State management** - Automatically transition work items between workflow states and update status when specific conditions are met. +**State management.** Automatically transition work items between workflow states when conditions are met — for example, move a work item to "In Review" when an assignee is added, or to "Done" when all sub-items are completed. -- **Team assignment and handoffs** - Automatically assign team members when work items reach specific stages, reassign work based on type or priority, and ensure proper handoffs between different teams or departments. +**Team assignment and handoffs.** Auto-assign team members when work items reach specific stages. Reassign work based on type or priority to ensure proper handoffs between teams. -- **Priority and categorization** - Auto-adjust work item priorities based on labels or assignees, apply consistent labeling across similar work item types, and maintain project organization through automated property updates. +**Priority and categorization.** Auto-adjust priorities based on labels or assignees. Apply consistent labeling across similar work item types to keep projects organized. -- **Communication and notifications** - Post automatic comments when work items are assigned or reassigned, add context when work items move between stages, and ensure stakeholders receive updates on critical changes. +**Communication.** Post automatic comments when work items are assigned, reassigned, or move between stages. This keeps stakeholders informed without requiring manual updates. ---