Skip to main content

Creating a Slack Workflow for integrartion with Plane.so [Part:2]

· 8 min read
Shubhasai Mohapatra

In this part we will understand how we can connect slack and https://plane.so to automate the creation of issues.

Before starting this blog, if you are not familiar with slack or Plane.so can checkout my this blog or can google it.

No more beating arround bush. Lets start ...

note

Assuming You already have a Slack Workspace and already you have set up your plane.so or deployed it (self hosting)

If you want to know about self hosting your plane.so can check out there documentation.

Setting up the Slack Bot Project:

There are many sample templates for slack bots here. You can pick any template according to your use case or start with a blank deno template

Cloning the project

    # Clone this project onto your machine
$ slack create my-app -t slack-samples/deno-issue-submission
# Change into the project directory
$ cd my-app

Running the Project Locally

    # Run app locally
$ slack run

Deploying the Bot

    $ slack deploy

Configuring Bot and Permissions:

For calling some external url (your plane.so instance url) you must add the url as outgoingDomains in the Manifest file manifest.ts.

    name: "NameOfYourBot",
description: "A basic sample that demonstrates issue submission to channel",
icon: "assets/yourBotIcon.png",
workflows: [YourWorkFlow],
functions: [YourFunction],
outgoingDomains: ["your_domain"],
botScopes: ["commands", "chat:write", "chat:write.public"],

Managing Trigger, Workflow and functions

Workflow and Function:

  • Initialising the workflow
    const SubmitIssueWorkflow = DefineWorkflow({
callback_id: "callback_id",
title: "Name",
description: "Submit an issue to Plane.so using user input",
input_parameters: {
properties: {
interactivity: {
type: Schema.slack.types.interactivity,
},
channel: {
type: Schema.slack.types.channel_id,
},
},
required: ["channel", "interactivity"],
},
});
  • Adding Steps to Workflow:
    • Creating a form to take input of issue details from the user
    const inputForm = SubmitIssueWorkflow.addStep(
    Schema.slack.functions.OpenForm,
    {
    title: "Submit an issue",
    interactivity: SubmitIssueWorkflow.inputs.interactivity,
    submit_label: "Submit",
    fields: {
    elements: [
    {
    name: "name",
    title: "Issue Name",
    type: Schema.types.string,
    },
    {
    name: "details",
    title: "Issue Details",
    type: Schema.types.string,
    long: true,
    },
    // if you have multiple projects
    {
    name: "project",
    title: "Select Project",
    type: Schema.types.string,
    enum: ["Mobile", "Web SDK", "Dashboard", "Backend"],
    },
    // if you have multiple modules
    {
    name: "module",
    title: "Select Module",
    type: Schema.types.array,
    items: {
    type: Schema.types.string,
    enum: [
    "BUGS",
    "ANDROID",
    "iOS",
    "FLUTTER",
    "REACTNATIVE",
    "DEVOPS",
    ],
    },
    default: ["NONE"],
    },
    // Assigning Issue to people
    {
    name: "people",
    title: "Assign Issue",
    type: Schema.types.array,
    items: {
    type: Schema.types.string,
    enum: ["Person1", "Person2", "Person3"],
    },
    default: ["NONE"],
    },
    // priority of issue
    {
    name: "priority",
    title: "Select Priority",
    type: Schema.types.string,
    enum: ["none", "urgent", "high", "low"],
    },
    ],
    required: ["name", "details", "project", "priority"],
    },
    },
    );

Prerequisites for Plane.so Api Integration:

Before reading this head over to plane.so documenataion here.

Have you API-KEY in hand before integrating the apis. To get your API-KEY just head to you Plane.so dashboard. In your workspace setting you will get an option to add and API-KEY.

note

If you are using the selfhosted version of the plane, make sure you have the Admin Access else you cant generate the Api Key. This is not a Organization level API-KEY. So all the api call that will happen using this key will executed on the behalf of the user who generated the Key.

