Skip to content

feat(jira): support structured custom fields in Jira Update tool - #5997

Open
mzxchandra wants to merge 13 commits into
stagingfrom
feat/jira-update-structured-custom-fields
Open

feat(jira): support structured custom fields in Jira Update tool#5997
mzxchandra wants to merge 13 commits into
stagingfrom
feat/jira-update-structured-custom-fields

Conversation

@mzxchandra

Copy link
Copy Markdown
Contributor

Supersedes #5921 (moved from fork mzxchandra/sim to an in-org branch now that I'm part of the org, so CI runs with org secrets). See #5921 for prior review discussion.

Summary

Extends the Jira Update tool so it can faithfully write structured Jira custom fields, and lets multiple custom fields + simple fields be sent together in one update.

Previously the tool accepted only a single customFieldId + a single string customFieldValue, serialized raw (fields[customfield_X] = "someString"). That can't represent Jira REST v3's option/user/cascading shapes, so select, multi-select, user-picker, and cascading custom fields could not be set at all.

New optional customFields param: an array of { fieldId, type, value } where type is one of text | number | select | multiselect | userpicker | multiuserpicker | cascading | raw. Serialization:

type emitted under fields[customfield_X]
select { value } (or { id } for a numeric option id; explicit {value}/{id} object respected)
multiselect [ { value }, ... ]
userpicker { accountId } (unwraps a { accountId } object)
multiuserpicker [ { accountId }, ... ]
cascading { value, child: { value } } (parent/child from explicit child, {parent,child}, or [parent,child])
text / number scalar (number coerces numeric strings)
raw passed through untouched (escape hatch)

The legacy customFieldId / customFieldValue params still work, normalized internally into the same pipeline as a raw entry (byte-for-byte identical to prior behavior). If both are supplied, customFields wins on fieldId collision. Custom fields are applied after the simple fields under one PUT, so summary/description/priority/labels/etc. and custom fields coexist. Description ADF auto-wrap is preserved.

Scope is limited to the Jira update path. No OAuth/scope changes, no schema/DB changes.

Type of Change

  • New feature
  • Bug fix
  • Breaking change
  • Documentation
  • Other: ___________

Testing

  • 40 unit tests across apps/sim/tools/jira/utils.test.ts (serializer + merge/precedence) and apps/sim/app/api/tools/jira/update/route.test.ts (asserts the exact PUT fields payload per type, backward-compat, collision, combined simple+custom, ADF wrap).
  • Live end-to-end against a real Jira Cloud instance (created a project with select / multiselect / userpicker / multiuserpicker / cascading / text custom fields): drove the actual buildJiraCustomFields serializer → PUT /rest/api/3/issue/{key} → read the issue back. 11/11 shapes accepted and stored correctly, including combined simple+custom in one call and the legacy path.
  • bun run check:api-validation passes. Typecheck clean for all changed files.

Reviewers: focus on serializeJiraCustomField / buildJiraCustomFields in apps/sim/tools/jira/utils.ts and the jiraCustomFieldEntrySchema addition in apps/sim/lib/api/contracts/selectors/jira.ts.

Known follow-up (not in this PR): customFields is reachable via the tool API and via an LLM/agent tool call, but the Jira block UI doesn't expose it yet (blocks/blocks/jira.ts forwards only the legacy single field). A future change can add a customFields subblock so it's configurable in the visual builder.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

…in Jira Update

The Jira Update tool only accepted a single customFieldId + string
customFieldValue and wrote it raw, so it could not represent Jira's
option/user/cascading field shapes or set more than one custom field per
call.

- Add a `customFields` param: array of { fieldId, type, value } where type
  is text | number | select | multiselect | userpicker | multiuserpicker |
  cascading | raw. Serializes each into the Jira REST v3 fields shape.
- Add serializeJiraCustomField + buildJiraCustomFields helpers in
  tools/jira/utils.ts (mirrors toAdf; unit-tested directly).
- Merge legacy customFieldId/customFieldValue into the same pipeline as a
  raw passthrough; customFields wins on fieldId collision. Backward compatible.
- Custom fields are applied after simple fields under one PUT `fields` body,
  so simple + custom fields coexist. ADF description auto-wrap preserved.
- Extend jiraUpdateBodySchema contract and JiraUpdateParams; document the
  new shape in the tool param descriptions.
- Add route + utils unit tests asserting exact `fields` payloads.
…lizer

Pre-landing review hardening:
- select now respects an explicit { value } / { id } object instead of
  re-applying the numeric-id heuristic to it (bare scalars keep the heuristic).
