Ticket systems — Jira Cloud, ServiceNow, Azure DevOps
Connect Jira Cloud, ServiceNow or Azure DevOps so every incident gets one ticket: filed at open, the analysis written back and the ticket closed at resolve.
How-to · Updated 2026-10-27 · Describes release 0.1.164
Ticket systems
Connect a ticket system once and every incident gets one ticket for its whole life: filed when the incident opens, the finished analysis written back and the ticket closed when the incident resolves. Two exceptions come from built-in detection: a held warning files its ticket only once the hold lifts, and an incident detection stopped watching keeps its ticket open (see What happens). Nothing to hand-write: the Ticket system preset discovers the project, issue type, priorities and close path, and proves them before it saves.
The incident page names the ticket the way its system does (Jira OPS-12, ServiceNow INC0010002, Azure DevOps #123).
What happens
| Moment | What the platform does |
|---|---|
| An incident opens | Files one ticket (Jira issue, ServiceNow incident, Azure DevOps work item) and records its key, browse link and API id on the incident. A re-fired alert never files a second one. A built-in detection warning is held: no ticket until Analyse, or an unheld alert joining the incident, lifts the hold — then it files as an open would. An unidentified service incident (alerts that name no service wait in the triage queue, on by default since 0.1.133) files no ticket until Analyse. |
| The incident resolves | Writes the analysis back (the RCA, the remediation pull requests and the validation verdicts, scrubbed) and closes the ticket. When a person resolved it with a note, the note comes first, under Resolution note, and the analysis follows under AIOps analysis. Only an analysis that finished counts: a stopped one (Stop this run, the cost cap) wrote a stop notice, not a root cause, so the write-back takes the latest analysis that finished, else says No finished analysis was recorded. |
| Built-in detection stops watching the incident (Turn off, a rule switched off, a connection opted out, a resource out of scope) | The incident resolves, but nothing was seen to clear: the write-back says so and leaves the ticket open for its owner. An advanced route waits for a person (Write back). |
| The incident came in from a ticket (an inbound webhook — the customer's own ticket) | Writes the analysis back to that ticket as an internal note and leaves its status alone: closing someone's own ticket is their call, never an automatic resolve path's. When its owner resolves that ticket in their system, the incident resolves — an escalated one too (see Inbound webhooks and echoes). |
| A ticket comes in without a required field (service, severity, priority, assignee, notify by default) | Holds it in the triage queue and asks on that ticket once for what is missing; the ticket's next update that names them all moves it on (see Tickets that come in must name five fields). |
Every resolve path closes the ticket: a person's Mark resolved or bulk close, the source alert clearing, the quiet-expiry auto-close, the agent's own resolve, the post-fix verification, a merge (accepted or grouping) and a legacy storm split. Built-in detection stopping to watch is not a clear: that ticket gets the note and stays open, and a held warning's clear writes nothing to a ticket an earlier episode filed. (A runner that writes the database directly is caught by the five-minute sweep below.) The one exception is a customer's own ticket with an advanced route — see Advanced routes.
- The analysis comes first. When the source alert clears while the analysis is still running, the write-back waits for it (at most an hour), so the ticket gets the RCA, not "no analysis recorded".
- Once per resolution. A reopened incident keeps its ticket; resolving it again writes the new analysis once more.
- Merges and splits say where the story went. A merged duplicate's ticket
moves to the primary when the primary has none; otherwise it closes with
"Merged by AIOps into incident …" (ServiceNow: the
Duplicateresolution code when the instance has one); an incident grouped under the service it depends on is a merge too. A legacy storm incident emptied by its split closes with "Split by AIOps into incidents …". - Scope. The preset files tickets for incidents the Incident agent handles.
Compliance and security findings are not ticketed by it (a dirty account's first
scanner pull would otherwise open dozens). Over the API, the route's
params.pillarslists the agents it covers (default["incident"];compliance,secopsand the other agent keys are accepted); saving the route again in the Tickets dialog sets it back to the default. Advanced routes can cover other pillars.
Connect the ticket system
Jira Cloud
| Field | Value |
|---|---|
| Site URL | https://<your-site>.atlassian.net (a pasted browser URL is cut back to the site) |
| Account email | the account the tickets are filed as — a dedicated one reads best |
| API token | an unscoped token from https://id.atlassian.com/manage-profile/security/api-tokens |
- Tokens expire after at most one year. An expired token turns the connection
red on the Connections page (and in the setup checklist) the first time a ticket
call is refused and a fresh identity read (
/rest/api/2/myself) is refused too; create a new one and press Test. - Scoped tokens are not supported yet. Atlassian serves them only through
api.atlassian.com, so on the site URL they answer401. - Project permissions needed: Browse projects, Create issues, Add comments,
Transition issues, and Resolve issues to set a resolution. Check reads them
for the chosen project (
mypermissionswithprojectKey). - The create screen of the chosen issue type must show Labels and Description (the platform marks and finds its tickets through them), and must have no required field without a default. Check blocks the save otherwise.
- Company-managed and team-managed projects both work. Jira Data Center / Server is not supported by the preset (it lacks the Cloud search API): use an advanced route.
ServiceNow
| Field | Value |
|---|---|
| Instance URL | https://<instance>.service-now.com |
| Integration user | a user with the itil role and Web service access only ticked |
| Password | its password |
- The itil role is enough. Verified end to end on a Zurich instance: a user with
only itil and Web service access only ticked passed Check, filed the
incident, wrote the analysis back and closed it — basic auth to the Table API
needed no extra REST role. Only an instance that enforces REST roles also needs
snc_basic_auth_api_access. - Keep the ticket credential off the agents' read connection. An itil user can update any incident. Connect it as a separate connection with the write (remediation-only) role — agents never see write-role connections — and keep the agents' ServiceNow read connection on a read-only user. The ticket dialog picks a write-role connection first when one exists. The same holds for a Jira token. When the agents should also read ServiceNow, that makes two connections: the itil user for tickets (Use this connection for = Approved changes only (separate credential)) and a read-only user for the agents (Investigate only). Leave Allow use for approved plans off on the ticket connection and save it with Save remediation credential: a ticket route files and closes tickets and never runs an approved change, so it needs no execution consent and no certification, whatever the connection's role.
- Check also reads
sys_user(the caller), the resolution codes andsys_user_group(the optional assignment group). The resolution codes come fromsys_choice; an itil user may not query that table (Zurich answers403), so Check then reads them from the incident form's metadata (GET /api/now/ui/meta/incident, which itil can read) and still finds theDuplicatecode a merged incident closes with. Only when both are refused does it warn and use Solution provided. - A personal developer instance (PDI) is free but may sit on a waitlist for days, hibernates when idle and is reclaimed after 10 days without a sign-in. Wake it in the developer portal before testing: a sleeping instance answers with a web page, which the platform reports as such, and a just-woken one is slow on its first write (the first test ticket's create took over 8 seconds).
Azure DevOps
| Field | Value |
|---|---|
| Organization URL | https://dev.azure.com/<organization> |
| PAT | Work Items: Read & write (add Code: Read & write if the same connection opens remediation pull requests) |
Set it up
- Data sources → the connection's Tickets action (or Use for tickets after a green Test).
- Leave the project empty and press Check to list the projects the account can file into; pick one (ServiceNow: optionally type an assignment group).
- Check again. It is read-only and shows what it found: identity, project, permissions, issue or work item type, priorities, the close path. Anything in red keeps Save disabled.
- Save ticket route. Only one route files a ticket per incident — the first that matches, by priority. The dialog lists the other routes that also file when an incident opens and offers to stop them filing (checked for a new ticket system). A stopped route keeps its on resolve half, so the tickets it already filed still get their write-back; close-only routes are never listed. A route you keep stays on for the incidents this one does not cover: the new route is saved ahead of it.
- Send test ticket files a real ticket and closes it again within seconds,
proving create, write-back and close end to end. It is audited
(
route.test_ticket). It works on a disabled route too, so you can prove a connection before any incident is ticketed through it: the answer then ends This route is disabled, so no incident is ticketed through it until you enable it. (route_enabled: falseover the API). Sending it never enables the route. A create gets up to 20 seconds (a just-woken developer instance is slow). One that still goes unanswered may have landed: on ServiceNow, whose search sees a create at once, the test looks for the ticket's mark straight away and closes what landed (filed and closed … — a search found it); when nothing is found it says so and names the mark (aiops:test-…) to look for. Jira and Azure DevOps report the timeout as before.
The route is saved as an ordinary route with params.preset, on_open and
on_resolve; the routes page lists it as Ticket system · Jira OPS and edits it in
the same dialog.
Over the API
Each call needs datasources.manage (and a write token).
- Check:
POST /api/aiops/{tenant_id}/datasources/{datasource_id}/ticket-discovery. The body{}lists what to pick underchoices(the projects, for Jira and Azure DevOps); send the picks the same way: Jiraprojectand optionallyissue_type(an id fromchoices), Azure DevOpsprojectand optionallywork_item_type, ServiceNow optionallyassignment_groupandclose_code. Go on only when the answer'sblockinglist is empty. - Save:
POST /api/aiops/{tenant_id}/routeswith the answer'sparamsunchanged: they carrypresetand everything Check found (priorities, the caller, the close path). The values below are examples of what Check returns. - Send test ticket:
POST /api/aiops/{tenant_id}/routes/{route_id}/test-ticket.
ServiceNow (only preset is required; Check adds close_code, and
caller_sys_id, assignment_group_sys_id and duplicate_close_code when it found
them):
{"name": "Tickets · ServiceNow", "mode": "api", "datasource_id": "<servicenow connection id>",
"on_open": true, "on_resolve": true,
"params": {"preset": "servicenow", "close_code": "Solution provided"}}
Jira (project and issue_type_id are required; Check also adds fillable, the
create-screen fields among components and assignee the platform may fill):
{"name": "Tickets · Jira OPS", "mode": "api", "datasource_id": "<jira connection id>",
"on_open": true, "on_resolve": true,
"params": {"preset": "jira", "project": "OPS", "issue_type_id": "10004", "issue_type": "Task",
"priorities": ["Highest", "High", "Medium", "Low", "Lowest"]}}
Azure DevOps (project, work_item_type and close_state are required; Check
also adds text_field, severities, priorities and completed_states):
{"name": "Tickets · Azure DevOps Operations", "mode": "api",
"datasource_id": "<azure-devops connection id>", "on_open": true, "on_resolve": true,
"params": {"preset": "azure-devops", "project": "Operations", "work_item_type": "Bug",
"close_state": "Resolved"}}
A route whose connection is not of the preset's own system, whose mode is not
api, or that lacks a required value answers 422; a missing value reads Run
Check first — the route is missing ….
What gets written
| Jira (REST v2, wiki markup) | ServiceNow (Table API) | Azure DevOps (work items 7.1) | |
|---|---|---|---|
| Title | [AIOps P1] <incident title> (one line, ≤ 255) |
short_description [AIOps P1] … (≤ 160) |
[AIOps P1] … #<8 hex> |
| Body | link back to the incident, priority, severity, service, assignee, notify, alert count, correlation key, opened | the same, plain text | the same, HTML in Repro Steps (Bug) or Description |
| Priority | the project's own name for it (below); none when the project has no priority field | impact + urgency — never priority, which the instance derives |
Severity when the type has it |
| Mark | labels aiops, aiops-<incident id> |
correlation_id aiops:<incident id>, correlation_display AIOps |
tag aiops + the title mark |
| Service | the Component of that name, when the project has exactly one and the create screen shows Components | the configuration item named after the service, when exactly one CI has that name (also the Business service when that CI is a service) | the Service: line |
| Severity | the Severity: line (Jira has no field) |
Severity: critical 1, error 2, warning and info 3 | Severity (above) |
| Assignee | Assignee: the one assignable user whose display name or email is exactly the incident's assignee, when the create screen shows it (Jira Cloud; on Data Center the Assignee: line only) |
the assignment group the incident's assignee names, else Assigned to the user it names (user name or full name); otherwise the route's assignment group | the Assignee: line (Assigned To refuses a name it does not know) |
| Notify | the Notify: line (Jira has no field) |
the Notify: line (the watch list stays empty: no address leaves the platform) |
the Notify: line |
| Also | caller = the integration user | Priority (P1 → 1 … P4 → 4) when the type has it |
The ticket the platform files carries what it requires of a ticket that comes in: service, severity, priority, assignee and notify (see Tickets that come in must name five fields). Every ticket names them as Field: value lines in the body, which every system's intake reads; the table above is where each system's own field is filled as well. A field is filled only with a value the ticket system knows — an exact, unambiguous match looked up when the ticket is filed — because a name it does not know would make it refuse the whole ticket. A lookup that finds nothing, finds two or is refused fills nothing, and the line still names the value.
Assignee is the incident's assignee: set by hand, or when the incident opens by the workspace's assignment map (settings.assignment_map, {"<service glob>": "<assignee>"}). With neither, the ticket has no Assignee line, and the only assignee field filled is a ServiceNow route's own assignment group. The assignee passes the same personal-data scrub as the rest of the body, so an email address arrives as a pseudonym ([REDACTED:email:…]) and is never looked up: put a group or a person's name (for example Payments on-call) in the assignment map. On ServiceNow a group of that name becomes the ticket's assignment group, ahead of the route's own; an unauthorized-change ticket still goes to its configured group, never a person.
Notify names the kinds of channel that deliver a new incident (for example Microsoft Teams, PagerDuty), never an address. It comes from the workspace's notification channels (settings.notifications, edited under Notifications & on-call); with none that takes a new incident, the ticket has no Notify line. A channel that is configured but cannot deliver is named with why, for example none (email configured, not sent: no mail relay): email needs a mail relay (the workspace's own or the platform's), and a pager needs its key stored. A line that starts with none names nothing to a ticket coming in (see Tickets that come in must name five fields). No ticket field gets an address: ServiceNow's watch list stays empty.
The Jira component and assignee need the create screen's fields, which Check records (fillable), and Azure DevOps' Priority needs the type's priority values, which Check reads (priorities): a route saved before this release fills them once you run Check and save it again.
| AIOps | Jira (first present) | ServiceNow impact / urgency | Azure DevOps severity |
|---|---|---|---|
| P1 | Highest, Critical, Blocker | 1 / 1 | 1 - Critical |
| P2 | High, Major | 1 / 2 | 2 - High |
| P3 (and no priority) | Medium, Normal | 2 / 2 | 3 - Medium |
| P4 | Low, Minor | 2 / 3 | 4 - Low |
A Jira scheme named after levels (P1 … P5, P1 - Critical) maps level to
level. Names the platform does not know map by rank (Jira lists a scheme highest
first), and Check shows the mapping it will use either way, with a warning for
the rank case.
How closing works
- Jira: a comment first (the RCA as wiki markup — never raw markdown, where
#is a numbered list), then the workflow's own transition to a status in the Done category. The resolution is set only when that transition's screen has the field (team-managed Done transitions have no screen). An issue a person already moved to Done gets the comment only. The comment is marked internal (sd.public.comment), so on a Jira Service Management request the customer does not see it; it is cut to fit Jira's comment limit, with a pointer to the full report on the incident. - ServiceNow: one
PATCHto state 6 (Resolved) with the resolution code (discovered; Solution provided by default), close notes (Resolution note: … when a person resolved it with one, then the root cause of the latest analysis that finished) and the RCA as plain-text work notes, then the platform reads the answer back: a200that kept the state or dropped the code is a failure ("an ACL or a business rule dropped the field"), never a green tick. An incident already Resolved gets the work note only; Closed (7) or Cancelled (8) is read-only and gets nothing. - Azure DevOps: one
PATCHto the type's first Resolved-category state (else Completed) with the analysis in History; an item already completed gets the History entry only. - A ticket the incident came in from (adopted through an inbound webhook) gets
the note only — the Jira comment, the ServiceNow work note, the Azure DevOps
History entry — never a transition, a state change or close notes. The note opens
with AIOps analysis — incident resolved in AIOps at …, never Resolved by
AIOps: the platform resolved its incident, not their ticket. When a person
resolved the incident in AIOps, either line says so (Resolved by a person in
AIOps at …, incident resolved by a person in AIOps at …). (An advanced
route on such a ticket: see Advanced routes.) That includes
an incident that adopted its ticket before 0.1.117 and kept no record of it: the
alert that brought the ticket in (its
ticket_ref/ticket_url) is the proof. - Computer-assist mode writes nothing: no ticket is filed, written back to or closed. The incident says so ("Not written back to …: Computer-assist mode …").
When something fails
- The incident page says Ticket filing failed: <reason> (or Writing back to … failed) with Retry. Retry acts only on a recorded failure — there is no manual "create ticket" — or on a write-back to a customer's own ticket that an automatic resolve left to a person, where the button reads Write back.
- A refusal (
401,403, a redirect or a web page) is followed by one identity read. Only when that is refused too — a wrong or expired credential — is the connection marked failing (auth_failed); the next green Test clears it. A refusal of one request — a project permission, a record ACL, a PAT without Work Items: write — stays on the incident and never turns red a connection that may also open remediation pull requests. A ServiceNow403Operation Failed (a data policy or a business rule) is reported with ServiceNow's own words. - Rate limits (
429) are waited out perRetry-After(up to 30 s). A5xxor a timeout on a create or a note is not retried in the same turn — the request may have landed. The next attempt searches for the platform's mark first, so a ticket is never filed twice and a note repeats at most once. Retry after such a create waits two minutes (the vendor's search index lags a write), and a search that itself fails keeps the next attempt searching first. - A deploy that interrupts a filing or a write-back leaves a marker; after ten minutes Retry, or the backstop sweep, finishes it — searching first. A filing that never started (queued behind others when the control plane restarted) is marked with the incident itself and filed by the sweep. The sweep runs every five minutes on the control plane, covers incidents resolved in the last 24 hours, oldest first, and tries a failed write-back at most twice before it waits for Retry. A write-back a person started — Mark resolved, bulk close, an accepted merge, Retry or Write back — is left to them: when it fails or is cut short it keeps its reason and waits for their Retry, and the sweep does not retry it. One the post-fix verification started is retried like any other. Five minutes after a create that may have landed it searches for it — and only searches: found, the incident links it (and closes it if the incident has resolved); not found, Retry files it.
- An incident outside the route's scope says No ticket: <reason> (for example Computer-assist mode, which writes nothing anywhere) and offers no Retry. Only an incident with no ticket route at all suggests setting one up.
- Monitoring:
aiops_ticket_operations_total{tenant, system, op, outcome}counts every filing (op="file") and write-back (op="close") by outcome (ok/failed) — see the platform monitoring guide in the delivered documentation for an alert on a run of failures (an expired token stops every filing).
Inbound webhooks and echoes
You can also send tickets in (ServiceNow, Jira and Azure DevOps in the webhook reference): a customer ticket becomes a signal, and its incident adopts that ticket.
Resolved at the source. When the ticket an incident adopted is resolved in its
own system (ServiceNow state 6 or 7, a Jira status in the Done category) and no
other alert of the incident still fires, the incident resolves — an escalated
one too. Telemetry clearing never closes an escalated incident (a person holds it);
here that person closed it, in their ticket system. The escalated incident's
resolution note reads auto-resolved: its ticket INC0010006 was resolved in
ServiceNow (a note a person already wrote on it stays; the fix, if they wrote it
down, is in their ticket) and the incident.resolved audit row carries
by: ticket-resolved; an open or investigating incident closes as any source clear
does. With Keep an incident open while its fix waits for a decision on, an
escalated incident whose change request still waits for a person stays open, and
its timeline records Kept open — a fix waits for a decision once. The
write-back that follows is the usual note on their ticket: its state, resolution
code and close notes stay theirs. A ticket that only joined the incident, or its
own ticket resolved while another alert still fires, leaves an escalated incident
with the person, also once that other alert clears. For Azure DevOps, subscribe
the service hook to both workitem.created and workitem.updated: a work item
moved to Done, Closed, Resolved or Removed resolves the same way, and an update
that does not change the state (a comment, a tag, the assignee) is ignored —
unless the work item waits for a missing field (below).
When the same project feeds both directions, the platform's own tickets come back
through the webhook — the create, every update and the resolve, whether its own
write-back or a person resolved it. No filter is needed: the platform drops the echo, counted as
ticket_echo in the webhook response and the ingest.batch audit row, when
- its mark names one of the tenant's incidents whose ticket is this one, or whose ticket is still being filed (the webhook can beat the platform's own record of it). A mark alone proves nothing: Jira Clone and Azure DevOps Create copy copy it onto a new ticket, which is a new signal;
- or it is a ticket the platform filed (same system, same site);
- or the platform wrote to it in the last five minutes — the note on a customer's ticket fires that ticket's own update hook. A ticket last seen resolved at its source that fires again is never that echo: a note never changes a ticket's state, so someone reopened it. Jira comment events never count as a signal at all.
If you filter on the vendor side anyway, keep tickets with no labels: Jira JQL
labels is EMPTY OR labels not in (aiops) (labels != aiops silently drops every
unlabelled issue), ServiceNow business-rule condition
current.correlation_display != 'AIOps'.
Tickets that come in must name five fields
A ticket sent in must name the same five things the platform writes on every
ticket it files: service, severity, priority, assignee and notify. Each system
takes them here (a Field: value line in the description, such as
Notify: payments-oncall, works for every field in every system; a value that
starts with TBD, n/a, none or - names nothing):
| Field | Jira | ServiceNow | Azure DevOps |
|---|---|---|---|
| Service | the first Component, a service:<name> label |
Configuration item, else Business service | a service:<name> tag |
| Severity | a Severity: line or a severity:<value> label |
Severity | Severity |
| Priority | Priority | Priority, else Impact and Urgency | Priority |
| Assignee | Assignee | Assigned to, else Assignment group | Assigned To |
| Notify | a Notify: line or a notify:<team> label |
Watch list | a notify:<team> tag |
ServiceNow names its severity and priority by default. An instance fills Severity (3 - Low on a base instance) and computes Priority from Impact and Urgency on every incident, so a ServiceNow ticket always names both: there the hold fires only for the service, the assignee and the watch list — and with a group filter in the business rule, the assignee is never missing either.
A stated value counts only when it is one. A service line or tag names one service
(Service: checkout-api; Service: checkout-api is down names none). A severity
line or label names critical, error, warning or info, a Jira priority word
(Highest to Lowest) or a value the workspace's severity_map maps to one of
them, and it only ever raises the severity the ticket's own fields give: a line
never lowers a ticket's priority.
A ticket missing any of them is held: it opens its own incident in the triage queue (Needs you, chip Triage; the incident wears awaiting info), is never grouped, and starts no analysis, notification or automatic fix. Its row reads Jira OPS-21 — asked for: severity, notify, with a link to the ticket.
AIOps asks on the ticket once. About a minute after the ticket arrives (an
automation that fills a field just after creation is not asked about), one note
lists the fields still missing, where this system takes each and the values its
own fields accept — for the service, up to 10 service names from the dependency map
and the estate model, scrubbed like every write to a ticket. It goes through the workspace's Ticket system route for that system, as
the write-back does: a Jira internal comment, ServiceNow work notes, an Azure DevOps
history entry. The incident's Timeline says Asked on the ticket for missing
fields (audit ticket.info_requested). The note is posted once per ticket, ever:
not again on the ticket's later updates, nor when the ticket comes back after
closing. It is never posted on a ticket the platform filed itself (its echo is
dropped first), on a ServiceNow incident already closed, in computer-assist mode, or
without an enabled ticket route of that system that is a Ticket system preset —
the incident's L3 step then says why no note was sent. It is only posted on a
ticket created within the hour before it reached AIOps (a ticket whose creation
time is not sent counts as new), and on at most 20 tickets per workspace an hour:
an older ticket (a bulk edit, the webhook switched on for a busy project) or one
past the 20 waits in the queue unasked, and its row says why.
The update that names them all moves it on. The held incident resolves
quietly — nothing is written to the ticket and nothing is announced — and the
ticket opens, or joins, the incident of the service it names, as a ticket that
arrived complete would (Ticket named its missing fields — moved on, audit
ticket.info_received). An update that still lacks a field changes nothing and asks
nothing. So the update events must reach the platform: Jira's webhook on
jira:issue_updated, ServiceNow's business rule on Update as well as Insert,
and the Azure DevOps service hook on workitem.updated (an edit of the tags, the
assignee, the severity, the priority, the description or the area path moves a
held work item on; for any other work item it is ignored). Analyse investigates
a held ticket at once instead.
Narrow or extend the list. The platform default is all five
(AIOPS_TICKET_REQUIRED_FIELDS). A workspace sets its own with
PATCH /api/aiops/{tenant_id} and
{"settings": {"ticket_required_fields": ["service", "priority"]}}
(members.manage; [] requires nothing, null follows the platform default).
Whatever the list, a ticket that names no service still waits in the triage queue
while Send alerts that name no service to the triage queue is on, and the note
asks for the service.
Upgrading from a release without this check: a ServiceNow business rule written
before it sends no severity, assignee or watch list; a Jira issue names its severity
and who to notify only with a Severity: / Notify: line or a severity: /
notify: label (Jira has no field for either); an Azure DevOps work item names its
service and who to notify only with a service:<name> / notify:<team> tag or a
Service: / Notify: description line. Tickets without them wait in the queue.
Update the business rule (template) and the
Jira and Azure DevOps conventions before the upgrade, or narrow the workspace's
list first — a ticket naming no service still waits while Send alerts that name
no service to the triage queue is on.
ServiceNow inbound Business Rule
In ServiceNow, System Definition → Business Rules → New:
| Field | Value |
|---|---|
| Name | AIOps — forward incidents |
| Table | Incident [incident] |
| Advanced | ticked |
| When | async |
| Insert, Update | both ticked |
Without the console, create the same rule through the ServiceNow Table API, with an
account allowed to write business rules: POST /api/now/table/sys_script with this
body, the script below as its script value:
{"name": "AIOps — forward incidents", "collection": "incident", "when": "async",
"action_insert": true, "action_update": true, "advanced": true, "active": true,
"script": "<the script below>"}
Why async: an async rule runs after the incident is saved, on a background
worker, so the service desk's save never waits on the platform — a slow or
unreachable platform delays the signal, never the ticket. A before or after
rule would make the HTTP call inside the save. A rule created through the Table
API with "when": "async" reads back as "when": "async_always" on newer
releases; it is still the async rule above (a rule stored that way forwarded every
insert and update in the platform's own end-to-end test), so leave it.
Forward only one group's tickets. The platform has no inbound group filter:
filter in the business rule. On its When to run tab add the condition
Assignment group — is — your group; over the Table API add
"filter_condition": "assignment_group=<group sys_id>^EQ" to the body above. The
rule then posts only the inserts and updates of incidents in that group:
- A ticket moved to another group stops updating. The platform sees nothing it does there afterwards — not an edit, not a resolve. An incident it opened stays as it was; a ticket held for a missing field stays in Needs you → Triage, awaiting info, because the update that would name the field never arrives. Move the ticket back to the group (its next update is forwarded again), choose Analyse to investigate it as it is, or resolve the incident in the platform.
- The assignee is never missing. Every forwarded ticket has the filter's assignment group, which names its assignee, so a hold can only ask for the service, the severity, the priority or the watch list.
- Tickets the platform files go to the route's assignment group, or the group the incident's assignee names (see What gets written). One filed into another group is not forwarded — nothing is lost: the platform drops its own tickets' echoes anyway.
Paste this on the Advanced tab with your platform's host, your workspace slug
and the workspace's webhook ingest token (Alert intake & tokens, the Webhook
ingest card). The token travels in the X-Webhook-Token header — never put it in
the URL, where every proxy and access log keeps it. Anyone who can read business
rules can read it: treat the rule as holding a secret. A rule that forwards change
requests (change_request) for unauthorized-change detection sends the workspace's
ITSM webhook token instead: a change ticket sent with the shared token never covers
a captured write (The ITSM webhook token).
(function executeRule(current, previous) {
var url = 'https://<your-platform-host>/webhook/servicenow/<workspace-slug>';
var token = '<INGEST_TOKEN>';
var body = {
sys_id: current.getUniqueValue(),
number: current.getValue('number'),
short_description: current.getValue('short_description'),
description: current.getValue('description'),
priority: current.getValue('priority'),
impact: current.getValue('impact'),
urgency: current.getValue('urgency'),
severity: current.getValue('severity'),
state: current.getValue('state'),
cmdb_ci: current.cmdb_ci.getDisplayValue(),
business_service: current.business_service.getDisplayValue(),
// the required fields (Tickets that come in must name five fields)
assigned_to: current.assigned_to.getDisplayValue(),
assignment_group: current.assignment_group.getDisplayValue(),
watch_list: current.getValue('watch_list'),
opened_at: current.getValue('opened_at'),
// the platform's mark on the incidents it files: how it drops its own echo
correlation_id: current.getValue('correlation_id'),
correlation_display: current.getValue('correlation_display'),
url: gs.getProperty('glide.servlet.uri') + 'nav_to.do?uri=incident.do%3Fsys_id%3D' +
current.getUniqueValue()
};
var r = new sn_ws.RESTMessageV2();
r.setEndpoint(url);
r.setHttpMethod('POST');
r.setRequestHeader('Content-Type', 'application/json');
r.setRequestHeader('X-Webhook-Token', token);
r.setRequestBody(JSON.stringify(body));
var resp = r.execute();
gs.info('AIOps webhook ' + current.getValue('number') + ' -> ' + resp.getStatusCode() +
' ' + resp.getBody());
})(current, previous);
Every insert and update posts the incident: cmdb_ci (else business_service)
becomes its service label, state 6 or 7 resolves it (see Resolved at the
source above), and the platform's own incidents come back as echoes it drops. The
system log (AIOps webhook INC… -> 200 {…}) shows each answer: ticket_echo for
an echo, joined for a ticket that joined the live incident of its service,
deduped for an update of a ticket already in, alerts_created for a new one.
Joining the live incident. A ticket joins the incident the platform already has
open only when its cmdb_ci display value (else business_service) equals,
character for character, the service label of that incident's alerts, for
example orders-api. Any other value opens an incident of its own. A ticket with
neither cmdb_ci nor business_service names no service: it waits under
Needs you → Triage, unanalysed, and AIOps asks on it for the service (see
Tickets that come in must name five fields). A Jira issue's
first component plays the same part, and an Azure DevOps work item's
service:<name> tag.
What the platform reads from the body. The record may also arrive nested
under record or resource, and a reference field as
{"display_value": …, "value": …} (the display value for cmdb_ci and
business_service, the value for priority and state):
| Field | What the platform does with it |
|---|---|
sys_id or number |
One of them is required: a body with neither is ignored. number becomes the ticket reference; a 32-character hex sys_id is the id the write-back addresses |
short_description |
The alert summary |
description |
The alert message, first 2,000 characters |
priority |
Severity: 1 critical, 2 error, 3 warning, 4 or 5 info, anything else warning; with impact and urgency, also the ticket's priority |
severity, assigned_to, assignment_group, watch_list |
Whether the ticket names its severity, assignee and who to notify (required fields); only that it names them is kept |
state |
6, 7, resolved or closed resolves the alert; anything else fires it |
cmdb_ci, else business_service |
The service label |
opened_at |
When the alert started |
correlation_id |
The mark on a ticket the platform filed (aiops:<incident id>), which drops the echo |
url |
The ticket link, and the site the write-back goes through (below) |
ServiceNow inbound rule — upgrade note. The mark rides in correlation_id,
so the business rule must post it; a rule written to the older template without
it cannot tell the platform's ticket from a customer's while the platform's create
is still in flight, and opens a second incident for it. Add it to the posted
record (the template above also posts correlation_display, which the platform
does not read):
{"sys_id": "...", "number": "INC0012345", "short_description": "...",
"priority": "2", "state": "2", "cmdb_ci": "orders-api",
"correlation_id": "...", "correlation_display": "...",
"url": "https://<instance>.service-now.com/..."}
url is also what lets the platform write the analysis back: the ticket is
written only through a connection on that host (a vanity domain is another
site). A rule that posts no url still gets the write-back while the workspace
has exactly one enabled ServiceNow connection; with two, or a url on another
host, the incident says Writing back to ServiceNow … failed: the ticket has no
link to prove which site it lives on (or … now points at …, not … where this
ticket lives). Retry checks the ticket's link against the connection's host
again, so it succeeds once a connection points at that site; a ticket that
arrived without a link while two connections were enabled is never proven, so
fix the rule and let the next ticket in. Only a route of the ticket's own system
counts: a workspace whose only ticket route is Jira keeps no enabled ticket
route writes back to this ticket for a ServiceNow ticket.
Advanced routes
A hand-written on_open / on_resolve route sends one request each, with no click,
so since the runbook-governance release it follows the rules of every route that runs
on its own (Runbooks and automatic fixes):
apimode only, and only to a ticket, paging or webhook system — a Jira, ServiceNow, Azure DevOps, PagerDuty, Opsgenie or generic HTTP API connection. A cloud, cluster, server, network-device or repository connection is refused when it is saved, and a row saved before is refused before every request.- Never through a write connection (role
writeorread_write): that credential is kept for released fixes. Use the preset, or areadconnection. - It sends nothing until an
approvals.decideholder certifies its revision (POST /routes/{id}/certification); an operator's new or changed one is a certification request on Needs you. Routes from before keep running as "certifier unknown" and are listed for approvers to confirm or withdraw. - The words on the screen: saving reads Saved. A hand-written ticket route sends its request on its own, so an approver certifies this revision on Needs you; until then it sends nothing.; the Certify dialog says This hand-written ticket route sends this request on its own whenever a matching incident opens or resolves — the automatic-fix switches do not apply to it.; a route from before certification reads A hand-written ticket route from before certification — certifier unknown and the enabled ones sit on Needs you → Certify; a refused or uncertified route leaves ticket route … : Not certified: … on the incident's ticket line.
- When none may run, the incident's ticket line says why and the next covering ticket route is tried.
The presets above are fixed code and need none of this. Placeholders in an advanced route's path, query and body:
| Placeholder | Value |
|---|---|
${incident_id}, ${incident_title}, ${incident_status}, ${incident_root_cause}, ${incident_assignee}, ${correlation_key}, ${alert_count}, ${opened_at}, ${resolved_at} |
from the incident (free text scrubbed) |
${incident_priority}, ${incident_severity}, ${incident_service} |
P1–P4, the canonical severity, the service it is keyed on |
${incident_url} |
the portal link (empty while the platform's public URL is unset) |
${incident_ticket_ref}, ${incident_ticket_url} |
the human key and browse link |
${incident_ticket_api_id} |
the id the ticket's API addresses: ServiceNow sys_id, Jira key, work item id |
${incident_rca_wiki} / ${incident_rca} / ${incident_rca_html} |
on resolve: the analysis as Jira wiki markup / plain text (ServiceNow work notes) / HTML (Azure DevOps History) |
These placeholders fill only a route written by hand like this one. A step compiled from a reviewed plan or a step runbook runs exactly as reviewed: a $ in it, such as the one in a Windows service name like MSSQL$SQLEXPRESS, is never filled from the incident.
From the answer of an advanced filing route the platform records the key before
the numeric id (Jira OPS-24, not 10000), builds the Jira browse link from
self (never the REST link itself), and keeps a ServiceNow sys_id with a
nav_to.do link. A redirect or a web page is a failed filing; any other non-JSON
answer still counts as filed (a plain webhook).
A customer's own ticket. An advanced on_resolve route is one request the
platform cannot inspect, so it may close the ticket. On a ticket the incident came
in from (adopted through an inbound webhook) it runs only when a person
resolves the incident — Mark resolved, bulk close, accepting a merge
proposal, Write back — or when the post-fix verification confirms the fix.
Every other resolve (the source alert clearing, quiet-expiry, the agent's own
resolve, a merge or grouping the platform makes, the sweep) sends nothing and
records why: Not written back to …: the ticket was raised by its owner, so the
platform writes back to it only when a person resolves the incident or a fix is
validated — use Write back to send it now. The sweep leaves that incident alone.
If it reopens and a person resolves it, the write-back runs as usual. Tickets the
platform filed itself are not affected.
Limits
- No two-way status sync and no reopen sync: the platform files and closes; it does not follow the later life of a ticket it filed. (A customer's ticket sent in through an inbound webhook does carry its own resolve: see Inbound webhooks and echoes.)
- No manual "create ticket" button, no OAuth (basic auth and PATs only), no JSM request types. The ServiceNow configuration item is found by exact name only — no CMDB relationship or alias lookup.
- Tickets are filed from the control plane: a ticket system reachable only inside
your network (through the private-network agent) is not supported, and the tenant
SSRF guard applies — a private address needs the tenant's
ssrf_allow_hosts. - The agent's own Jira reads use
GET /rest/api/3/search/jql; the old/searchanswers410on Jira Cloud.
Next steps
- Integration pages: Jira · ServiceNow · Azure DevOps — the connection fields, Test and the read surface per system.
- Notifications, paging, on-call and tickets — the rest of the on-call wiring.
- Webhook reference — the inbound ticket payloads field by field.