Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions integrations/jira/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 23 additions & 0 deletions integrations/jira/integration.definition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { IntegrationDefinition } from '@botpress/sdk'

import { configuration, states, user, channels, actions } from './src/definitions'

export default new IntegrationDefinition({
name: 'jira',
title: 'Jira',
description:
'This integration allows you to work with your Jira workspace, users, projects, and workflow transitions.',
version: '0.3.0',
readme: 'readme.md',
icon: 'icon.svg',
configuration,
channels,
user,
actions,
events: {},
states,
attributes: {
category: 'Project Management',
repo: 'botpress',
},
})
24 changes: 24 additions & 0 deletions integrations/jira/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "@botpresshub/jirasoftware",
"description": "Work with your Jira workspace, users, projects, and workflow transitions from Botpress.",
"scripts": {
"build": "bp add -y && bp build",
"check:type": "tsc --noEmit",
"check:bplint": "bp lint",
"test": "vitest --run"
},
"keywords": [],
"private": true,
"author": "",
"license": "MIT",
"dependencies": {
"@botpress/client": "workspace:*",
"@botpress/sdk": "workspace:*",
"jira.js": "^2.19.1"
},
"devDependencies": {
"@botpress/cli": "workspace:*",
"@botpress/common": "workspace:*",
"@types/node": "^22.16.4"
}
}
51 changes: 51 additions & 0 deletions integrations/jira/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Botpress Jira Software Integration

This integration allows you to connect your Botpress chatbot with Jira Software, a popular platform for project management and issue tracking. With this integration, you can search, create, update, and transition issues, list projects, find Jira users, and post issue comments from your chatbot.

To set up the integration, you will need to provide your **host**, **email**, and **API token** credentials. Once the integration is set up, you can use the built-in actions to manage issues and projects, and use the issue comments channel to post comments.

For more detailed instructions on how to set up and use the Botpress Jira Software integration, please refer to our documentation.

## Prerequisites

Before enabling the Botpress Jira Software Integration, please ensure that you have the following:

- A Botpress cloud account.
- Access to a Jira Software account.
- API token generated from your Jira Software account.

## Enable Integration

To enable the Jira Software integration in Botpress, follow these steps:

- Access your Botpress admin panel.
- Navigate to the “Integrations” section.
- Locate the Jira Software integration and click on "Install Integration".
- Provide the required API token, host, and email configuration details.
- Save the configuration.

## Usage

Once the integration is enabled, you can start using Jira from your Botpress chatbot. The integration offers the following actions:

- **Issues**: `searchIssues` (JQL with cursor pagination), `countIssues`, `pickIssue`, `getIssue`, `newIssue`, `newIssues` (batch up to 50), `updateIssue`, `assignIssue`, `deleteIssue`, `getIssueTransitions`, `transitionIssue`
- **Projects**: `listProjects`, `listProjectStatuses`, `listIssueTypes` (per project)
- **Users**: `findUser`, `findAllUsers`

To post comments to Jira issues, send text messages through the `issueComments` channel with the target `issueKey` conversation tag.

To move an issue through its workflow, first call `getIssueTransitions` for that issue to discover valid transition IDs, then pass one to `transitionIssue`.

> Issue search uses Atlassian's `POST /rest/api/3/search/jql` endpoint (replacing the deprecated `/rest/api/3/search` retired in May 2025). Pagination is cursor-based — pass the `nextToken` from the previous response to fetch the next page. Use `countIssues` if you only need a total.

For more detailed information and examples, refer to the Botpress documentation or the Jira Software documentation for configuring the integration.

## Limitations

Remember that Jira's administrative limits also apply to the use of the API.

## Contributing

Contributions are welcome! If you encounter any issues or have suggestions for improvement, please submit them via the project’s issue tracker. Pull requests are also appreciated.

Enjoy the seamless project management integration between Botpress and Jira Software!
30 changes: 30 additions & 0 deletions integrations/jira/src/actions/assign-issue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { RuntimeError } from '@botpress/sdk'

