Skip to content

Commit 0a52b09

Browse files
authored
feat(jira): add search_users tool for user lookup by email (#3451)
* feat(jira): add search_users tool for user lookup by email * improvement(jira): reuse shared transformUser utility in search_users * improvement(jira): add pagination fields to search_users response * update * fix(jira): filter falsy entries before transforming search_users results * fix(jira): add defensive fallback for nullable transformUser in search_users * fix(jira): align search_users response type with transformUser return type
1 parent 1d36b80 commit 0a52b09

File tree

6 files changed

+279
-0
lines changed

6 files changed

+279
-0
lines changed

apps/docs/content/docs/en/tools/jira.mdx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1014,4 +1014,36 @@ Get Jira users. If an account ID is provided, returns a single user. Otherwise,
10141014
| `startAt` | number | Pagination start index |
10151015
| `maxResults` | number | Maximum results per page |
10161016

1017+
### `jira_search_users`
1018+
1019+
Search for Jira users by email address or display name. Returns matching users with their accountId, displayName, and emailAddress.
1020+
1021+
#### Input
1022+
1023+
| Parameter | Type | Required | Description |
1024+
| --------- | ---- | -------- | ----------- |
1025+
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
1026+
| `query` | string | Yes | A query string to search for users. Can be an email address, display name, or partial match. |
1027+
| `maxResults` | number | No | Maximum number of users to return \(default: 50, max: 1000\) |
1028+
| `startAt` | number | No | The index of the first user to return \(for pagination, default: 0\) |
1029+
| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. |
1030+
1031+
#### Output
1032+
1033+
| Parameter | Type | Description |
1034+
| --------- | ---- | ----------- |
1035+
| `ts` | string | ISO 8601 timestamp of the operation |
1036+
| `users` | array | Array of matching Jira users |
1037+
|`accountId` | string | Atlassian account ID of the user |
1038+
|`displayName` | string | Display name of the user |
1039+
|`active` | boolean | Whether the user account is active |
1040+
|`emailAddress` | string | Email address of the user |
1041+
|`accountType` | string | Type of account \(e.g., atlassian, app, customer\) |
1042+
|`avatarUrl` | string | URL to the user avatar \(48x48\) |
1043+
|`timeZone` | string | User timezone |
1044+
|`self` | string | REST API URL for this user |
1045+
| `total` | number | Number of users returned in this page \(may be less than total matches\) |
1046+
| `startAt` | number | Pagination start index |
1047+
| `maxResults` | number | Maximum results per page |
1048+
10171049

apps/sim/blocks/blocks/jira.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
4747
{ label: 'Add Watcher', id: 'add_watcher' },
4848
{ label: 'Remove Watcher', id: 'remove_watcher' },
4949
{ label: 'Get Users', id: 'get_users' },
50+
{ label: 'Search Users', id: 'search_users' },
5051
],
5152
value: () => 'read',
5253
},
@@ -673,6 +674,31 @@ Return ONLY the comment text - no explanations.`,
673674
placeholder: 'Maximum users to return (default: 50)',
674675
condition: { field: 'operation', value: 'get_users' },
675676
},
677+
// Search Users fields
678+
{
679+
id: 'searchUsersQuery',
680+
title: 'Search Query',
681+
type: 'short-input',
682+
required: true,
683+
placeholder: 'Enter email address or display name to search',
684+
condition: { field: 'operation', value: 'search_users' },
685+
},
686+
{
687+
id: 'searchUsersMaxResults',
688+
title: 'Max Results',
689+
type: 'short-input',
690+
placeholder: 'Maximum users to return (default: 50)',
691+
condition: { field: 'operation', value: 'search_users' },
692+
mode: 'advanced',
693+
},
694+
{
695+
id: 'searchUsersStartAt',
696+
title: 'Start At',
697+
type: 'short-input',
698+
placeholder: 'Pagination start index (default: 0)',
699+
condition: { field: 'operation', value: 'search_users' },
700+
mode: 'advanced',
701+
},
676702
// Trigger SubBlocks
677703
...getTrigger('jira_issue_created').subBlocks,
678704
...getTrigger('jira_issue_updated').subBlocks,
@@ -707,6 +733,7 @@ Return ONLY the comment text - no explanations.`,
707733
'jira_add_watcher',
708734
'jira_remove_watcher',
709735
'jira_get_users',
736+
'jira_search_users',
710737
],
711738
config: {
712739
tool: (params) => {
@@ -767,6 +794,8 @@ Return ONLY the comment text - no explanations.`,
767794
return 'jira_remove_watcher'
768795
case 'get_users':
769796
return 'jira_get_users'
797+
case 'search_users':
798+
return 'jira_search_users'
770799
default:
771800
return 'jira_retrieve'
772801
}
@@ -1023,6 +1052,18 @@ Return ONLY the comment text - no explanations.`,
10231052
: undefined,
10241053
}
10251054
}
1055+
case 'search_users': {
1056+
return {
1057+
...baseParams,
1058+
query: params.searchUsersQuery,
1059+
maxResults: params.searchUsersMaxResults
1060+
? Number.parseInt(params.searchUsersMaxResults)
1061+
: undefined,
1062+
startAt: params.searchUsersStartAt
1063+
? Number.parseInt(params.searchUsersStartAt)
1064+
: undefined,
1065+
}
1066+
}
10261067
default:
10271068
return baseParams
10281069
}
@@ -1102,6 +1143,13 @@ Return ONLY the comment text - no explanations.`,
11021143
},
11031144
usersStartAt: { type: 'string', description: 'Pagination start index for users' },
11041145
usersMaxResults: { type: 'string', description: 'Maximum users to return' },
1146+
// Search Users operation inputs
1147+
searchUsersQuery: {
1148+
type: 'string',
1149+
description: 'Search query (email address or display name)',
1150+
},
1151+
searchUsersMaxResults: { type: 'string', description: 'Maximum users to return from search' },
1152+
searchUsersStartAt: { type: 'string', description: 'Pagination start index for user search' },
11051153
},
11061154
outputs: {
11071155
// Common outputs across all Jira operations

apps/sim/tools/jira/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { jiraGetWorklogsTool } from '@/tools/jira/get_worklogs'
1717
import { jiraRemoveWatcherTool } from '@/tools/jira/remove_watcher'
1818
import { jiraRetrieveTool } from '@/tools/jira/retrieve'
1919
import { jiraSearchIssuesTool } from '@/tools/jira/search_issues'
20+
import { jiraSearchUsersTool } from '@/tools/jira/search_users'
2021
import { jiraTransitionIssueTool } from '@/tools/jira/transition_issue'
2122
import { jiraUpdateTool } from '@/tools/jira/update'
2223
import { jiraUpdateCommentTool } from '@/tools/jira/update_comment'
@@ -48,4 +49,5 @@ export {
4849
jiraAddWatcherTool,
4950
jiraRemoveWatcherTool,
5051
jiraGetUsersTool,
52+
jiraSearchUsersTool,
5153
}
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import type { JiraSearchUsersParams, JiraSearchUsersResponse } from '@/tools/jira/types'
2+
import { TIMESTAMP_OUTPUT, USER_OUTPUT_PROPERTIES } from '@/tools/jira/types'
3+
import { getJiraCloudId, transformUser } from '@/tools/jira/utils'
4+
import type { ToolConfig } from '@/tools/types'
5+
6+
export const jiraSearchUsersTool: ToolConfig<JiraSearchUsersParams, JiraSearchUsersResponse> = {
7+
id: 'jira_search_users',
8+
name: 'Jira Search Users',
9+
description:
10+
'Search for Jira users by email address or display name. Returns matching users with their accountId, displayName, and emailAddress.',
11+
version: '1.0.0',
12+
13+
oauth: {
14+
required: true,
15+
provider: 'jira',
16+
},
17+
18+
params: {
19+
accessToken: {
20+
type: 'string',
21+
required: true,
22+
visibility: 'hidden',
23+
description: 'OAuth access token for Jira',
24+
},
25+
domain: {
26+
type: 'string',
27+
required: true,
28+
visibility: 'user-only',
29+
description: 'Your Jira domain (e.g., yourcompany.atlassian.net)',
30+
},
31+
query: {
32+
type: 'string',
33+
required: true,
34+
visibility: 'user-or-llm',
35+
description:
36+
'A query string to search for users. Can be an email address, display name, or partial match.',
37+
},
38+
maxResults: {
39+
type: 'number',
40+
required: false,
41+
visibility: 'user-or-llm',
42+
description: 'Maximum number of users to return (default: 50, max: 1000)',
43+
},
44+
startAt: {
45+
type: 'number',
46+
required: false,
47+
visibility: 'user-or-llm',
48+
description: 'The index of the first user to return (for pagination, default: 0)',
49+
},
50+
cloudId: {
51+
type: 'string',
52+
required: false,
53+
visibility: 'hidden',
54+
description:
55+
'Jira Cloud ID for the instance. If not provided, it will be fetched using the domain.',
56+
},
57+
},
58+
59+
request: {
60+
url: (params: JiraSearchUsersParams) => {
61+
if (params.cloudId) {
62+
const queryParams = new URLSearchParams()
63+
queryParams.append('query', params.query)
64+
if (params.maxResults !== undefined)
65+
queryParams.append('maxResults', String(params.maxResults))
66+
if (params.startAt !== undefined) queryParams.append('startAt', String(params.startAt))
67+
return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/user/search?${queryParams.toString()}`
68+
}
69+
return 'https://api.atlassian.com/oauth/token/accessible-resources'
70+
},
71+
method: 'GET',
72+
headers: (params: JiraSearchUsersParams) => ({
73+
Accept: 'application/json',
74+
Authorization: `Bearer ${params.accessToken}`,
75+
}),
76+
},
77+
78+
transformResponse: async (response: Response, params?: JiraSearchUsersParams) => {
79+
const fetchUsers = async (cloudId: string) => {
80+
const queryParams = new URLSearchParams()
81+
queryParams.append('query', params!.query)
82+
if (params!.maxResults !== undefined)
83+
queryParams.append('maxResults', String(params!.maxResults))
84+
if (params!.startAt !== undefined) queryParams.append('startAt', String(params!.startAt))
85+
86+
const usersUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/user/search?${queryParams.toString()}`
87+
88+
const usersResponse = await fetch(usersUrl, {
89+
method: 'GET',
90+
headers: {
91+
Accept: 'application/json',
92+
Authorization: `Bearer ${params!.accessToken}`,
93+
},
94+
})
95+
96+
if (!usersResponse.ok) {
97+
let message = `Failed to search Jira users (${usersResponse.status})`
98+
try {
99+
const err = await usersResponse.json()
100+
message = err?.errorMessages?.join(', ') || err?.message || message
101+
} catch (_e) {}
102+
throw new Error(message)
103+
}
104+
105+
return usersResponse.json()
106+
}
107+
108+
let data: any
109+
110+
if (!params?.cloudId) {
111+
const cloudId = await getJiraCloudId(params!.domain, params!.accessToken)
112+
data = await fetchUsers(cloudId)
113+
} else {
114+
if (!response.ok) {
115+
let message = `Failed to search Jira users (${response.status})`
116+
try {
117+
const err = await response.json()
118+
message = err?.errorMessages?.join(', ') || err?.message || message
119+
} catch (_e) {}
120+
throw new Error(message)
121+
}
122+
data = await response.json()
123+
}
124+
125+
const users = Array.isArray(data) ? data.filter(Boolean) : []
126+
127+
return {
128+
success: true,
129+
output: {
130+
ts: new Date().toISOString(),
131+
users: users.map((user: any) => ({
132+
...(transformUser(user) ?? { accountId: '', displayName: '' }),
133+
self: user.self ?? null,
134+
})),
135+
total: users.length,
136+
startAt: params?.startAt ?? 0,
137+
maxResults: params?.maxResults ?? 50,
138+
},
139+
}
140+
},
141+
142+
outputs: {
143+
ts: TIMESTAMP_OUTPUT,
144+
users: {
145+
type: 'array',
146+
description: 'Array of matching Jira users',
147+
items: {
148+
type: 'object',
149+
properties: {
150+
...USER_OUTPUT_PROPERTIES,
151+
self: {
152+
type: 'string',
153+
description: 'REST API URL for this user',
154+
optional: true,
155+
},
156+
},
157+
},
158+
},
159+
total: {
160+
type: 'number',
161+
description: 'Number of users returned in this page (may be less than total matches)',
162+
},
163+
startAt: { type: 'number', description: 'Pagination start index' },
164+
maxResults: { type: 'number', description: 'Maximum results per page' },
165+
},
166+
}

apps/sim/tools/jira/types.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1549,6 +1549,34 @@ export interface JiraGetUsersParams {
15491549
cloudId?: string
15501550
}
15511551

1552+
export interface JiraSearchUsersParams {
1553+
accessToken: string
1554+
domain: string
1555+
query: string
1556+
maxResults?: number
1557+
startAt?: number
1558+
cloudId?: string
1559+
}
1560+
1561+
export interface JiraSearchUsersResponse extends ToolResponse {
1562+
output: {
1563+
ts: string
1564+
users: Array<{
1565+
accountId: string
1566+
accountType?: string | null
1567+
active?: boolean | null
1568+
displayName: string
1569+
emailAddress?: string | null
1570+
avatarUrl?: string | null
1571+
timeZone?: string | null
1572+
self?: string | null
1573+
}>
1574+
total: number
1575+
startAt: number
1576+
maxResults: number
1577+
}
1578+
}
1579+
15521580
export interface JiraGetUsersResponse extends ToolResponse {
15531581
output: {
15541582
ts: string
@@ -1594,3 +1622,4 @@ export type JiraResponse =
15941622
| JiraAddWatcherResponse
15951623
| JiraRemoveWatcherResponse
15961624
| JiraGetUsersResponse
1625+
| JiraSearchUsersResponse

apps/sim/tools/registry.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1085,6 +1085,7 @@ import {
10851085
jiraRemoveWatcherTool,
10861086
jiraRetrieveTool,
10871087
jiraSearchIssuesTool,
1088+
jiraSearchUsersTool,
10881089
jiraTransitionIssueTool,
10891090
jiraUpdateCommentTool,
10901091
jiraUpdateTool,
@@ -2536,6 +2537,7 @@ export const tools: Record<string, ToolConfig> = {
25362537
jira_add_watcher: jiraAddWatcherTool,
25372538
jira_remove_watcher: jiraRemoveWatcherTool,
25382539
jira_get_users: jiraGetUsersTool,
2540+
jira_search_users: jiraSearchUsersTool,
25392541
jsm_get_service_desks: jsmGetServiceDesksTool,
25402542
jsm_get_request_types: jsmGetRequestTypesTool,
25412543
jsm_get_request_type_fields: jsmGetRequestTypeFieldsTool,

0 commit comments

Comments
 (0)