Skip to content

LSA-9069 — Jira Software + ServiceNow opt-in outbound create

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Story / ticket: LSA-9069 — Ticketing — Jira Software / ServiceNow opt-in outbound create (not bidirectional v1) Estimated PR size: large (~2000–2500 LOC) Owner: @floh/server connectors + itsm-sync; operator docs; admin ticket detail Spec / design doc: clone docs/plans/2026-08-28-lsa-9894-plane-outbound-create.md + ADR-014 Parent epic: LSA-9065 Blocked by: LSA-9894 (link table, {{ticket.*}}, Plane persist) — present on this branch

Product: same as Plane — catalog create_ticket plus an explicit connector createWorkItem step. Jira Software issues (POST /rest/api/3/issue), not JSM request APIs. ServiceNow v1 is incident. Local e2e: Jira Cloud free site + ServiceNow PDI (option A), not Docker. One PR ships both vendors.


1. Goal

Operators can add a jira and/or servicenow connector instance and bind createWorkItem on catalogs that should open engineering/IT work in that system. Success writes ticket_external_link (external_system jira or servicenow). Admin ticket detail lists every stored link (Plane + Jira + ServiceNow). Email inbound and POST /api/tickets still do not create remote items. No inbound webhooks, no status PATCH, no comments.

1a. Why Jira Software issues (not JSM requests)

Plane v1 opens a work item in an engineering project, not a second intake form. Floh is already the customer-facing ticket of record. The Jira peer matches that role: POST /rest/api/3/issue. JSM /rest/servicedeskapi/request is a second service desk and is deferred (jira_sm CHECK value reserved). ServiceNow v1 stays incident, not sc_req_item.


2. Scope contract

Hard rule: the implementation MUST NOT modify files outside this list.

In scope (will modify)

File Reason
docs/plans/2026-09-02-lsa-9069-jira-servicenow-outbound-create.md This plan
docs/architecture/decisions.md ADR-014 one-paragraph update
docs/connectors/jira-connector.md Operator setup
docs/connectors/servicenow-connector.md Operator setup
docs/connectors/README.md Index rows
packages/server/src/db/migrations/20260908120000_ticket_external_link_jira_system.ts Widen CHECK to include jira
packages/server/src/modules/itsm-sync/constants.ts EXTERNAL_SYSTEM_JIRA
packages/server/src/modules/itsm-sync/itsm-create-handler.ts Dispatch table
packages/server/src/modules/itsm-sync/persist-itsm-link.ts Generic persist
packages/server/src/modules/itsm-sync/persist-plane-link.ts Plane wrapper
packages/server/src/modules/itsm-sync/plane-create-work-item-step.ts Shared prepare
packages/server/src/modules/itsm-sync/jira-url.ts Browse URL
packages/server/src/modules/itsm-sync/servicenow-url.ts Incident URL
packages/server/src/modules/itsm-sync/resolve-plane-connector.ts Fallback URLs for jira/SN
packages/server/src/modules/itsm-sync/README.md Module doc
packages/server/src/modules/workflows/step-executor.ts Dispatch table instead of plane-only if
packages/server/src/modules/connectors/handlers/jira.ts defineConnector
packages/server/src/modules/connectors/handlers/jira-support/** Client, URL validator, config
packages/server/src/modules/connectors/handlers/servicenow.ts defineConnector
packages/server/src/modules/connectors/handlers/servicenow-support/** Client, URL validator, config
packages/server/src/modules/connectors/handlers/index.ts Register both
packages/web/src/app/features/tickets/ticket-detail.component.ts List all external links
packages/web/src/app/features/tickets/ticket-detail.component.spec.ts Multi-link spec
Unit tests under packages/server/test/unit/connectors/ and …/itsm-sync/ Mirror Plane
packages/server/test/unit/step-executor-connector.test.ts Persist mock path

Out of scope (will NOT modify in this PR)

File / area Why deferred Tracking ticket
Jira Service Management request types / service desk API Product chose Software issues Future (reserved jira_sm)
OAuth 2.0 Clone Plane: t.secret API token / password Defer
Inbound HMAC, CSRF-exempt routes, status PATCH, comments Same park as 9895/9896 LSA-9895 / LSA-9896 analogues later
Auto-create on every Floh ticket Opt-in step only —
Portal My Tickets URLs Admin externalLinks already —
Configurable ServiceNow table name v1 = incident only —
Assignee / attachment sync Not asked —
In-repo Docker Compose for Jira or ServiceNow SN cannot run locally; Jira DC eval self-serve ended 2026-03-30 Manual e2e only

Adjacent code that would benefit from a sweep but is explicitly NOT included

  • Plane inbound HMAC / status PATCH (LSA-9895).
  • Renaming resolve-plane-connector.ts (keep filename; extend fallbacks).

3. Invariants matrix

# Invariant Enforcement layer Covering test
1 Outbound HTTP uses connectorHttpRequest only (auth: basic for Jira and SN). Clients + no-raw-fetch-in-modules jira-client.test.ts, servicenow-client.test.ts
2 Jira apiToken and SN password are t.secret; never logged; Basic auth only. defineConnector + errorMap handler secret-flag + redaction
3 baseUrl is origin; shared URL validator; private net needs allowPrivateNetworkEndpoint; strip trailing /rest/api/3 (Jira) and /api/now (SN). Config resolvers SSRF / suffix tests
4 Create only from explicit jira/servicenow createWorkItem step; catalog/email/POST /api/tickets do not call them. No new hooks existing hook-site specs stay green
5 Reserved {{ticket.*}} bind: engine overwrites externalId / ticketNumber; missing snapshot fails before HTTP. Shared prepare create-step tests
6 Local adopt: existing (ticket_id, system) link completes with stored ids, no HTTP. Prepare create-step tests
7 Remote idempotency: Jira lookup by label floh-{ticketUuid} then create; SN correlation_id = ticket UUID then create. Clients client lookup+create tests
8 Persist unique-violation OK only when stored external_id matches. Persist helper persist tests
9 Jira create id (numeric issue id) and key required; SN sys_id + number required. Parse guards malformed-body tests
10 Logs: ids/keys/status only — never summary, description, token, password. Clients + errorMap redaction assertions
11 Disabled connector fails before HTTP (same as Plane). isConnectorDisabled existing step-executor
12 Jira projectKey allowlisted (/^[A-Z][A-Z0-9_]+$/); SN table hardcoded incident. Runtime guard unsafe-slug tests
13 Manager GET still omits externalLinks without ticket:manage; stored web_url preferred. Existing route existing + URL-builder tests

4. Persistence- and runtime-boundary matrix

Boundary Reads / writes Validator Test
Migration CHECK ticket_external_link.external_system SQL CHECK migration up/down
Connector execute existing authenticated routes existing handler tests
step-executor persist after createWorkItem writes link row payload parse + unique match persist + step-executor
No new public route prefix — routes not registered §4b

4a. Platform contract checklist

Applies: persistence (CHECK migration) + connector secrets + operator docs.

# Topic Yes / No / N/A Where enforced / documented
1 env vars N/A Connector row fields, not env
2 secrets Yes Jira apiToken, SN password via t.secret
3 bootstrap N/A loadBuiltInConnectors already
4 shutdown N/A No new worker
5 test-only hooks N/A No new hooks
6 redaction Yes Invariant 10; client + handler tests
7 Docker N/A No Compose peers (option A)
8 public-export surface Yes handlers/index.ts built-in seed only
9 operator runbook Yes docs/connectors/jira-connector.md, servicenow-connector.md
10 CI two-step N/A Existing vitest; no new required check
11 rollback Yes down() restores CHECK; disable instance / remove step
12 PII Yes Never log description/summary; externalLinks manage-only

4b. Negative space

Behavior the PR does NOT support How accidental support is prevented Follow-up ticket
JSM /request API No servicedeskapi client; jira_sm unused Future JSM story
OAuth Fields not declared Defer
Inbound / CSRF /api/jira-ticketing or /api/servicenow-ticketing Routes not registered Later
updateWorkItem / comments Commands absent Later
SN tables other than incident Hardcoded path Defer
Auto-create No ticket-route enqueue —

5. File map

docs/plans/2026-09-02-lsa-9069-jira-servicenow-outbound-create.md  [create]
docs/architecture/decisions.md                                     [modify]
docs/connectors/jira-connector.md                                  [create]
docs/connectors/servicenow-connector.md                           [create]
docs/connectors/README.md                                          [modify]
packages/server/src/db/migrations/20260908120000_ticket_external_link_jira_system.ts [create]
packages/server/src/modules/itsm-sync/                             [modify + create]
packages/server/src/modules/workflows/step-executor.ts              [modify]
packages/server/src/modules/connectors/handlers/jira.ts             [create]
packages/server/src/modules/connectors/handlers/jira-support/     [create]
packages/server/src/modules/connectors/handlers/servicenow.ts     [create]
packages/server/src/modules/connectors/handlers/servicenow-support/ [create]
packages/server/src/modules/connectors/handlers/index.ts            [modify]
packages/web/src/app/features/tickets/ticket-detail.component.ts     [modify]
packages/server/test/unit/connectors/jira-*.test.ts               [create]
packages/server/test/unit/connectors/servicenow-*.test.ts        [create]
packages/server/test/unit/itsm-sync/*.test.ts                      [modify + create]
packages/server/test/unit/step-executor-connector.test.ts          [modify]

6. Implementation steps

Step 1 — Plan + CHECK + dispatch

  • Write this plan; rewrite LSA-9069 Jira AC.
  • Widen CHECK; add jira to EXTERNAL_SYSTEMS.
  • Extract ITSM_CREATE_HANDLERS + generic prepare/persist; wire step-executor.

Verification:

  • [ ] Plane unit tests still pass.
  • [ ] preparePlaneCreateWorkItemStep / persist-plane wrappers remain.

Step 2 — Jira connector

  • jira handler: test + createWorkItem; Basic auth; label lookup; persist jira.

Verification:

  • [ ] pnpm --filter @floh/server run test:unit -- jira passes.

Step 3 — ServiceNow connector

  • servicenow handler: test + createWorkItem; correlation_id lookup; persist servicenow.

Verification:

  • [ ] pnpm --filter @floh/server run test:unit -- servicenow passes.

Step 4 — Admin UI + docs

  • List all externalLinks; operator docs; ADR-014.

Verification:

  • [ ] Ticket-detail spec covers Plane + Jira links.

7. Testing strategy

  • Unit tests: URL/SSRF, Basic auth not logged, create/lookup/adopt, malformed id, unique conflict mismatch, prepare without snapshot, persist failure after HTTP success.
  • Architectural tests: existing no-raw-fetch-in-modules and connector-url-validators-userinfo cover new validate-*.ts files.
  • Integration tests: not required — CI stays mocked HTTP. Manual e2e is Jira Cloud free + ServiceNow PDI.

7.5 Malformed-input matrix

# Input source Trust boundary Malformed shape Expected behavior Spec file / it(...) §3 inv.
1 Jira baseUrl validateJiraBaseUrl userinfo / metadata IP fail closed, no HTTP validate-jira-base-url.test.ts 3
2 Jira projectKey resolveJiraConfig / or .. fail closed jira-handler.test.ts unsafe projectKey 12
3 Jira create body parseJiraIssue missing id or key step fail, no link jira-client.test.ts missing id/key 9
4 SN create body parseIncident missing sys_id/number step fail servicenow-client.test.ts 9
5 Step externalId interpolated prepareItsmCreateWorkItemStep other ticket UUID overwritten from snapshot create-step tests 5
6 JQL/SN query prepare already failed non-UUID ticket id never interpolated prepare tests 5

8. Stopping criteria for the autonomous loop

  • [ ] Zero unresolved review comments tagged CRITICAL or MAJOR.
  • [ ] Every invariant in §3 has a passing test.
  • [ ] All CI checks (typecheck, lint, unit, integration, architectural) are green.
  • [ ] Every modified file is listed in §2 (scope contract).
  • [ ] The PR description references this plan doc.

Reassess rule — newly-raised MAJOR findings

If two consecutive review rounds raise NEW MAJOR findings, halt and reassess against §4b (do not add bidirectional sync, JSM, or OAuth).


9. Acceptance gates (user-visible)

  • [ ] Catalog with jira / createWorkItem after create_ticket creates a Jira issue; admin sees key + browse URL.
  • [ ] Same for ServiceNow incident number + URL.
  • [ ] Second step run adopts; no duplicate remote item.
  • [ ] Catalog without those steps: Floh ticket only.
  • [ ] Forged/unauthenticated “webhook” has no route.

10. Risks and unknowns

  • Large PR. User chose one PR; keep commits sequential (dispatch → Jira → SN) so review can be chunked.
  • Jira ADF. Minimal one-paragraph ADF; do not invent a full wiki converter.
  • Jira labels. floh-{uuid} must be unique enough; do not use summary-equality for adopt.
  • Epic vs story drift. Epic LSA-9065 still says bidirectional + OAuth + 60s auto-create. Do not implement the epic AC.
  • Local e2e is cloud, not Docker. Locked to option A. CI stays mocked HTTP.

10a. Local development (option A — locked)

Peer Instance Connector baseUrl Auth
Jira Software Jira Cloud free (https://<site>.atlassian.net) that origin email + API token (Basic)
ServiceNow PDI (https://devXXXXX.service-now.com) that origin PDI admin Basic

11. Out-of-band cleanups deferred from this PR

  • Parked Plane sync: LSA-9895 / LSA-9896 unchanged.
  • JSM request-type connector: new story when product asks (uses reserved jira_sm).
  • Comment on LSA-9065 AC vs shipped opt-in model.

12. Sign-off

  • Author — agent: clone of 9894 for Jira Software + SN incident, one PR.
  • Architectural reviewer (human) — confirmed via Cursor plan lsa-9069_jira_servicenow_95d36879 before Agent implementation.
  • Platform reviewer (human) — required (§4a/§4b): CHECK migration + secrets + operator docs. Recorded with the Cursor plan approval that started this implementation.