import { assignIssueInputSchema, assignIssueOutputSchema } from '../misc/custom-schemas'
import type { Implementation } from '../misc/types'

import { getClient, getErrorMessage, serializeErrorForLog } from '../utils'

export const assignIssue: Implementation['actions']['assignIssue'] = async ({ ctx, input, logger }) => {
const validatedInput = assignIssueInputSchema.parse(input)
const jiraClient = getClient(ctx.configuration)

try {
await jiraClient.assignIssue(validatedInput.issueKey, validatedInput.accountId)
logger
.forBot()
.info(
`Successful - Assign Issue - ${validatedInput.issueKey} ${
validatedInput.accountId === null ? 'unassigned' : `→ ${validatedInput.accountId}`
}`
)
return assignIssueOutputSchema.parse({
issueKey: validatedInput.issueKey,
accountId: validatedInput.accountId,
})
} catch (error) {
logger.forBot().debug(`'Assign Issue' exception ${serializeErrorForLog(error)}`)
const message = getErrorMessage(error)
throw new RuntimeError(`Failed to assign issue ${validatedInput.issueKey}: ${message}`)
}
}
18 changes: 18 additions & 0 deletions integrations/jira/src/actions/count-issues.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { countIssuesInputSchema, countIssuesOutputSchema } from '../misc/custom-schemas'
import type { Implementation } from '../misc/types'

import { buildRuntimeError, getClient, serializeErrorForLog } from '../utils'

export const countIssues: Implementation['actions']['countIssues'] = async ({ ctx, input, logger }) => {
const validatedInput = countIssuesInputSchema.parse(input)
const jiraClient = getClient(ctx.configuration)

try {
const count = await jiraClient.countIssues(validatedInput.jql)
logger.forBot().info(`Successful - Count Issues - ${count} match`)
return countIssuesOutputSchema.parse({ count })
} catch (error) {
logger.forBot().debug(`'Count Issues' exception ${serializeErrorForLog(error)}`)
throw buildRuntimeError('Failed to count issues', error)
}
}
21 changes: 21 additions & 0 deletions integrations/jira/src/actions/delete-issue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { RuntimeError } from '@botpress/sdk'

import { deleteIssueInputSchema, deleteIssueOutputSchema } from '../misc/custom-schemas'
import type { Implementation } from '../misc/types'

import { getClient, getErrorMessage, serializeErrorForLog } from '../utils'

export const deleteIssue: Implementation['actions']['deleteIssue'] = async ({ ctx, input, logger }) => {
const validatedInput = deleteIssueInputSchema.parse(input)
const jiraClient = getClient(ctx.configuration)

try {
await jiraClient.deleteIssue(validatedInput.issueKey, validatedInput.deleteSubtasks ?? false)
logger.forBot().info(`Successful - Delete Issue - ${validatedInput.issueKey}`)
return deleteIssueOutputSchema.parse({ issueKey: validatedInput.issueKey })
} catch (error) {
logger.forBot().debug(`'Delete Issue' exception ${serializeErrorForLog(error)}`)
const message = getErrorMessage(error)
throw new RuntimeError(`Failed to delete issue ${validatedInput.issueKey}: ${message}`)
}
}
26 changes: 26 additions & 0 deletions integrations/jira/src/actions/find-all-users.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { findAllUsersInputSchema, findAllUsersOutputSchema } from '../misc/custom-schemas'
import type { Implementation } from '../misc/types'

import { buildRuntimeError, getClient, serializeErrorForLog } from '../utils'