Understanding Endpoints:

  • Creating an Issue
  url --request POST \
--url https://api.plane.so/api/v1/workspaces/{workspace-slug}/projects/{project_id}/issues/ \
--header 'Content-Type: application/json' \
--header 'x-api-key: <api-key>' \
--data '{
"name": "<string>"
}'
  • Adding the Issue to Module
  curl --request POST \
--url https://api.plane.so/api/v1/workspaces/{workspace-slug}/projects/{project_id}/modules/{module_id}/module-issues/ \
--header 'Content-Type: application/json' \
--header 'x-api-key: <api-key>' \
--data '{
"issues": [
"<string>"
]
}'

issues accepts array of issue id. In the response of Creating an Issue you will get an issue id. Send that issue id in a array for the issues field.

note

If you have selfhosted Plane.so make sure you replace api.plane.so/ with the url of your instance.

Defining an Function to execute the Api Request

  • Accept the input parameter from the Slack Form
  callback_id: "post_issue_message",
title: "Create an issue mobile sdk",
description: "Create an issue in Plane.so using user input",
source_file: "functions/post_issue_message.ts",
input_parameters: {
properties: {
name: {
type: Schema.types.string,
description: "Issue name",
},
details: {
type: Schema.types.string,
description: "Issue details",
},
module: {
type: Schema.types.array,
description: "Modules associated with the issue",
items: {
type: Schema.types.string,
},
},
people: {
type: Schema.types.array,
description: "People assigned to the issue",
items: {
type: Schema.types.string,
},
},
project: {
type: Schema.types.string,
description: "Issue details",
},
priority: {
type: Schema.types.string,
description: "Issue details",
},
submitting_user:{
type: Schema.types.string,
description: "Issue details",
}
},
required: ["name", "details","project"],
},
output_parameters: {
properties: {
success: {
type: Schema.types.boolean,
description: "Indicates if the issue was created successfully",
},
message: {
type: Schema.types.string,
description: "Response message",
},
},
required: ["success"],
},
});
  • Adding Details of Your Projects, Modules, People's Plane.so Id and Slack Id
const projectMappings = {
Mobile: "YOUR_ID",
Backend: "YOUR_ID",
};
// Can go to the your project page in Plane.so. In the url as path parameter you will get your project id.
const teamTag = {
Mobile: "<!subteam^teamid>",
// These are group tag of Slack group. Required to notify team on slack that new issue has been added.
};

const moduleMappings = {
BUGS: "YOUR_MODULE_ID",
// Add more module mappings as needed
};
// Can go to the your module page in Plane.so. In the url as path parameter you will get your project id.
const peopleMappings = {
USER1: "USER_ID1",
// Add more people mappings as needed
};
// Can go to the your profile page of people in Plane.so. In the url as path parameter you will get your project id.
const slackpeopleMappings = {
"USER1's SLACK_ID": "USER_ID1",
// Add more people mappings as needed Example Fromat: ("<@SOMEID>")
};
// need this to tag people who have been assigned the issue
  • Calling the Api and Sending Slack Message:
export default SlackFunction(PostIssueMessage, async ({ inputs, client }) => {
const { name, details, project, module, people, priority, submitting_user } =
inputs;
console.log(people);
console.log(name);
console.log(details);
console.log(submitting_user);

// Map the project, module, and people inputs to their corresponding IDs
const PROJECT_ID = projectMappings[project];

// Safely handle undefined or empty module input
const MODULE_IDS = module ? module.map((mod) => moduleMappings[mod]) : [];

const PEOPLE_IDS = people
? people.map((person) => peopleMappings[person])
: [];
const channelID = channelMappings[project];
const groupID = peopleTag[project];

try {
// Create the issue
const response = await fetch(
`https://[YOUR-HOST-ADDRESS]/api/v1/workspaces/${WORKSPACE_SLUG}/projects/${PROJECT_ID}/issues/`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": "YOUR_API_KEY",
},
body: JSON.stringify({
name: name,
description_html: details,
assignees: PEOPLE_IDS, // Assigning to people
priority: priority,
}),
},
);

const data = await response.json();
const id = data.id;

// Assign the issue to modules
if (MODULE_IDS.length > 0) {
for (const moduleId of MODULE_IDS) {
await fetch(
`https://[YOUR-HOST-ADDRESS]/api/v1/workspaces/${WORKSPACE_SLUG}/projects/${PROJECT_ID}/modules/${moduleId}/module-issues/`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": "YOUR_API_KEY",
},
body: JSON.stringify({
issues: [id],
}),
},
);
}
}
const issueCreatorID = data.created_by;
const assigneesIDs = data.assignees;
console.log(assigneesIDs);

const issueCreatorName = Object.keys(peopleMappings).find(
(key) => peopleMappings[key] === issueCreatorID,
);

const assigneeNames = assigneesIDs.map((assigneeID) =>
Object.keys(slackpeopleMappings).find(
(key) => slackpeopleMappings[key] === assigneeID,
),
);
const cleanDescription = data.description_html.replace(
/<\/?[^>]+(>|$)/g,
"",
);
let priorityEmoji;
switch (data.priority.toLowerCase()) {
case "urgent":
priorityEmoji = ":red_circle:"; // Urgent
break;
case "high":
priorityEmoji = ":large_orange_circle:"; // High
break;
case "low":
priorityEmoji = ":large_yellow_circle:"; // Low
break;
case "none":
default:
priorityEmoji = ":white_circle:"; // None
break;
}
// sending slack message on Issue Creation
const successMessage = `
*${priorityEmoji} ${data.priority.toUpperCase()} Priority*

*${groupID}*
:heavy_check_mark: *New Issue Created*

\`\`\`
Issue Details:
- Created By: <@${submitting_user}>
- Issue ID: ${data.id}
- Name: ${data.name}
- Description: ${cleanDescription}
- Modules: ${module}
- Priority: ${data.priority}
\`\`\`
Assigned To: ${assigneeNames}
`;

await client.chat.postMessage({
channel: channelID,
text: successMessage,
});

return {
outputs: { success: true, message: successMessage },
};
} catch (error) {
console.error(error);
const errorMessage = `Failed to create the issue. Error: ${error.message}`;

await client.chat.postMessage({
channel: "CHANNEL_ID", // Your Slack Channel in which you want to send the notification
text: errorMessage,
});

return {
outputs: { success: false, message: errorMessage },
};
}
});

Executing the Plane.so api for creating issue:

Call the PostIssueMessage from the submit-workflow.ts file as Add step.

    SubmitIssueWorkflow.addStep(PostIssueMessage, {
name: inputForm.outputs.fields.name,
details: inputForm.outputs.fields.details,
project:inputForm.outputs.fields.project,
module:inputForm.outputs.fields.module,
people:inputForm.outputs.fields.people,
channel: SubmitIssueWorkflow.inputs.channel,
submitting_user: SubmitIssueWorkflow.inputs.interactivity.interactor.id, //person who has tringgered the workflow (i.e who is creating issue)
priority:inputForm.outputs.fields.priority,
});

Creating And Trigger

In the template you will have a trigger folder. In side that customize the trigger function according to your use case.

const submitIssue: Trigger<typeof SubmitIssueWorkflow.definition> = {
type: TriggerTypes.Shortcut,
name: "Name of Your Trigger",
description: "Description of your Channel",
workflow: "#/workflows/submit_issue",
inputs: {
interactivity: {
value: TriggerContextData.Shortcut.interactivity,
},
channel: {
value: TriggerContextData.Shortcut.channel_id,
},
},
};

export default submitIssue;

Closing words

I know its a long blog, but i think it covers most of things that you will be requiring for building a Slack Bot for creating issues in Plane.so. Kudos to Plane.so team for giving a SelfHosting options for Plane.so

Thank you for your patience. Until next time 👋👋