- userpicker / multiuserpicker unwrap a { accountId } object instead of
  stringifying it to "[object Object]".
Adds unit tests for both paths.
…cading

Review round (Greptile P1 + Cursor):
- Contract: replace the permissive customFields `value: z.unknown()` with a
  discriminated union on `type`, so a shape/value mismatch (select as { label },
  userpicker as { email }, non-numeric number) is rejected at the boundary
  instead of serializing into a malformed value that would make Jira reject the
  whole combined update.
- Serializer: a cascading entry with no resolvable parent now returns undefined
  and is skipped, instead of emitting the literal "undefined" as the parent
  value. buildJiraCustomFields skips any entry whose serialization is undefined.
- Add tests for both plus boundary rejection of mismatched shapes.
Round 2 (Greptile + Cursor): cascading was validated more loosely than
select/userpicker — value accepted any record and child was z.unknown(), so an
unresolvable shape like { id: '10' } or an object-valued parent/child passed the
boundary and was then either silently dropped or serialized to '[object
Object]', rejecting the whole combined update. Constrain cascading parent/child
to scalars (matching the other option types); an arbitrary record now 400s at
the boundary. Adds a route test for the rejection.
… options

Round 3 (Greptile + Cursor) boundary hardening:
- cascading array values are capped at [parent, child] (.max(2)) so a 3+ element
  array is rejected instead of silently truncated by the serializer.
- text values must be strings; a bare number (common from LLM tool calls) is
  rejected rather than passed through to a string-typed Jira field.
- select/multiselect option values must be non-empty, so { value: '' } / { id: '' }
  / an empty array element can't serialize into an invalid Jira option.
Adds route tests for all three rejections.
…iases

Round 4 (Greptile): a cascading object like { parent: 'A', value: 'B' } passed
validation but the serializer always uses parent and silently discarded value.
Add a refine rejecting the ambiguous case at the boundary. Adds a route test.
Round 5 (Cursor + Greptile):
- cascading scalars must be non-empty (min 1), matching option/userpicker, so
  '', [''], { parent: '' } are rejected at the boundary instead of passing
  validation and then being silently dropped by the serializer.
- reject a cascading entry that sets child both at the top level and inside
  value (the serializer would keep one and discard the other). Adds route tests.
Completes the round-5 conflicting-child refine, which only checked the object
form of value.child. A [parent, child] array plus a top-level child now also
rejects at the boundary (nested child is value[1] for the array form). Adds a
route test for the array case.
Round 7 (Cursor):
- customFields tool param was type: 'json', which the schema builder exposes to
  LLMs as a JSON Schema object while the contract requires an array — a model
  emitting an object/map would 400. Switch to type: 'array' with an items schema
  describing { fieldId, type, value }, matching the array params in jira/write.ts.
- text custom-field values must be non-empty (min 1), so '' is rejected at the
  boundary instead of passing validation and being silently skipped — consistent
  with select/userpicker/cascading. Adds a route test.
…ue and id

Round 8 (Greptile): an option object like { id: '10', value: 'High' } passed the
{ value } / { id } union, but toSelectOption keeps only one and silently discards
the other. Require an option object to set exactly one of value or id (mirrors
the cascading parent/value alias rule). Adds a route test.
Round 9 (Cursor):
- multiselect / multiuserpicker no longer accept an empty array; [] would
  serialize to [] and silently clear the Jira field, inconsistent with the
  empty-rejection for text/select/cascading. Clearing stays explicit via raw+null.
- remove the exported-but-unused jiraCustomFieldTypeSchema enum (the discriminated
  union rebuilds the literals); dead code that could drift. Adds a route test.
Round 10 (Cursor): two customFields entries that normalize to the same
customfield_XXXXX (including a lookalike pair like 10001 and customfield_10001)
would last-write-win in buildJiraCustomFields and silently apply one value. Add
a superRefine on the customFields array rejecting the collision at the boundary,
consistent with the other ambiguity rejections. Adds a route test.

The legacy customFieldId vs customFields collision stays intentional (customFields
wins) and is unaffected.
@mzxchandra
mzxchandra requested a review from waleedlatif1 July 28, 2026 01:12
@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs Ready Ready Preview, Comment Jul 28, 2026 1:17am

Request Review

@cursor

cursor Bot commented Jul 28, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes how issue updates are built for Jira (including a raw escape hatch) but keeps legacy params and adds strict validation; wrong raw values could still write incorrect field data to customer issues.