export const findAllUsers: Implementation['actions']['findAllUsers'] = async ({ ctx, input, logger }) => {
const validatedInput = findAllUsersInputSchema.parse(input)
const jiraClient = getClient(ctx.configuration)
const addParams = {
startAt: validatedInput.startAt,
maxResults: validatedInput.maxResults,
}
try {
const response = await jiraClient.findAllUsers(addParams)
logger.forBot().info(`Successful - Find All User - Total Users ${response.length}`)
return findAllUsersOutputSchema.parse({
users: response.map((user) => ({
...user,
active: user.active ?? false,
})),
})
} catch (error) {
logger.forBot().debug(`'Find All User' exception ${serializeErrorForLog(error)}`)
throw buildRuntimeError('Failed to find all users', error)
}
}
24 changes: 24 additions & 0 deletions integrations/jira/src/actions/find-user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { RuntimeError } from '@botpress/sdk'
import { findUserInputSchema, findUserOutputSchema } from '../misc/custom-schemas'
import type { Implementation } from '../misc/types'

import { getClient, getErrorMessage, serializeErrorForLog } from '../utils'

export const findUser: Implementation['actions']['findUser'] = async ({ ctx, input, logger }) => {
const validatedInput = findUserInputSchema.parse(input)
const jiraClient = getClient(ctx.configuration)
try {
const response = await jiraClient.findUser(validatedInput.query)
logger
.forBot()
.info(`Successful - Find User - ${response?.displayName || 'Unknown'} - with query: ${validatedInput.query}`)
return findUserOutputSchema.parse({
...response,
active: response.active ?? false,
})
} catch (error) {
logger.forBot().debug(`'Find User' exception ${serializeErrorForLog(error)}`)
const message = getErrorMessage(error)
throw new RuntimeError(`Failed to find user: ${message}`)
}
}
37 changes: 37 additions & 0 deletions integrations/jira/src/actions/get-issue-transitions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { RuntimeError } from '@botpress/sdk'
import { getIssueTransitionsInputSchema, getIssueTransitionsOutputSchema } from '../misc/custom-schemas'
import type { Implementation } from '../misc/types'
import { getClient, getErrorMessage, serializeErrorForLog } from '../utils'

export const getIssueTransitions: Implementation['actions']['getIssueTransitions'] = async ({ ctx, input, logger }) => {
const validatedInput = getIssueTransitionsInputSchema.parse(input)
const jiraClient = getClient(ctx.configuration)

try {
const response = await jiraClient.getIssueTransitions({
issueIdOrKey: validatedInput.issueKey,
includeUnavailableTransitions: true,
})

const transitions = (response.transitions ?? []).flatMap((t) => {
if (!t.id) return []
return [
{
id: t.id,
name: t.name,
toStatus: t.to?.name,
toStatusCategory: t.to?.statusCategory?.name,
isAvailable: t.isAvailable,
hasScreen: t.hasScreen,
},
]
})

logger.forBot().info(`Successful - Get Issue Transitions - ${transitions.length} for ${validatedInput.issueKey}`)
return getIssueTransitionsOutputSchema.parse({ transitions })
} catch (error) {
logger.forBot().debug(`'Get Issue Transitions' exception ${serializeErrorForLog(error)}`)
const message = getErrorMessage(error)
throw new RuntimeError(`Failed to get transitions for ${validatedInput.issueKey}: ${message}`)
}
}
24 changes: 24 additions & 0 deletions integrations/jira/src/actions/get-issue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { RuntimeError } from '@botpress/sdk'

import { getIssueInputSchema, getIssueOutputSchema } from '../misc/custom-schemas'
import type { Implementation } from '../misc/types'

import { ISSUE_SEARCH_FIELDS, flattenIssue, getClient, getErrorMessage, serializeErrorForLog } from '../utils'

export const getIssue: Implementation['actions']['getIssue'] = async ({ ctx, input, logger }) => {
const validatedInput = getIssueInputSchema.parse(input)
const jiraClient = getClient(ctx.configuration)

try {
const response = await jiraClient.getIssue({
issueIdOrKey: validatedInput.issueKey,
fields: ISSUE_SEARCH_FIELDS,
})
logger.forBot().info(`Successful - Get Issue - ${response.key}`)
return getIssueOutputSchema.parse(flattenIssue(response, ctx.configuration.host))
} catch (error) {
logger.forBot().debug(`'Get Issue' exception ${serializeErrorForLog(error)}`)
const message = getErrorMessage(error)
throw new RuntimeError(`Failed to get issue ${validatedInput.issueKey}: ${message}`)
}
}
35 changes: 35 additions & 0 deletions integrations/jira/src/actions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { assignIssue } from './assign-issue'
import { countIssues } from './count-issues'
import { deleteIssue } from './delete-issue'
import { findAllUsers } from './find-all-users'
import { findUser } from './find-user'
import { getIssue } from './get-issue'
import { getIssueTransitions } from './get-issue-transitions'
import { listIssueTypes } from './list-issue-types'
import { listProjectStatuses } from './list-project-statuses'
import { listProjects } from './list-projects'
import { newIssue } from './new-issue'
import { newIssues } from './new-issues'
import { pickIssue } from './pick-issue'
import { searchIssues } from './search-issues'
import { transitionIssue } from './transition-issue'
import { updateIssue } from './update-issue'

export default {
findUser,
newIssue,
newIssues,
updateIssue,
assignIssue,
deleteIssue,
findAllUsers,
searchIssues,
countIssues,
pickIssue,
getIssue,
listProjects,
getIssueTransitions,
transitionIssue,
listIssueTypes,
listProjectStatuses,
}
28 changes: 28 additions & 0 deletions integrations/jira/src/actions/list-issue-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { RuntimeError } from '@botpress/sdk'

import { listIssueTypesInputSchema, listIssueTypesOutputSchema } from '../misc/custom-schemas'
import type { Implementation } from '../misc/types'

import { getClient, getErrorMessage, serializeErrorForLog } from '../utils'

export const listIssueTypes: Implementation['actions']['listIssueTypes'] = async ({ ctx, input, logger }) => {
const validatedInput = listIssueTypesInputSchema.parse(input)
const jiraClient = getClient(ctx.configuration)

try {
const response = await jiraClient.listIssueTypesForProject(validatedInput.projectKey)
const items = (response.issueTypes ?? []).map((t) => ({
id: t.id,
name: t.name,
description: t.description,
subtask: t.subtask,
hierarchyLevel: t.hierarchyLevel,
}))
logger.forBot().info(`Successful - List Issue Types - ${items.length} for ${validatedInput.projectKey}`)
return listIssueTypesOutputSchema.parse({ items })
} catch (error) {
logger.forBot().debug(`'List Issue Types' exception ${serializeErrorForLog(error)}`)
const message = getErrorMessage(error)
throw new RuntimeError(`Failed to list issue types for project ${validatedInput.projectKey}: ${message}`)
}
}
28 changes: 28 additions & 0 deletions integrations/jira/src/actions/list-project-statuses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { RuntimeError } from '@botpress/sdk'
import { listProjectStatusesInputSchema, listProjectStatusesOutputSchema } from '../misc/custom-schemas'
import type { Implementation } from '../misc/types'
import { getClient, getErrorMessage, serializeErrorForLog } from '../utils'

export const listProjectStatuses: Implementation['actions']['listProjectStatuses'] = async ({ ctx, input, logger }) => {
const validatedInput = listProjectStatusesInputSchema.parse(input)
const jiraClient = getClient(ctx.configuration)

try {
const response = await jiraClient.listProjectStatuses(validatedInput.projectKey)
const items = response.flatMap((typeWithStatus) =>
(typeWithStatus.statuses ?? []).map((s) => ({
id: s.id,
name: s.name,
description: s.description,
category: s.statusCategory?.name,
issueType: typeWithStatus.name,
}))
)
logger.forBot().info(`Successful - List Project Statuses - ${items.length} for ${validatedInput.projectKey}`)
return listProjectStatusesOutputSchema.parse({ items })
} catch (error) {
logger.forBot().debug(`'List Project Statuses' exception ${serializeErrorForLog(error)}`)
const message = getErrorMessage(error)
throw new RuntimeError(`Failed to list statuses for project ${validatedInput.projectKey}: ${message}`)
}
}
Loading
Loading