Skip to content

Commit a779ea0

Browse files
waleedlatif1claude
andcommitted
feat(jsm): add ProForma/JSM Forms discovery tools
Add three new tools for discovering and inspecting JSM Forms (ProForma) templates and their structure, enabling dynamic form-based workflows: - jsm_get_form_templates: List form templates in a project with request type bindings - jsm_get_form_structure: Get full form design (questions, layout, conditions, sections) - jsm_get_issue_forms: List forms attached to an issue with submission status All endpoints validated against the official Atlassian Forms REST API OpenAPI spec. Uses the Forms Cloud API base URL (jira/forms/cloud/{cloudId}) with X-ExperimentalApi header. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7bd271a commit a779ea0

File tree

13 files changed

+1030
-1
lines changed

13 files changed

+1030
-1
lines changed

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

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -678,4 +678,84 @@ Get the fields required to create a request of a specific type in Jira Service M
678678
|`defaultValues` | json | Default values for the field |
679679
|`jiraSchema` | json | Jira field schema with type, system, custom, customId |
680680

681+
### `jsm_get_form_templates`
682+
683+
List forms (ProForma/JSM Forms) in a Jira project to discover form IDs for request types
684+
685+
#### Input
686+
687+
| Parameter | Type | Required | Description |
688+
| --------- | ---- | -------- | ----------- |
689+
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
690+
| `cloudId` | string | No | Jira Cloud ID for the instance |
691+
| `projectIdOrKey` | string | Yes | Jira project ID or key \(e.g., "10001" or "SD"\) |
692+
693+
#### Output
694+
695+
| Parameter | Type | Description |
696+
| --------- | ---- | ----------- |
697+
| `ts` | string | Timestamp of the operation |
698+
| `projectIdOrKey` | string | Project ID or key |
699+
| `templates` | array | List of forms in the project |
700+
|`id` | string | Form template ID \(UUID\) |
701+
|`name` | string | Form template name |
702+
|`updated` | string | Last updated timestamp \(ISO 8601\) |
703+
|`issueCreateIssueTypeIds` | json | Issue type IDs that auto-attach this form on issue create |
704+
|`issueCreateRequestTypeIds` | json | Request type IDs that auto-attach this form on issue create |
705+
|`portalRequestTypeIds` | json | Request type IDs that show this form on the customer portal |
706+
|`recommendedIssueRequestTypeIds` | json | Request type IDs that recommend this form |
707+
| `total` | number | Total number of forms |
708+
709+
### `jsm_get_form_structure`
710+
711+
Get the full structure of a ProForma/JSM form including all questions, field types, choices, layout, and conditions
712+
713+
#### Input
714+
715+
| Parameter | Type | Required | Description |
716+
| --------- | ---- | -------- | ----------- |
717+
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
718+
| `cloudId` | string | No | Jira Cloud ID for the instance |
719+
| `projectIdOrKey` | string | Yes | Jira project ID or key \(e.g., "10001" or "SD"\) |
720+
| `formId` | string | Yes | Form ID \(UUID from Get Form Templates\) |
721+
722+
#### Output
723+
724+
| Parameter | Type | Description |
725+
| --------- | ---- | ----------- |
726+
| `ts` | string | Timestamp of the operation |
727+
| `projectIdOrKey` | string | Project ID or key |
728+
| `formId` | string | Form ID |
729+
| `design` | json | Full form design with questions \(field types, labels, choices, validation\), layout \(field ordering\), and conditions |
730+
| `updated` | string | Last updated timestamp |
731+
| `publish` | json | Publishing and request type configuration |
732+
733+
### `jsm_get_issue_forms`
734+
735+
List forms (ProForma/JSM Forms) attached to a Jira issue with metadata (name, submitted status, lock)
736+
737+
#### Input
738+
739+
| Parameter | Type | Required | Description |
740+
| --------- | ---- | -------- | ----------- |
741+
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
742+
| `cloudId` | string | No | Jira Cloud ID for the instance |
743+
| `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., "SD-123", "10001"\) |
744+
745+
#### Output
746+
747+
| Parameter | Type | Description |
748+
| --------- | ---- | ----------- |
749+
| `ts` | string | Timestamp of the operation |
750+
| `issueIdOrKey` | string | Issue ID or key |
751+
| `forms` | array | List of forms attached to the issue |
752+
|`id` | string | Form instance ID \(UUID\) |
753+
|`name` | string | Form name |
754+
|`updated` | string | Last updated timestamp \(ISO 8601\) |
755+
|`submitted` | boolean | Whether the form has been submitted |
756+
|`lock` | boolean | Whether the form is locked |
757+
|`internal` | boolean | Whether the form is internal-only |
758+
|`formTemplateId` | string | Source form template ID \(UUID\) |
759+
| `total` | number | Total number of forms |
760+
681761

apps/sim/app/(landing)/integrations/data/integrations.json

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6614,9 +6614,21 @@
66146614
{
66156615
"name": "Get Request Type Fields",
66166616
"description": "Get the fields required to create a request of a specific type in Jira Service Management"
6617+
},
6618+
{
6619+
"name": "Get Form Templates",
6620+
"description": "List forms (ProForma/JSM Forms) in a Jira project to discover form IDs for request types"
6621+
},
6622+
{
6623+
"name": "Get Form Structure",
6624+
"description": "Get the full structure of a ProForma/JSM form including all questions, field types, choices, layout, and conditions"
6625+
},
6626+
{
6627+
"name": "Get Issue Forms",
6628+
"description": "List forms (ProForma/JSM Forms) attached to a Jira issue with metadata (name, submitted status, lock)"
66176629
}
66186630
],
6619-
"operationCount": 21,
6631+
"operationCount": 24,
66206632
"triggers": [],
66216633
"triggerCount": 0,
66226634
"authType": "oauth",
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { createLogger } from '@sim/logger'
2+
import { type NextRequest, NextResponse } from 'next/server'
3+
import { checkInternalAuth } from '@/lib/auth/hybrid'
4+
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
5+
import { getJiraCloudId, getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
6+
7+
export const dynamic = 'force-dynamic'
8+
9+
const logger = createLogger('JsmIssueFormsAPI')
10+
11+
function parseJsmErrorMessage(status: number, statusText: string, errorText: string): string {
12+
try {
13+
const errorData = JSON.parse(errorText)
14+
if (errorData.errorMessage) {
15+
return `JSM Forms API error: ${errorData.errorMessage}`
16+
}
17+
} catch {
18+
if (errorText) {
19+
return `JSM Forms API error: ${errorText}`
20+
}
21+
}
22+
return `JSM Forms API error: ${status} ${statusText}`
23+
}
24+
25+
export async function POST(request: NextRequest) {
26+
const auth = await checkInternalAuth(request)
27+
if (!auth.success || !auth.userId) {
28+
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
29+
}
30+
31+
try {
32+
const body = await request.json()
33+
const { domain, accessToken, cloudId: cloudIdParam, issueIdOrKey } = body
34+
35+
if (!domain) {
36+
logger.error('Missing domain in request')
37+
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
38+
}
39+
40+
if (!accessToken) {
41+
logger.error('Missing access token in request')
42+
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
43+
}
44+
45+
if (!issueIdOrKey) {
46+
logger.error('Missing issueIdOrKey in request')
47+
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
48+
}
49+
50+
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
51+
52+
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
53+
if (!cloudIdValidation.isValid) {
54+
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
55+
}
56+
57+
const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey')
58+
if (!issueIdOrKeyValidation.isValid) {
59+
return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 })
60+
}
61+
62+
const baseUrl = getJsmFormsApiBaseUrl(cloudId)
63+
const url = `${baseUrl}/issue/${encodeURIComponent(issueIdOrKey)}/form`
64+
65+
logger.info('Fetching issue forms from:', { url, issueIdOrKey })
66+
67+
const response = await fetch(url, {
68+
method: 'GET',
69+
headers: getJsmHeaders(accessToken),
70+
})
71+
72+
if (!response.ok) {
73+
const errorText = await response.text()
74+
logger.error('JSM Forms API error:', {
75+
status: response.status,
76+
statusText: response.statusText,
77+
error: errorText,
78+
})
79+
80+
return NextResponse.json(
81+
{
82+
error: parseJsmErrorMessage(response.status, response.statusText, errorText),
83+
details: errorText,
84+
},
85+
{ status: response.status }
86+
)
87+
}
88+
89+
const data = await response.json()
90+
91+
const forms = Array.isArray(data) ? data : (data.values ?? data.forms ?? [])
92+
93+
return NextResponse.json({
94+
success: true,
95+
output: {
96+
ts: new Date().toISOString(),
97+
issueIdOrKey,
98+
forms: forms.map((form: Record<string, unknown>) => ({
99+
id: form.id ?? null,
100+
name: form.name ?? null,
101+
updated: form.updated ?? null,
102+
submitted: form.submitted ?? false,
103+
lock: form.lock ?? false,
104+
internal: form.internal ?? null,
105+
formTemplateId: (form.formTemplate as Record<string, unknown>)?.id ?? null,
106+
})),
107+
total: forms.length,
108+
},
109+
})
110+
} catch (error) {
111+
logger.error('Error fetching issue forms:', {
112+
error: error instanceof Error ? error.message : String(error),
113+
stack: error instanceof Error ? error.stack : undefined,
114+
})
115+
116+
return NextResponse.json(
117+
{
118+
error: error instanceof Error ? error.message : 'Internal server error',
119+
success: false,
120+
},
121+
{ status: 500 }
122+
)
123+
}
124+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { createLogger } from '@sim/logger'
2+
import { type NextRequest, NextResponse } from 'next/server'
3+
import { checkInternalAuth } from '@/lib/auth/hybrid'
4+
import { validateJiraCloudId } from '@/lib/core/security/input-validation'
5+
import { getJiraCloudId, getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
6+
7+
export const dynamic = 'force-dynamic'
8+
9+
const logger = createLogger('JsmFormStructureAPI')
10+
11+
function parseJsmErrorMessage(status: number, statusText: string, errorText: string): string {
12+
try {
13+
const errorData = JSON.parse(errorText)
14+
if (errorData.errorMessage) {
15+
return `JSM Forms API error: ${errorData.errorMessage}`
16+
}
17+
} catch {
18+
if (errorText) {
19+
return `JSM Forms API error: ${errorText}`
20+
}
21+
}
22+
return `JSM Forms API error: ${status} ${statusText}`
23+
}
24+
25+
export async function POST(request: NextRequest) {
26+
const auth = await checkInternalAuth(request)
27+
if (!auth.success || !auth.userId) {
28+
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
29+
}
30+
31+
try {
32+
const body = await request.json()
33+
const { domain, accessToken, cloudId: cloudIdParam, projectIdOrKey, formId } = body
34+
35+
if (!domain) {
36+
logger.error('Missing domain in request')
37+
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
38+
}
39+
40+
if (!accessToken) {
41+
logger.error('Missing access token in request')
42+
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
43+
}
44+
45+
if (!projectIdOrKey) {
46+
logger.error('Missing projectIdOrKey in request')
47+
return NextResponse.json({ error: 'Project ID or key is required' }, { status: 400 })
48+
}
49+
50+
if (!formId) {
51+
logger.error('Missing formId in request')
52+
return NextResponse.json({ error: 'Form ID is required' }, { status: 400 })
53+
}
54+
55+
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
56+
57+
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
58+
if (!cloudIdValidation.isValid) {
59+
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
60+
}
61+
62+
const baseUrl = getJsmFormsApiBaseUrl(cloudId)
63+
const url = `${baseUrl}/project/${encodeURIComponent(projectIdOrKey)}/form/${encodeURIComponent(formId)}`
64+
65+
logger.info('Fetching form template from:', { url, projectIdOrKey, formId })
66+
67+
const response = await fetch(url, {
68+
method: 'GET',
69+
headers: getJsmHeaders(accessToken),
70+
})
71+
72+
if (!response.ok) {
73+
const errorText = await response.text()
74+
logger.error('JSM Forms API error:', {
75+
status: response.status,
76+
statusText: response.statusText,
77+
error: errorText,
78+
})
79+
80+
return NextResponse.json(
81+
{
82+
error: parseJsmErrorMessage(response.status, response.statusText, errorText),
83+
details: errorText,
84+
},
85+
{ status: response.status }
86+
)
87+
}
88+
89+
const data = await response.json()
90+
91+
return NextResponse.json({
92+
success: true,
93+
output: {
94+
ts: new Date().toISOString(),
95+
projectIdOrKey,
96+
formId,
97+
design: data.design ?? null,
98+
updated: data.updated ?? null,
99+
publish: data.publish ?? null,
100+
},
101+
})
102+
} catch (error) {
103+
logger.error('Error fetching form structure:', {
104+
error: error instanceof Error ? error.message : String(error),
105+
stack: error instanceof Error ? error.stack : undefined,
106+
})
107+
108+
return NextResponse.json(
109+
{
110+
error: error instanceof Error ? error.message : 'Internal server error',
111+
success: false,
112+
},
113+
{ status: 500 }
114+
)
115+
}
116+
}

0 commit comments

Comments
 (0)