Overview
Adds optional customFields to the Jira Update tool and API so callers can set multiple custom fields with Jira REST v3 shapes (select, multiselect, user pickers, cascading, text/number, or raw passthrough) in one update alongside standard fields.

The update route no longer assigns a single raw string for customFieldId / customFieldValue; it merges legacy and structured input via buildJiraCustomFields / serializeJiraCustomField in utils.ts. Legacy params still behave as raw passthrough; structured entries win on the same normalized field id. Request validation adds jiraCustomFieldEntrySchema with per-type value checks, duplicate-id rejection, and cascading ambiguity guards so bad payloads return 400 before Jira is called.

Tool params, types, and contract wiring expose customFields; route and utils tests cover serialization, compatibility, and combined payloads.

Reviewed by Cursor Bugbot for commit 552bc9c. Bugbot is set up for automated code reviews on this repo. Configure here.

@greptile-apps

greptile-apps Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Extends the Jira Update tool/API to write structured custom fields in one PUT.

  • Adds customFields: { fieldId, type, value, child? }[] with types text | number | select | multiselect | userpicker | multiuserpicker | cascading | raw
  • Serializes each type to Jira REST v3 field shapes via serializeJiraCustomField / buildJiraCustomFields
  • Keeps legacy customFieldId/customFieldValue as raw passthrough; structured entries win on id collision
  • Zod jiraCustomFieldEntrySchema rejects bad shapes, empty multi-values, duplicate normalized ids, and ambiguous cascading/option forms
  • Unit/route tests cover serialization, validation, backward compat, and simple+custom combined updates

Confidence Score: 5/5

This PR appears safe to merge; custom-field updates are validated at the API boundary and exercise a single, well-tested serialization path.

Structured custom fields are schema-validated before serialization, legacy behavior is preserved as raw passthrough with explicit collision precedence, and tests cover the main type shapes and failure modes without a remaining blocking defect on the update path.

Important Files Changed

Filename Overview
apps/sim/tools/jira/utils.ts Adds typed custom-field serialization and merge of legacy + structured entries into a customfield_* map.
apps/sim/lib/api/contracts/selectors/jira.ts Adds per-type Zod validation, duplicate-id refine, and cascading child ambiguity checks for customFields.
apps/sim/app/api/tools/jira/update/route.ts Replaces inline legacy custom-field assign with buildJiraCustomFields after simple fields.
apps/sim/tools/jira/update.ts Exposes customFields on the tool schema and forwards it in the request body.
apps/sim/app/api/tools/jira/update/route.test.ts Route tests for serialization shapes, rejection cases, legacy override, and combined simple+custom payloads.
apps/sim/tools/jira/utils.test.ts Unit tests for serializer heuristics, cascading forms, and build/merge precedence.

Sequence Diagram

sequenceDiagram
  participant Tool as jiraUpdateTool
  participant API as PUT /api/tools/jira/update
  participant Zod as jiraUpdateBodySchema
  participant Ser as buildJiraCustomFields
  participant Jira as Jira REST v3

  Tool->>API: body with customFields / legacy pair
  API->>Zod: parseRequest
  alt invalid shape / duplicate id
    Zod-->>Tool: 400
  else valid
    Zod->>Ser: customFields + legacy
    Ser->>Ser: normalize ids, serialize per type
    API->>Jira: PUT fields (simple + custom)
    Jira-->>Tool: 204 / error
  end
Loading

Reviews (1): Last reviewed commit: "fix(jira): reject duplicate custom field..." | Re-trigger Greptile

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 552bc9c. Configure here.

result.child = { value: optionValue(child) }
}
return result
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cascading ignores numeric option ids

Medium Severity

The toCascadingOption function for cascading custom fields always serializes numeric-looking parent/child values as { value: ... }. This differs from select and multiselect fields, which correctly serialize such values as { id: ... }. As a result, updates relying on numeric Jira option IDs for cascading fields may fail or set the wrong option.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 552bc9c. Configure here.

z
.string()
.refine((s) => s.trim() !== '' && Number.isFinite(Number(s)), 'number value must be numeric'),
])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-finite number values accepted

Low Severity

number custom fields validate numeric strings with Number.isFinite, but JSON numbers accepted by z.number() are not required to be finite. Values such as overflow Infinity pass the schema, are returned unchanged from serializeJiraCustomField, and JSON.stringify turns them into null in the Jira PUT body instead of a valid number.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 552bc9c. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant