/claim #2281
Adds a new Jira Service Management (JSM) connector that indexes service desk tickets — service requests, incidents, problems, and changes — with full SLA enrichment, dynamic field discovery, and complete frontend support.
Jira Software and Jira Service Management are distinct products with different data models. JSM tickets have additional metadata that the existing Jira connector does not capture:
Without a dedicated JSM connector, indexing a service desk project with the existing Jira connector loses all SLA and request-type context — making it harder for support agents and end-users to search and understand ticket history.
JiraServiceManagementConnector extends JiraConnector directly, inheriting all production-tested capabilities for free:
| Capability | Inherited from JiraConnector |
|---|---|
| Checkpoint-based incremental sync | ✅ |
| Slim document support (pruning) | ✅ |
| Permission sync (EE) | ✅ |
| JQL flexibility | ✅ |
| Comment indexing | ✅ |
| Hierarchy nodes (epics/projects) | ✅ |
| Error handling / credential validation | ✅ |
The connector only overrides what JSM needs: _get_jql_query (adds JSM issue type filter) and adds _enrich_document_with_jsm_metadata (adds JSM-specific fields to indexed documents).
This is the critical design decision that differentiates this implementation.
Previous attempts hard-coded JSM custom field IDs like customfield_10010, customfield_10020, customfield_10030. This is fundamentally broken: Jira custom field IDs are assigned sequentially at instance creation time and differ between instances. A field that is customfield_10010 on one instance may be customfield_20045 on another.
This connector discovers field IDs dynamically via jira_client.fields() (the standard JIRA Python library API, wrapping /rest/api/2/field), matching by field name patterns:
_FIELD_NAME_REQUEST_TYPE = ("request type", "customer request type")
_FIELD_NAME_TIME_TO_FIRST_RESPONSE = ("time to first response",)
_FIELD_NAME_TIME_TO_RESOLUTION = ("time to resolution",)
The result is cached per connector instance after the first call — zero overhead on subsequent syncs.
def _discover_jsm_fields(self) -> dict[str, str]:
if self._jsm_field_map is not None:
return self._jsm_field_map # cached
all_fields = self.jira_client.fields() # single API call
for field in all_fields:
field_name = (field.get("name") or "").lower()
if any(pat in field_name for pat in _FIELD_NAME_REQUEST_TYPE):
field_map["request_type"] = field.get("id")
# ... etc
self._jsm_field_map = field_map # cache
return field_map
If field discovery fails (permissions issue, older Jira version), the connector logs a warning and continues — SLA enrichment is skipped gracefully, core ticket indexing still works.
JSM SLA fields have different structures across Cloud and Server/DC:
Cloud format (nested dict):
{
"ongoingCycle": {
"breached": false,
"remainingTime": {"friendly": "2h 30m"}
}
}
Server/DC format (plain string): "3h remaining"
The _extract_sla_display() helper handles both formats, plus completed cycles (for resolved tickets), with breach flag propagation:
def _extract_sla_display(sla_value: Any) -> tuple[str | None, bool]:
# Cloud: ongoingCycle
if isinstance(sla_value, dict):
ongoing = sla_value.get("ongoingCycle")
if isinstance(ongoing, dict):
breached = ongoing.get("breached", False)
remaining = ongoing.get("remainingTime", {})
return remaining.get("friendly"), breached
# Cloud: completedCycles (resolved tickets)
completed = sla_value.get("completedCycles")
if completed:
last = completed[-1]
return last.get("elapsedTime", {}).get("friendly"), last.get("breached", False)
# Server: plain string
if isinstance(sla_value, str):
return sla_value, False
return None, False
When no custom JQL is provided, the connector automatically filters to JSM issue types:
(project = "SD" AND updated >= "2025-01-01 00:00" AND updated <= "2026-01-01 00:00")
AND issuetype in ("Service Request", "Service Request with Approvals", "Incident", "Problem", "Change")
When the user provides a custom JQL query, it is passed through unchanged (no filter injection) — giving full flexibility for advanced use cases.
backend/onyx/connectors/jira_service_management/__init__.py
Package init.
backend/onyx/connectors/jira_service_management/connector.py
Main connector implementation (~270 lines):
JiraServiceManagementConnector class extending JiraConnector_discover_jsm_fields() — dynamic field discovery with caching_enrich_document_with_jsm_metadata() — adds request type + SLA to indexed docs_extract_sla_display() — Cloud/Server SLA format handling with breach detection_extract_request_type_name() — handles Cloud dict, Server string, resource object_get_jql_query() override — injects JSM issue type filterprocess_jsm_issue() — convenience wrapper composing base + enrichmentbackend/tests/daily/connectors/jira_service_management/__init__.py
Test package init.
backend/tests/daily/connectors/jira_service_management/test_jsm_connector.py
25 unit tests across 5 test classes:
| Class | Tests |
|---|---|
TestJSMConnectorInitialization |
6 tests: init, inheritance, service_desk_id, optional fields, credential loading, DocumentSource enum |
TestDynamicFieldDiscovery |
4 tests: success, failure graceful, caching (fields() called once), partial fields |
TestJSMMetadataEnrichment |
4 tests: request type enrichment, full SLA extraction, missing SLA graceful, source override |
TestJQLGeneration |
3 tests: JSM types injected, with project key, custom JQL passthrough |
TestSLAHelpers |
8 tests: ongoing cycle, breached, completed cycle, string format, None, request type dict/string/None |
backend/onyx/configs/constants.py
JIRA_SERVICE_MANAGEMENT = "jira_service_management" to DocumentSource enum"jira service management data (service requests, incidents, problems, changes, SLAs)"backend/onyx/connectors/registry.py
DocumentSource.JIRA_SERVICE_MANAGEMENT → JiraServiceManagementConnector mappingweb/src/lib/sources.ts
jira_service_management entry with Jira icon, “Jira Service Management” display name, TicketingAndTaskManagement categoryweb/src/lib/connectors/connectors.tsx
web/src/lib/connectors/credentials.ts
JiraServiceManagementCredentialJson interface (jira_user_email, jira_api_token)credentialTemplatesFor end-to-end validation against a real JSM instance:
# Set environment variables
export JIRA_BASE_URL="https://your-domain.atlassian.net"
export JIRA_USER_EMAIL="your-email@domain.com"
export JIRA_API_TOKEN="your-api-token"
export JSM_PROJECT_KEY="SD" # your JSM project key
# Run the daily connector test
cd backend
python -m pytest tests/daily/connectors/jira_service_management/ -v
# Or run unit tests (no Jira instance needed)
python -m pytest tests/daily/connectors/jira_service_management/test_jsm_connector.py -v
Unit test output (no Jira instance required):
tests/daily/connectors/jira_service_management/test_jsm_connector.py::TestJSMConnectorInitialization::test_connector_initialization PASSED
tests/daily/connectors/jira_service_management/test_jsm_connector.py::TestJSMConnectorInitialization::test_inherits_jira_connector PASSED
tests/daily/connectors/jira_service_management/test_jsm_connector.py::TestJSMConnectorInitialization::test_service_desk_id_stored PASSED
tests/daily/connectors/jira_service_management/test_jsm_connector.py::TestJSMConnectorInitialization::test_service_desk_id_optional PASSED
tests/daily/connectors/jira_service_management/test_jsm_connector.py::TestJSMConnectorInitialization::test_credential_loading PASSED
tests/daily/connectors/jira_service_management/test_jsm_connector.py::TestJSMConnectorInitialization::test_document_source_enum_exists PASSED
tests/daily/connectors/jira_service_management/test_jsm_connector.py::TestDynamicFieldDiscovery::test_dynamic_field_discovery_success PASSED
tests/daily/connectors/jira_service_management/test_jsm_connector.py::TestDynamicFieldDiscovery::test_dynamic_field_discovery_failure_graceful PASSED
tests/daily/connectors/jira_service_management/test_jsm_connector.py::TestDynamicFieldDiscovery::test_dynamic_field_discovery_cached PASSED
tests/daily/connectors/jira_service_management/test_jsm_connector.py::TestDynamicFieldDiscovery::test_discovery_partial_fields PASSED
... (25/25 PASSED)
| Feature | This PR | promisingcoder #9536 | Raw9982 #9121 | zhaog100 #9437 |
|---|---|---|---|---|
| Inherits JiraConnector | ✅ | ✅ | ❌ (copy-paste) | ❌ (custom client) |
| Dynamic field IDs | ✅ | ❌ hard-coded | ❌ hard-coded | ❌ hard-coded |
| Frontend (connectors.tsx) | ✅ | ❌ | ✅ | ❌ |
| Frontend (sources.ts) | ✅ | ❌ | ✅ | ❌ |
| credentials.ts interface | ✅ | ❌ | ❌ | ❌ |
| Unit tests | ✅ 25 tests | ✅ 3 tests | ❌ | ❌ |
| Cloud + Server SLA formats | ✅ | ✅ (partial) | ❌ | ❌ |
| Breach detection | ✅ | ✅ | ❌ | ❌ |
| Graceful field discovery fail | ✅ | ❌ | ❌ | ❌ |
| Field discovery caching | ✅ | N/A | N/A | N/A |
customfield_XXXXX)DocumentSource enum updatedregistry.py updatedsources.ts, connectors.tsx, credentials.tsAdds a Jira Service Management (jira_service_management) connector to index service desk tickets with request type and SLA details via dynamic field discovery. Enrichment runs through the Jira connector’s _enrich_document hook so every synced ticket includes JSM metadata; addresses #2281.
New Features
JiraServiceManagementConnector extends the Jira connector and enriches documents via _enrich_document; filters to JSM issue types.sla_breached; returns “Breached” when breached with no display.service_desk_id is set without project_key or custom JQL, injects project = <service_desk_id>; with custom JQL + service_desk_id, still appends the JSM filter; otherwise passthrough.Migration
jira_user_email and jira_api_token.Written for commit 902772e7d5cc63b2421920d55a0c292ed2f333d0. Summary will update on new commits.
CharlesWong
@CharlesWong
Onyx (YC W24)
@onyx-dot-app