Skip to content

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

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

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

Conversation

@mzxchandra

Copy link
Copy Markdown
Contributor

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.
@vercel

vercel Bot commented Jul 24, 2026

Copy link
Copy Markdown

@mzxchandra is attempting to deploy a commit to the Sim Team on Vercel.

A member of the Team first needs to authorize it.

@cursor

cursor Bot commented Jul 24, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes live Jira issue update payloads and validation; mistakes could cause failed or incorrect field writes, though legacy behavior is preserved and coverage is strong.

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

The update route no longer inlines legacy customFieldId / customFieldValue logic; it merges simple fields and custom fields via buildJiraCustomFields / serializeJiraCustomField in tools/jira/utils.ts. Request validation adds jiraCustomFieldEntrySchema (per-type value shapes, cascading ambiguity rules, max 50 entries). Legacy single-field params remain behavior-compatible; customFields wins on the same field id.

Tool params, types, and broad unit/route tests cover serialization, 400s on bad input, backward compatibility, and combined simple + custom payloads.

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

@greptile-apps

greptile-apps Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Extends Jira issue updates with validated, type-aware structured custom fields.

  • Adds serializers for option, user-picker, cascading, numeric, text, and raw custom-field values.
  • Merges multiple structured fields, legacy custom fields, and standard issue fields into one Jira update request.
  • Tightens boundary validation for cascading aliases and child representations.
  • Adds route and utility coverage for serialization, validation, precedence, and backward compatibility.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failures remain in the fixes associated with the previous review threads.

Important Files Changed

Filename Overview
apps/sim/lib/api/contracts/selectors/jira.ts Adds type-discriminated custom-field validation and rejects ambiguous cascading parent and child representations.
apps/sim/tools/jira/utils.ts Adds structured Jira custom-field serialization, field-ID normalization, and legacy/structured field merging.
apps/sim/app/api/tools/jira/update/route.ts Incorporates structured custom fields into the existing combined Jira issue update payload.
apps/sim/tools/jira/update.ts Exposes structured custom fields through the Jira update tool and forwards them to the API route.
apps/sim/app/api/tools/jira/update/route.test.ts Covers route-level validation, serialization, compatibility, collision precedence, and combined field updates.
apps/sim/tools/jira/utils.test.ts Covers each custom-field serializer and the legacy/structured merge behavior.

Reviews (7): Last reviewed commit: "fix(jira): also reject cascading child c..." | Re-trigger Greptile

Comment thread apps/sim/lib/api/contracts/selectors/jira.ts Outdated
Comment thread apps/sim/tools/jira/utils.ts
…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.
@mzxchandra

Copy link
Copy Markdown
Contributor Author

@greptile

@mzxchandra

Copy link
Copy Markdown
Contributor Author

@cursor review

Comment thread apps/sim/lib/api/contracts/selectors/jira.ts Outdated
Comment thread apps/sim/lib/api/contracts/selectors/jira.ts Outdated
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.
@mzxchandra

Copy link
Copy Markdown
Contributor Author

@greptile

@mzxchandra

Copy link
Copy Markdown
Contributor Author

@cursor review

Comment thread apps/sim/lib/api/contracts/selectors/jira.ts Outdated
Comment thread apps/sim/lib/api/contracts/selectors/jira.ts Outdated
Comment thread apps/sim/lib/api/contracts/selectors/jira.ts
… 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.
@mzxchandra

Copy link
Copy Markdown
Contributor Author

@greptile

@mzxchandra

Copy link
Copy Markdown
Contributor Author

@cursor review

Comment thread apps/sim/lib/api/contracts/selectors/jira.ts
Comment thread apps/sim/lib/api/contracts/selectors/jira.ts
…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.
@mzxchandra

Copy link
Copy Markdown
Contributor Author

@greptile

@mzxchandra

Copy link
Copy Markdown
Contributor Author

@cursor review

Comment thread apps/sim/lib/api/contracts/selectors/jira.ts Outdated

@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.

✅ Bugbot reviewed your changes and found no new issues!

1 issue from previous review remains unresolved.

Fix All in Cursor

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 8743c54. Configure here.

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.
@mzxchandra

Copy link
Copy Markdown
Contributor Author

@greptile

@mzxchandra

Copy link
Copy Markdown
Contributor Author

@cursor review

Comment thread apps/sim/lib/api/contracts/selectors/jira.ts
Comment thread apps/sim/lib/api/contracts/selectors/jira.ts
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.
@mzxchandra

Copy link
Copy Markdown
Contributor Author

@greptile

@mzxchandra

Copy link
Copy Markdown
Contributor Author

@cursor review

@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 default 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 19cd38e. Configure here.

Comment thread apps/sim/lib/api/contracts/selectors/jira.ts
Comment thread apps/sim/tools/jira/update.ts
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.
@mzxchandra

Copy link
Copy Markdown
Contributor Author

@greptile

@mzxchandra

Copy link
Copy Markdown
Contributor Author

@cursor review

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