feat(servicenow): semantic incident, change, catalog, approval, CMDB, and knowledge tools (#6747)

* feat(servicenow): add semantic incident, change, catalog, approval, CMDB, knowledge, and directory tools

The ServiceNow block only exposed generic Table API CRUD, so every real task
started with "which table is that on?". This adds 27 semantic tools that wrap
the same Table API plumbing under the names customers actually use.

- Incidents: create, get by number or sys_id, search, update, resolve, close,
  and append a work note or customer-visible comment.
- Change: create, get, list, update, move state, and list change tasks through
  the documented Change Management API.
- Service catalog: browse items, order one via the Service Catalog API
  order_now endpoint, and list or get requested items.
- Approvals: list pending approvals for an approver, approve, and reject.
- CMDB: search CIs on any class, read a CI with its inbound and outbound
  relations through the CMDB Instance API, and list cmdb_rel_ci rows.
- Knowledge: search and read articles through the Knowledge Management API.
- Directory: find a user by email or user name and list group members, which
  is what fills assigned_to and assignment_group.

Reference fields are the usual source of confusion, so every semantic read
defaults to sysparm_display_value=all — a reference comes back as both its
sys_id and its label — and every semantic write exposes
sysparm_input_display_value so a display name can be written instead of a
sys_id. Coded state values are exposed as labelled dropdowns built from one
constants module rather than raw integers.

The shared instance-URL, Basic Auth, sysparm, envelope, and error handling now
live in tools/servicenow/utils.ts, and the existing eight generic tools were
moved onto it rather than keeping their own copies.

* fix(servicenow): stop per-operation subblock defaults colliding on a shared id

Subblock initial values are seeded into block state keyed by subblock id, so
two subblocks sharing an id leave one stored value and the last definition
wins. Three ids were duplicated with differing defaults:

- `displayValue` was defined twice, unset for the generic Table API tools and
  `all` for the semantic ones. The semantic definition won, so a new block set
  to Read Records or Aggregate Records sent `sysparm_display_value=all` — a
  wire change to two already-shipped tools.
- `state` was defined four times. The Approval State definition won, so every
  new block carried `state=requested`, which Create Incident wrote to the
  incident and Move Change State used instead of its own `-5` default.

Give the colliding controls their own ids and map them back to the tool params
per operation, so the generic tools keep their original request shape and each
semantic operation keeps its own default.

Also correct descriptions that overstated what the API does: the LIKE operator
is not documented as case-sensitive, List Requested Items has no requester
filter, and the Change Management API task shape differs from the Table API.

Adds tool tests covering the refactor invariants for the eight pre-existing
Table API tools and the display-value separation.

* feat(servicenow): read a change request's real next states from the instance

The change tools describe state transitions using the base-system codes, which
only hold on an instance that has not customized its change model. ServiceNow
publishes an endpoint that answers the question directly for the record in
hand, so use it rather than keep assuming.

GET /api/sn_chg_rest/change/{sys_id}/nextstates returns the states reachable
from the change request, the instance's own state-value-to-label map, and, for
model-driven changes, each transition with the conditions it has and has not
met. The tool flattens the per-target-state grouping ServiceNow returns (each
transition already carries from_state and to_state, so nothing is lost) and
derives the states whose conditions currently pass.

Also record the sourcing for the coded values in constants.ts: the change
states and close codes are published as a table, but the incident state codes
are not — only 6 (Resolved) appears in the docs — so mark the rest as defaults
rather than guarantees. Note that sysparm_input_display_value also reinterprets
date and time values in the caller's timezone instead of GMT, which matters for
the change start and end dates.

* docs(servicenow): stop asserting undocumented coded values in placeholders

The additional-fields examples used hold_reason with a coded value of "1".
ServiceNow documents the On hold reason choices by label only — Awaiting
Caller, Awaiting Change, Awaiting Problem, Awaiting Vendor — and publishes
neither the column name nor the codes, so the example was asserting something
unsourced. Use a field whose value is caller-supplied instead, and record the
On Hold requirement on the incident state control using the labels the docs
actually give, including that Awaiting Caller makes Additional Comments
mandatory.

* fix(servicenow): drop phantom parent fields from the catalog order output

order_catalog_item read parent_id and parent_table off the order_now response.
Those fields belong to submit_producer, a different Service Catalog endpoint;
the documented order_now result is sys_id, number, request_number, request_id,
and table. Both outputs were therefore always null.

* fix(servicenow): correct what knowledge search returns as an article id

Search results carry a table-prefixed identifier — "kb_knowledge:9e528db1..."
— not a bare sys_id, while GET /knowledge/articles/{id} accepts only a bare
sys_id or a KB number. The output described it as a sys_id and the tool
description told callers it was what they needed to fetch the article, so
chaining the two tools on that field would fail. Point callers at the KB
number instead. Relevancy score is documented as a number, not a string.

* docs(servicenow): cite the page that actually documents approval statuses

The approval state constants pointed at the classic-approvals landing page,
which does not list the statuses. Approval status is documented separately and
names four — Requested, Approved, Rejected, and Not Requested.

* fix(servicenow): stop constant interpolation leaking into tool descriptions

The docs generator and the client-facing integration catalog read tool
descriptions from source rather than from the evaluated module, so a
template literal like `state ${INCIDENT_STATE.RESOLVED}` shipped to users
verbatim: `apps/sim/lib/integrations/integrations.json` and the published
ServiceNow integration page both rendered `${INCIDENT_STATE.RESOLVED}`
instead of `6`. Inline the base-system coded values in the description
text; the constants stay in use everywhere behavior depends on them.

Also drops an escaped `\'` in the `inputDisplayValue` description for the
same reason, and adds a standing guard test asserting no subBlock id
carries two different seeded defaults — the invariant behind the
per-operation defaulting bug, now checked structurally rather than only
through the four per-operation cases.

* refactor(servicenow): type the shared response boundary instead of any

`parseServiceNowResponse` returned `any`, so every tool reading `data.result`
did unchecked property access — a shape change on the instance side would have
produced a wrong-typed output silently rather than a type error.

Introduces `ServiceNowEnvelope` (`result?: unknown`) as the parser's return
type and narrows the record index signatures from `any` to `unknown`. Adds
`toRecordObject`, `readString`, and `readNestedNumber` so the tools that read
individual fields narrow deliberately at the point of use.

This surfaced five genuinely unchecked reads: Order Catalog Item, Get Knowledge
Article, and Search Knowledge were declaring `string | null` / `number | null`
outputs while emitting whatever the instance sent, and Get Change Next States
assigned an unvalidated object to `Record<string, string>`. Each now coerces or
drops a non-matching value rather than passing it through.

* fix(servicenow): publish the shared tool params and stop offering inert controls

The docs generator reads tool source rather than importing it, so the shared
`params.ts` consts the semantic tools spread were dropped from every published
Input table — 27 of 35 ServiceNow tools listed no instance URL, username, or
password at all. Follow a spread into the module it is imported from so those
rows are published; ten other integrations gain the rows they were missing for
the same reason.

Two controls were dead on arrival: Additional Fields was offered on Move Change
State and Add Incident Comment, and neither tool read it. Wire it through the
change transition, which needs it, and drop it from the comment tool, whose body
is exactly one journal field.

Every coded-value control was a select-only dropdown, so a customized instance's
state or close code was unreachable — sharpest on Move Change State, whose
target state is required and whose real codes come from Get Change Next States.
Make them comboboxes.

Also correct two doc claims ServiceNow does not publish (the incident state
citation pointed at a page that does not exist and compares the legacy
incident_state field; closing an incident is not documented as requiring
itil_admin), replace Record<string, any> with checked narrowing that surfaced
two unsound widenings, and document that List Change Tasks returns a fixed
{value, display_value} shape under `tasks` rather than `records`.

* fix(servicenow): stop one subblock id from carrying two value spaces

Subblock values are stored per block keyed by id, so an id reused across
operations keeps its value when the operation changes. Incident and change
shared `state`, and `closeCode`, `closeNotes`, `comments`, and the knowledge
search phrase were each reused for a different value space — so an incident
state could be written onto a change request, an incident close code sent as a
change close code, or an encoded query searched as knowledge text.

Give each value space its own subblock and republish it to the tool param from
the operation that owns it, the way targetState and approvalState already work.
The generic Table API ids stay exactly as they are, since renaming one would
orphan the stored value of every workflow already using those shipped tools.

The previous guard only compared seeded defaults, which is why this class stayed
hidden; the new one asserts against the merged params a tool actually receives.

* fix(servicenow): point the canvas sentences at the renamed subblocks

The split of the colliding subblock ids left the operation sentences anchored on
ids that no longer exist, so those clauses would silently drop from the card.

* fix(servicenow): validate collection members and split the fields projection

toRecordArray cast every member of a successful response, so a null or scalar in
a collection was handed to the next block as a record while the tool reported
success and its declared output said that could not happen. Members that are not
plain objects are now dropped, and knowledge articles and change transitions get
the same narrowing. The two response types that described an unverified inner
shape now say what is actually checked.

The 'fields' subblock also carried two value spaces: a JSON body on Create and
Update Record, a comma-separated projection everywhere else. Operations added
since read a separate returnFields control, so a body can no longer arrive as a
projection or the reverse. The shipped ids are untouched, since renaming one
orphans the stored value of every workflow already using those tools.
This commit is contained in:
Waleed
2026-08-15 18:33:30 -07:00
committed by GitHub
parent 57611bda18
commit 9e67655b23
59 changed files with 7989 additions and 134 deletions
@@ -40,6 +40,8 @@ Retrieves leads from Email Bison with optional search and tag filters.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Email Bison API token |
| `apiBaseUrl` | string | Yes | Email Bison instance URL that issued the token |
| `search` | string | No | Search term for filtering leads |
| `campaignStatus` | string | No | Lead campaign status filter: in_sequence, sequence_finished, sequence_stopped, never_contacted, or replied |
| `tagIds` | array | No | Tag IDs to include |
@@ -73,6 +75,8 @@ Retrieves a lead by Email Bison lead ID or email address.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Email Bison API token |
| `apiBaseUrl` | string | Yes | Email Bison instance URL that issued the token |
| `leadId` | string | Yes | Lead ID or email address |
#### Output
@@ -102,6 +106,8 @@ Creates a single lead in Email Bison.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Email Bison API token |
| `apiBaseUrl` | string | Yes | Email Bison instance URL that issued the token |
| `firstName` | string | Yes | Lead first name |
| `lastName` | string | Yes | Lead last name |
| `email` | string | Yes | Lead email address |
@@ -137,6 +143,8 @@ Updates an existing Email Bison lead. Fields omitted from a PUT update may be cl
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Email Bison API token |
| `apiBaseUrl` | string | Yes | Email Bison instance URL that issued the token |
| `leadId` | string | Yes | Lead ID or email address |
| `firstName` | string | Yes | Lead first name |
| `lastName` | string | Yes | Lead last name |
@@ -173,6 +181,8 @@ Retrieves Email Bison campaigns.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Email Bison API token |
| `apiBaseUrl` | string | Yes | Email Bison instance URL that issued the token |
#### Output
@@ -201,6 +211,8 @@ Creates a new Email Bison campaign.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Email Bison API token |
| `apiBaseUrl` | string | Yes | Email Bison instance URL that issued the token |
| `name` | string | Yes | Campaign name |
| `campaignType` | string | No | Campaign type: outbound or reply_followup |
@@ -231,6 +243,8 @@ Updates Email Bison campaign settings.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Email Bison API token |
| `apiBaseUrl` | string | Yes | Email Bison instance URL that issued the token |
| `campaignId` | number | Yes | Campaign ID |
| `name` | string | No | Campaign name |
| `maxEmailsPerDay` | number | No | Maximum emails per day |
@@ -269,6 +283,8 @@ Pauses, resumes, or archives an Email Bison campaign.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Email Bison API token |
| `apiBaseUrl` | string | Yes | Email Bison instance URL that issued the token |
| `campaignId` | number | Yes | Campaign ID |
| `action` | string | Yes | Status action: pause, resume, or archive |
@@ -299,6 +315,8 @@ Adds existing Email Bison leads to a campaign.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Email Bison API token |
| `apiBaseUrl` | string | Yes | Email Bison instance URL that issued the token |
| `campaignId` | number | Yes | Campaign ID |
| `leadIds` | array | Yes | Lead IDs to add to the campaign |
| `allowParallelSending` | boolean | No | Force add leads already in sequence in other campaigns |
@@ -330,6 +348,8 @@ Retrieves Email Bison replies with optional status, folder, campaign, sender, le
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Email Bison API token |
| `apiBaseUrl` | string | Yes | Email Bison instance URL that issued the token |
| `search` | string | No | Search term for replies |
| `status` | string | No | Reply status: interested, automated_reply, or not_automated_reply |
| `folder` | string | No | Reply folder: inbox, sent, spam, bounced, or all |
@@ -366,6 +386,8 @@ Retrieves all Email Bison tags for the authenticated workspace.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Email Bison API token |
| `apiBaseUrl` | string | Yes | Email Bison instance URL that issued the token |
#### Output
@@ -394,6 +416,8 @@ Creates a new Email Bison tag.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Email Bison API token |
| `apiBaseUrl` | string | Yes | Email Bison instance URL that issued the token |
| `name` | string | Yes | Tag name |
#### Output
@@ -423,6 +447,8 @@ Attaches Email Bison tags to one or more leads.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Email Bison API token |
| `apiBaseUrl` | string | Yes | Email Bison instance URL that issued the token |
| `tagIds` | array | Yes | Tag IDs to attach |
| `leadIds` | array | Yes | Lead IDs to tag |
| `skipWebhooks` | boolean | No | Skip Email Bison webhooks for this action |
@@ -73,6 +73,8 @@ Fetch and parse a file from a URL with optional custom headers.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `request` | string | No | No description |
| `outputs` | array | No | Array of parsed files with content and metadata |
| `headers` | object | No | HTTP headers to include when fetching URL-based files. |
#### Output
@@ -49,6 +49,7 @@ Start a background Flint agent task that modifies a site from a natural-language
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Flint API key \(found in Flint team settings, starts with ak_\) |
| `siteId` | string | Yes | ID of the Flint site the agent should modify |
| `prompt` | string | Yes | Natural-language instructions for the agent \(e.g., "Add a new About page with a team section"\) |
| `callbackUrl` | string | No | HTTPS webhook URL that Flint will POST to when the task completes or fails |
@@ -70,6 +71,7 @@ Start a background Flint agent task that generates up to 10 pages from a templat
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Flint API key \(found in Flint team settings, starts with ak_\) |
| `siteId` | string | Yes | ID of the Flint site the agent should modify |
| `templatePageSlug` | string | Yes | Slug of the existing template page to generate from \(e.g., /case-studies/template\) |
| `items` | json | Yes | JSON array of 1-10 pages to generate. Each item requires targetPageSlug \(slug for the new page\) and context \(content details the agent should use\). |
@@ -92,6 +94,7 @@ Get the status and results of a background Flint agent task.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Flint API key \(found in Flint team settings, starts with ak_\) |
| `taskId` | string | Yes | Identifier of the task returned when it was created \(e.g., bg-...\) |
#### Output
@@ -43,6 +43,10 @@ Fetch PR details including diff and files changed
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `owner` | string | Yes | Repository owner |
| `repo` | string | Yes | Repository name |
| `pullNumber` | number | Yes | Pull request number |
| `apiKey` | string | Yes | GitHub API token |
| `includeFiles` | boolean | No | Whether to fetch changed-file details from the separate files endpoint |
#### Output
@@ -40,6 +40,7 @@ Retrieves Instantly V2 leads with search, campaign, list, and pagination filters
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Instantly API key with the required V2 scopes |
| `search` | string | No | Search by first name, last name, or email |
| `filter` | string | No | Instantly lead filter value, such as FILTER_VAL_CONTACTED or FILTER_VAL_ACTIVE |
| `campaign` | string | No | Campaign ID to filter leads |
@@ -90,6 +91,7 @@ Retrieves an Instantly V2 lead by ID.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Instantly API key with the required V2 scopes |
| `leadId` | string | Yes | Lead ID |
#### Output
@@ -124,6 +126,7 @@ Creates an Instantly V2 lead in a campaign or lead list.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Instantly API key with the required V2 scopes |
| `campaign` | string | No | Campaign ID associated with the lead |
| `list_id` | string | No | Lead list ID associated with the lead |
| `email` | string | No | Lead email address. Required when adding to a campaign. |
@@ -177,6 +180,7 @@ Updates fields on an existing Instantly V2 lead.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Instantly API key with the required V2 scopes |
| `leadId` | string | Yes | Lead ID |
| `first_name` | string | No | Lead first name |
| `last_name` | string | No | Lead last name |
@@ -222,6 +226,7 @@ Deletes Instantly V2 leads in bulk from a campaign or lead list.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Instantly API key with the required V2 scopes |
| `campaign_id` | string | No | Campaign ID to delete leads from. Required if list_id is not provided. |
| `list_id` | string | No | Lead list ID to delete leads from. Required if campaign_id is not provided. |
| `status` | number | No | Optional lead status filter |
@@ -242,6 +247,7 @@ Submits an Instantly V2 background job to update a lead interest status.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Instantly API key with the required V2 scopes |
| `lead_email` | string | Yes | Lead email address |
| `interest_value` | number | No | Interest status value. Leave empty in the block or pass null to reset to Lead. |
| `campaign_id` | string | No | Campaign ID for the lead |
@@ -263,6 +269,7 @@ Retrieves Instantly V2 campaigns with search, status, tag, and pagination filter
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Instantly API key with the required V2 scopes |
| `limit` | number | No | Number of campaigns to return, from 1 to 100 |
| `starting_after` | string | No | Pagination cursor from next_starting_after |
| `search` | string | No | Search by campaign name |
@@ -302,6 +309,7 @@ Creates an Instantly V2 campaign using the documented campaign schedule schema.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Instantly API key with the required V2 scopes |
| `name` | string | Yes | Campaign name |
| `campaign_schedule` | json | Yes | Campaign schedule object with schedules array |
| `sequences` | array | No | Campaign sequence definitions |
@@ -347,6 +355,7 @@ Updates documented Instantly V2 campaign fields.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Instantly API key with the required V2 scopes |
| `campaignId` | string | Yes | Campaign ID |
| `name` | string | No | Campaign name |
| `campaign_schedule` | json | No | Campaign schedule object with schedules array |
@@ -393,6 +402,7 @@ Activates, starts, or resumes an Instantly V2 campaign.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Instantly API key with the required V2 scopes |
| `campaignId` | string | Yes | Campaign ID |
#### Output
@@ -427,6 +437,7 @@ Pauses a running Instantly V2 campaign, stopping further email sends.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Instantly API key with the required V2 scopes |
| `campaignId` | string | Yes | Campaign ID |
#### Output
@@ -461,6 +472,7 @@ Permanently deletes an Instantly V2 campaign.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Instantly API key with the required V2 scopes |
| `campaignId` | string | Yes | Campaign ID |
#### Output
@@ -495,6 +507,7 @@ Retrieves Instantly V2 Unibox emails with search and pagination filters.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Instantly API key with the required V2 scopes |
| `limit` | number | No | Number of emails to return, from 1 to 100 |
| `starting_after` | string | No | Pagination cursor from next_starting_after |
| `search` | string | No | Search query, email address, or thread:&lt;thread-id&gt; |
@@ -537,6 +550,7 @@ Sends an Instantly V2 reply to an existing Unibox email.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Instantly API key with the required V2 scopes |
| `eaccount` | string | Yes | Connected email account used to send the reply |
| `reply_to_uuid` | string | Yes | Email ID to reply to |
| `subject` | string | Yes | Reply subject |
@@ -576,6 +590,7 @@ Retrieves Instantly V2 lead lists with search and pagination filters.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Instantly API key with the required V2 scopes |
| `limit` | number | No | Number of lead lists to return, from 1 to 100 |
| `starting_after` | string | No | Starting-after timestamp cursor |
| `has_enrichment_task` | boolean | No | Filter by enrichment task setting |
@@ -613,6 +628,7 @@ Creates an Instantly V2 lead list.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Instantly API key with the required V2 scopes |
| `name` | string | Yes | Lead list name |
| `has_enrichment_task` | boolean | No | Whether this list runs enrichment for every added lead |
| `owned_by` | string | No | User ID of the lead list owner |
@@ -55,6 +55,8 @@ List one page of a NetSuite record collection, optionally filtered with a q expr
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account |
| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential |
| `recordType` | string | Yes | NetSuite REST record type script ID, such as customer or salesOrder |
| `q` | string | No | NetSuite record collection filter expression |
| `limit` | number | No | Results to return in this page \(1-1000; default 100\) |
@@ -87,6 +89,8 @@ Retrieve one NetSuite record by internal or external ID.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account |
| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential |
| `recordType` | string | Yes | NetSuite REST record type script ID, such as customer or salesOrder |
| `recordId` | string | Yes | NetSuite internal ID or an external-ID reference beginning with eid: |
| `fields` | string | No | Comma-separated record fields to return |
@@ -108,6 +112,8 @@ Create a NetSuite record using the account-specific record metadata schema.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account |
| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential |
| `recordType` | string | Yes | NetSuite REST record type script ID, such as customer or salesOrder |
| `body` | json | Yes | Record fields matching the account-specific NetSuite metadata schema |
| `replace` | string | No | Comma-separated sublists whose default lines should be replaced |
@@ -128,6 +134,8 @@ Update fields on an existing NetSuite record with PATCH.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account |
| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential |
| `recordType` | string | Yes | NetSuite REST record type script ID, such as customer or salesOrder |
| `recordId` | string | Yes | NetSuite internal ID or an external-ID reference beginning with eid: |
| `body` | json | Yes | Record fields matching the account-specific NetSuite metadata schema |
@@ -149,6 +157,8 @@ Create or update a NetSuite record by external ID with PUT.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account |
| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential |
| `recordType` | string | Yes | NetSuite REST record type script ID, such as customer or salesOrder |
| `externalId` | string | Yes | External ID without the eid: prefix |
| `body` | json | Yes | Record fields matching the account-specific NetSuite metadata schema |
@@ -169,6 +179,8 @@ Delete one NetSuite record by internal or external ID.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account |
| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential |
| `recordType` | string | Yes | NetSuite REST record type script ID, such as customer or salesOrder |
| `recordId` | string | Yes | NetSuite internal ID or an external-ID reference beginning with eid: |
@@ -187,6 +199,8 @@ Retrieve a record sublist, subrecord, referenced record, or nested subresource.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account |
| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential |
| `recordType` | string | Yes | NetSuite REST record type script ID, such as customer or salesOrder |
| `recordId` | string | Yes | NetSuite internal ID or an external-ID reference beginning with eid: |
| `subresourcePath` | string | Yes | Slash-separated subresource path, such as item or item/1/inventoryDetail |
@@ -206,6 +220,8 @@ Return a prepopulated create form, or an edit form when a record ID is supplied.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account |
| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential |
| `recordType` | string | Yes | NetSuite REST record type script ID, such as customer or salesOrder |
| `recordId` | string | No | Existing record ID; omit to request a create form |
| `body` | json | No | Record fields matching the account-specific NetSuite metadata schema |
@@ -228,6 +244,8 @@ Retrieve valid select values for one or more fields on a new or existing record.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account |
| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential |
| `recordType` | string | Yes | NetSuite REST record type script ID, such as customer or salesOrder |
| `recordId` | string | No | Existing record ID; omit to evaluate options for a new record |
| `fields` | string | Yes | Comma-separated select field IDs |
@@ -254,6 +272,8 @@ Attach a contact or file to another NetSuite record.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account |
| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential |
| `recordType` | string | Yes | NetSuite REST record type script ID, such as customer or salesOrder |
| `recordId` | string | Yes | NetSuite internal ID or an external-ID reference beginning with eid: |
| `relatedType` | string | Yes | Related resource type: contact or file |
@@ -276,6 +296,8 @@ Detach a contact or file from another NetSuite record.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account |
| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential |
| `recordType` | string | Yes | NetSuite REST record type script ID, such as customer or salesOrder |
| `recordId` | string | Yes | NetSuite internal ID or an external-ID reference beginning with eid: |
| `relatedType` | string | Yes | Related resource type: contact or file |
@@ -296,6 +318,8 @@ Execute a supported NetSuite record action such as approve, reject, or confirm.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account |
| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential |
| `recordType` | string | Yes | NetSuite REST record type script ID, such as customer or salesOrder |
| `recordId` | string | Yes | NetSuite internal ID or an external-ID reference beginning with eid: |
| `action` | string | Yes | NetSuite record action ID without the @ prefix |
@@ -317,6 +341,8 @@ Transform a supported source record into another NetSuite record type.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account |
| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential |
| `recordType` | string | Yes | NetSuite REST record type script ID, such as customer or salesOrder |
| `recordId` | string | Yes | NetSuite internal ID or an external-ID reference beginning with eid: |
| `targetRecordType` | string | Yes | Target record type supported by the source record metadata |
@@ -338,6 +364,8 @@ Submit an asynchronous request to retrieve up to 100 records of one type.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account |
| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential |
| `recordType` | string | Yes | NetSuite REST record type script ID, such as customer or salesOrder |
| `ids` | string | Yes | Up to 100 comma-separated internal IDs or eid: external-ID references |
| `fields` | string | No | Comma-separated record fields to return |
@@ -362,6 +390,8 @@ Submit an asynchronous batch that creates up to 100 records of one type.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account |
| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential |
| `recordType` | string | Yes | NetSuite REST record type script ID, such as customer or salesOrder |
| `items` | array | Yes | Array of 1-100 records matching the account-specific metadata schema |
| `idempotencyKey` | string | No | Optional unique idempotency key for retrying the batch |
@@ -383,6 +413,8 @@ Submit an asynchronous batch that updates up to 100 records of one type.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account |
| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential |
| `recordType` | string | Yes | NetSuite REST record type script ID, such as customer or salesOrder |
| `items` | array | Yes | Array of 1-100 records; every item must include an internal or external ID |
| `idempotencyKey` | string | No | Optional unique idempotency key for retrying the batch |
@@ -404,6 +436,8 @@ Submit an asynchronous batch that creates or updates up to 100 records by extern
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account |
| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential |
| `recordType` | string | Yes | NetSuite REST record type script ID, such as customer or salesOrder |
| `items` | array | Yes | Array of 1-100 records; every item must include externalId |
| `idempotencyKey` | string | No | Optional unique idempotency key for retrying the batch |
@@ -425,6 +459,8 @@ Submit an asynchronous request to delete up to 100 records of one type.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account |
| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential |
| `recordType` | string | Yes | NetSuite REST record type script ID, such as customer or salesOrder |
| `ids` | string | Yes | Up to 100 comma-separated internal IDs or eid: external-ID references |
| `idempotencyKey` | string | No | Optional unique idempotency key for retrying the batch |
@@ -446,6 +482,8 @@ Execute one page of a SuiteQL query through SuiteTalk REST web services.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account |
| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential |
| `query` | string | Yes | SuiteQL SELECT query; use a complete unique ORDER BY when retrieving multiple pages |
| `limit` | number | No | Results to return in this page \(1-1000; default 100\) |
| `offset` | number | No | Zero-based result offset; must be divisible by limit and stay within the first 100,000 results and 1,000 pages |
@@ -473,6 +511,8 @@ List one page of SuiteAnalytics Workbook datasets available to the authenticated
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account |
| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential |
| `limit` | number | No | Results to return in this page \(1-1000; default 100\) |
| `offset` | number | No | Zero-based result offset; must be divisible by limit and stay within the first 100,000 results and 1,000 pages |
@@ -499,6 +539,8 @@ Execute one page of a standard or custom SuiteAnalytics Workbook dataset.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account |
| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential |
| `datasetId` | string | Yes | SuiteAnalytics dataset script ID |
| `limit` | number | No | Results to return in this page \(1-1000; default 100\) |
| `offset` | number | No | Zero-based result offset; must be divisible by limit and stay within the first 100,000 results and 1,000 pages |
@@ -526,6 +568,8 @@ List record types exposed to the authenticated role by the REST metadata catalog
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account |
| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential |
#### Output
@@ -551,6 +595,8 @@ Retrieve account-specific metadata for one NetSuite record type.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account |
| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential |
| `recordType` | string | Yes | NetSuite REST record type script ID, such as customer or salesOrder |
| `format` | string | No | Metadata representation: default, openapi, or json_schema |
@@ -569,6 +615,8 @@ Retrieve job status, list job tasks, or retrieve one task status.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account |
| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential |
| `jobId` | string | Yes | Asynchronous job ID |
| `view` | string | No | Retrieve job status, list tasks for the job, or retrieve one task status |
| `taskId` | string | No | Task ID; required when view is task |
@@ -605,6 +653,8 @@ Retrieve the provider response for one task within a completed asynchronous job.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account |
| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential |
| `jobId` | string | Yes | Asynchronous job ID |
| `taskId` | string | Yes | Task ID within the asynchronous job |
@@ -623,6 +673,8 @@ Retrieve the current UTC time from the NetSuite server.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account |
| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential |
#### Output
@@ -640,6 +692,8 @@ Retrieve REST web-services concurrency limits for the NetSuite account and integ
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | NetSuite OAuth 2.0 client-credentials service account |
| `instanceUrl` | string | No | SuiteTalk account origin injected by the executor from the selected credential |
#### Output
@@ -651,3 +705,5 @@ Retrieve REST web-services concurrency limits for the NetSuite account and integ
| ↳ `accountUnallocatedConcurrencyLimit` | number | Account concurrency not allocated to integrations |
| ↳ `integrationConcurrencyLimit` | number | Concurrency allocated to this integration |
| ↳ `integrationLimitType` | string | Limit assignment: integrationSpecific, accountLimit, or internal |
@@ -58,6 +58,10 @@ Publish a message to a RabbitMQ exchange with a routing key. Reports whether the
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `host` | string | Yes | RabbitMQ Management API base URL, e.g. https://rabbit.example.com:15672. Must use https unless the broker is on a loopback host. |
| `username` | string | Yes | RabbitMQ username |
| `password` | string | Yes | RabbitMQ password |
| `vhost` | string | No | Virtual host to operate on. Defaults to / |
| `exchange` | string | No | Exchange to publish to. Leave empty to publish to the default exchange, which routes by queue name. Empty is a valid value, so this is not required. |
| `routingKey` | string | Yes | Routing key. When publishing to the default exchange this is the target queue name. |
| `payload` | string | Yes | Message body to publish |
@@ -81,6 +85,10 @@ Retrieve messages from a RabbitMQ queue. Defaults to requeueing the messages so
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `host` | string | Yes | RabbitMQ Management API base URL, e.g. https://rabbit.example.com:15672. Must use https unless the broker is on a loopback host. |
| `username` | string | Yes | RabbitMQ username |
| `password` | string | Yes | RabbitMQ password |
| `vhost` | string | No | Virtual host to operate on. Defaults to / |
| `queue` | string | Yes | Queue to read messages from |
| `count` | number | No | Maximum number of messages to retrieve, from 1 to $\{MAX_MESSAGE_COUNT\}. Defaults to 1 |
| `ackmode` | string | No | How retrieved messages are handled: ack_requeue_true \(default, leaves messages in the queue\), ack_requeue_false \(removes them\), reject_requeue_true, or reject_requeue_false |
@@ -103,6 +111,10 @@ List queues in a RabbitMQ virtual host with their depth, consumer count, and con
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `host` | string | Yes | RabbitMQ Management API base URL, e.g. https://rabbit.example.com:15672. Must use https unless the broker is on a loopback host. |
| `username` | string | Yes | RabbitMQ username |
| `password` | string | Yes | RabbitMQ password |
| `vhost` | string | No | Virtual host to operate on. Defaults to / |
| `page` | number | No | Page of results to return, starting at 1 |
| `pageSize` | number | No | Queues per page, from 1 to $\{RABBITMQ_MAX_PAGE_SIZE\}. Defaults to $\{DEFAULT_PAGE_SIZE\} |
| `name` | string | No | Filter queues whose name contains this value |
@@ -126,6 +138,10 @@ Read a single RabbitMQ queue, including its depth, consumer count, and declarati
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `host` | string | Yes | RabbitMQ Management API base URL, e.g. https://rabbit.example.com:15672. Must use https unless the broker is on a loopback host. |
| `username` | string | Yes | RabbitMQ username |
| `password` | string | Yes | RabbitMQ password |
| `vhost` | string | No | Virtual host to operate on. Defaults to / |
| `queue` | string | Yes | Queue name to read |
#### Output
@@ -142,6 +158,10 @@ Declare a RabbitMQ queue. Declaring a queue that already exists with the same se
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `host` | string | Yes | RabbitMQ Management API base URL, e.g. https://rabbit.example.com:15672. Must use https unless the broker is on a loopback host. |
| `username` | string | Yes | RabbitMQ username |
| `password` | string | Yes | RabbitMQ password |
| `vhost` | string | No | Virtual host to operate on. Defaults to / |
| `queue` | string | Yes | Name of the queue to declare |
| `durable` | boolean | No | Whether the queue survives a broker restart. Defaults to true |
| `autoDelete` | boolean | No | Delete the queue when its last consumer disconnects. Defaults to false |
@@ -163,6 +183,10 @@ Delete a RabbitMQ queue and every message still in it. Can be guarded so the del
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `host` | string | Yes | RabbitMQ Management API base URL, e.g. https://rabbit.example.com:15672. Must use https unless the broker is on a loopback host. |
| `username` | string | Yes | RabbitMQ username |
| `password` | string | Yes | RabbitMQ password |
| `vhost` | string | No | Virtual host to operate on. Defaults to / |
| `queue` | string | Yes | Name of the queue to delete |
| `ifUnused` | boolean | No | Only delete the queue when it has no consumers |
| `ifEmpty` | boolean | No | Only delete the queue when it holds no messages |
@@ -183,6 +207,10 @@ Discard every ready message in a RabbitMQ queue while leaving the queue itself i
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `host` | string | Yes | RabbitMQ Management API base URL, e.g. https://rabbit.example.com:15672. Must use https unless the broker is on a loopback host. |
| `username` | string | Yes | RabbitMQ username |
| `password` | string | Yes | RabbitMQ password |
| `vhost` | string | No | Virtual host to operate on. Defaults to / |
| `queue` | string | Yes | Name of the queue to purge |
#### Output
@@ -201,6 +229,10 @@ List exchanges in a RabbitMQ virtual host with their type and declaration settin
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `host` | string | Yes | RabbitMQ Management API base URL, e.g. https://rabbit.example.com:15672. Must use https unless the broker is on a loopback host. |
| `username` | string | Yes | RabbitMQ username |
| `password` | string | Yes | RabbitMQ password |
| `vhost` | string | No | Virtual host to operate on. Defaults to / |
| `page` | number | No | Page of results to return, starting at 1 |
| `pageSize` | number | No | Exchanges per page, from 1 to $\{RABBITMQ_MAX_PAGE_SIZE\}. Defaults to $\{DEFAULT_PAGE_SIZE\} |
| `name` | string | No | Filter exchanges whose name contains this value |
@@ -224,6 +256,10 @@ Read a single RabbitMQ exchange and the settings it was declared with.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `host` | string | Yes | RabbitMQ Management API base URL, e.g. https://rabbit.example.com:15672. Must use https unless the broker is on a loopback host. |
| `username` | string | Yes | RabbitMQ username |
| `password` | string | Yes | RabbitMQ password |
| `vhost` | string | No | Virtual host to operate on. Defaults to / |
| `exchange` | string | No | Exchange name to read. Leave empty for the default exchange, which is a valid value, so this is not required |
#### Output
@@ -240,6 +276,10 @@ Declare a RabbitMQ exchange. Declaring an exchange that already exists with the
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `host` | string | Yes | RabbitMQ Management API base URL, e.g. https://rabbit.example.com:15672. Must use https unless the broker is on a loopback host. |
| `username` | string | Yes | RabbitMQ username |
| `password` | string | Yes | RabbitMQ password |
| `vhost` | string | No | Virtual host to operate on. Defaults to / |
| `exchange` | string | Yes | Name of the exchange to declare |
| `exchangeType` | string | No | Routing behaviour: direct \(exact routing key, default\), topic \(wildcard patterns\), fanout \(every bound queue\), or headers \(match on binding arguments\) |
| `durable` | boolean | No | Whether the exchange survives a broker restart. Defaults to true |
@@ -263,6 +303,10 @@ Delete a RabbitMQ exchange and every binding attached to it. Publishers targetin
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `host` | string | Yes | RabbitMQ Management API base URL, e.g. https://rabbit.example.com:15672. Must use https unless the broker is on a loopback host. |
| `username` | string | Yes | RabbitMQ username |
| `password` | string | Yes | RabbitMQ password |
| `vhost` | string | No | Virtual host to operate on. Defaults to / |
| `exchange` | string | Yes | Name of the exchange to delete |
| `ifUnused` | boolean | No | Only delete the exchange when nothing is bound to it |
@@ -282,6 +326,10 @@ List the bindings that route messages into a RabbitMQ queue, including the impli
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `host` | string | Yes | RabbitMQ Management API base URL, e.g. https://rabbit.example.com:15672. Must use https unless the broker is on a loopback host. |
| `username` | string | Yes | RabbitMQ username |
| `password` | string | Yes | RabbitMQ password |
| `vhost` | string | No | Virtual host to operate on. Defaults to / |
| `queue` | string | Yes | Queue whose bindings should be listed |
#### Output
@@ -300,6 +348,10 @@ List everything an exchange routes to, so you can see which routing keys reach w
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `host` | string | Yes | RabbitMQ Management API base URL, e.g. https://rabbit.example.com:15672. Must use https unless the broker is on a loopback host. |
| `username` | string | Yes | RabbitMQ username |
| `password` | string | Yes | RabbitMQ password |
| `vhost` | string | No | Virtual host to operate on. Defaults to / |
| `exchange` | string | No | Exchange whose outgoing bindings should be listed. Leave empty for the default exchange, which is a valid value, so this is not required |
#### Output
@@ -318,6 +370,10 @@ Bind a queue or another exchange to a RabbitMQ exchange so messages matching a r
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `host` | string | Yes | RabbitMQ Management API base URL, e.g. https://rabbit.example.com:15672. Must use https unless the broker is on a loopback host. |
| `username` | string | Yes | RabbitMQ username |
| `password` | string | Yes | RabbitMQ password |
| `vhost` | string | No | Virtual host to operate on. Defaults to / |
| `exchange` | string | Yes | Source exchange to bind from |
| `queue` | string | Yes | Destination queue, or destination exchange when binding exchange to exchange |
| `destinationType` | string | No | Whether the destination is a queue \(default\) or an exchange. Exchange-to-exchange bindings chain routing between exchanges |
@@ -342,6 +398,10 @@ Remove a binding so an exchange stops routing its matching messages to that dest
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `host` | string | Yes | RabbitMQ Management API base URL, e.g. https://rabbit.example.com:15672. Must use https unless the broker is on a loopback host. |
| `username` | string | Yes | RabbitMQ username |
| `password` | string | Yes | RabbitMQ password |
| `vhost` | string | No | Virtual host to operate on. Defaults to / |
| `exchange` | string | Yes | Source exchange the binding reads from |
| `destination` | string | Yes | Destination queue or exchange the binding routes to |
| `destinationType` | string | No | Whether the destination is a queue \(default\) or an exchange |
@@ -364,6 +424,10 @@ Read broker-wide RabbitMQ status: version, cluster name, object totals, queue de
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `host` | string | Yes | RabbitMQ Management API base URL, e.g. https://rabbit.example.com:15672. Must use https unless the broker is on a loopback host. |
| `username` | string | Yes | RabbitMQ username |
| `password` | string | Yes | RabbitMQ password |
| `vhost` | string | No | Virtual host to operate on. Defaults to / |
#### Output
@@ -395,6 +459,10 @@ Run one of the broker health checks and report whether it passed. A failing chec
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `host` | string | Yes | RabbitMQ Management API base URL, e.g. https://rabbit.example.com:15672. Must use https unless the broker is on a loopback host. |
| `username` | string | Yes | RabbitMQ username |
| `password` | string | Yes | RabbitMQ password |
| `vhost` | string | No | Virtual host to operate on. Defaults to / |
| `check` | string | No | Which check to run: alarms \(cluster-wide resource alarms, default\), local-alarms, virtual-hosts, node-is-quorum-critical, port-listener, protocol-listener, or certificate-expiration |
| `port` | number | No | Port to verify a listener on. Required for the port-listener check |
| `protocol` | string | No | Protocol to verify a listener for, e.g. amqp, amqp/ssl, mqtt, stomp, or http. Required for the protocol-listener check |
@@ -419,6 +487,10 @@ List the cluster nodes with memory, disk, file-descriptor, and alarm state. A fi
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `host` | string | Yes | RabbitMQ Management API base URL, e.g. https://rabbit.example.com:15672. Must use https unless the broker is on a loopback host. |
| `username` | string | Yes | RabbitMQ username |
| `password` | string | Yes | RabbitMQ password |
| `vhost` | string | No | Virtual host to operate on. Defaults to / |
#### Output
@@ -435,6 +507,10 @@ List the virtual hosts on the broker with their message totals, so you can disco
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `host` | string | Yes | RabbitMQ Management API base URL, e.g. https://rabbit.example.com:15672. Must use https unless the broker is on a loopback host. |
| `username` | string | Yes | RabbitMQ username |
| `password` | string | Yes | RabbitMQ password |
| `vhost` | string | No | Virtual host to operate on. Defaults to / |
#### Output
@@ -451,6 +527,10 @@ List client connections to the broker with their user, state, and channel count.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `host` | string | Yes | RabbitMQ Management API base URL, e.g. https://rabbit.example.com:15672. Must use https unless the broker is on a loopback host. |
| `username` | string | Yes | RabbitMQ username |
| `password` | string | Yes | RabbitMQ password |
| `vhost` | string | No | Virtual host to operate on. Defaults to / |
| `page` | number | No | Page of results to return, starting at 1 |
| `pageSize` | number | No | Connections per page, from 1 to $\{RABBITMQ_MAX_PAGE_SIZE\}. Defaults to $\{DEFAULT_PAGE_SIZE\} |
| `name` | string | No | Filter connections whose name contains this value |
@@ -474,6 +554,10 @@ List open channels with their prefetch limit and unacknowledged message count, w
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `host` | string | Yes | RabbitMQ Management API base URL, e.g. https://rabbit.example.com:15672. Must use https unless the broker is on a loopback host. |
| `username` | string | Yes | RabbitMQ username |
| `password` | string | Yes | RabbitMQ password |
| `vhost` | string | No | Virtual host to operate on. Defaults to / |
| `page` | number | No | Page of results to return, starting at 1 |
| `pageSize` | number | No | Channels per page, from 1 to $\{RABBITMQ_MAX_PAGE_SIZE\}. Defaults to $\{DEFAULT_PAGE_SIZE\} |
| `name` | string | No | Filter channels whose name contains this value |
@@ -497,6 +581,10 @@ List the consumers subscribed in a virtual host. An empty result for a queue wit
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `host` | string | Yes | RabbitMQ Management API base URL, e.g. https://rabbit.example.com:15672. Must use https unless the broker is on a loopback host. |
| `username` | string | Yes | RabbitMQ username |
| `password` | string | Yes | RabbitMQ password |
| `vhost` | string | No | Virtual host to operate on. Defaults to / |
#### Output
@@ -513,6 +601,10 @@ List the policies in a virtual host. Policies are how dead-lettering, TTLs, and
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `host` | string | Yes | RabbitMQ Management API base URL, e.g. https://rabbit.example.com:15672. Must use https unless the broker is on a loopback host. |
| `username` | string | Yes | RabbitMQ username |
| `password` | string | Yes | RabbitMQ password |
| `vhost` | string | No | Virtual host to operate on. Defaults to / |
#### Output
@@ -529,6 +621,10 @@ Create or replace a RabbitMQ policy, applying settings such as dead-lettering, T
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `host` | string | Yes | RabbitMQ Management API base URL, e.g. https://rabbit.example.com:15672. Must use https unless the broker is on a loopback host. |
| `username` | string | Yes | RabbitMQ username |
| `password` | string | Yes | RabbitMQ password |
| `vhost` | string | No | Virtual host to operate on. Defaults to / |
| `policyName` | string | Yes | Name of the policy. Reusing an existing name replaces that policy |
| `pattern` | string | Yes | Regular expression matched against queue or exchange names, e.g. ^orders\\. to match every name starting with orders. |
| `definition` | string | Yes | Settings to apply, as a JSON object, e.g. \{"dead-letter-exchange":"dlx","message-ttl":86400000,"max-length":10000\} |
@@ -551,6 +647,10 @@ Delete a RabbitMQ policy. Every queue and exchange it matched immediately loses
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `host` | string | Yes | RabbitMQ Management API base URL, e.g. https://rabbit.example.com:15672. Must use https unless the broker is on a loopback host. |
| `username` | string | Yes | RabbitMQ username |
| `password` | string | Yes | RabbitMQ password |
| `vhost` | string | No | Virtual host to operate on. Defaults to / |
| `policyName` | string | Yes | Name of the policy to delete |
#### Output
@@ -50,6 +50,8 @@ Send an iMessage or SMS to a single recipient via Sendblue.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKeyId` | string | Yes | Sendblue API Key ID \(sb-api-key-id\) |
| `apiSecretKey` | string | Yes | Sendblue API Secret Key \(sb-api-secret-key\) |
| `number` | string | Yes | Recipient phone number in E.164 format \(e.g., +19998887777\) |
| `from_number` | string | Yes | One of your registered Sendblue phone numbers to send from, in E.164 format \(e.g., +18887776666\) |
| `content` | string | No | Message text content. Either content or media_url must be provided. |
@@ -86,6 +88,8 @@ Send an iMessage or SMS to a group of recipients via Sendblue.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKeyId` | string | Yes | Sendblue API Key ID \(sb-api-key-id\) |
| `apiSecretKey` | string | Yes | Sendblue API Secret Key \(sb-api-secret-key\) |
| `numbers` | array | No | Recipient phone numbers in E.164 format \(e.g., \["+19998887777", "+13334445555"\]\). Optional when sending to an existing group via group_id. |
| `from_number` | string | Yes | One of your registered Sendblue phone numbers to send from, in E.164 format \(e.g., +18887776666\) |
| `content` | string | No | Message text content. Either content or media_url must be provided. |
@@ -125,6 +129,8 @@ Check whether a phone number can receive iMessage or only SMS.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKeyId` | string | Yes | Sendblue API Key ID \(sb-api-key-id\) |
| `apiSecretKey` | string | Yes | Sendblue API Secret Key \(sb-api-secret-key\) |
| `number` | string | Yes | Phone number to evaluate, in E.164 format \(e.g., +19998887777\) |
#### Output
@@ -142,6 +148,8 @@ Display a typing indicator to a recipient (not supported in group chats).
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKeyId` | string | Yes | Sendblue API Key ID \(sb-api-key-id\) |
| `apiSecretKey` | string | Yes | Sendblue API Secret Key \(sb-api-secret-key\) |
| `number` | string | Yes | Recipient's phone number in E.164 format \(e.g., +19998887777\) |
| `from_number` | string | No | Your Sendblue line number to send from, in E.164 format. |
| `state` | string | No | "start" \(default\) shows the indicator; "stop" ends an active indicator before max_duration_ms expires. |
@@ -164,6 +172,8 @@ Retrieve a single message and its current status by message handle/ID.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKeyId` | string | Yes | Sendblue API Key ID \(sb-api-key-id\) |
| `apiSecretKey` | string | Yes | Sendblue API Secret Key \(sb-api-secret-key\) |
| `message_id` | string | Yes | The message handle/ID returned when the message was sent. |
#### Output
File diff suppressed because it is too large Load Diff
@@ -26,6 +26,7 @@ Retrieves all Smartlead campaigns for the authenticated account.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Smartlead API key |
| `clientId` | number | No | Only return campaigns for this client \(agency accounts\) |
| `includeTags` | boolean | No | Include campaign tags in the response |
@@ -136,6 +137,8 @@ Retrieves a single Smartlead campaign by ID.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Smartlead API key |
| `campaignId` | number | Yes | Smartlead campaign ID |
#### Output
@@ -244,6 +247,7 @@ Creates a Smartlead campaign. The campaign starts in DRAFTED status; add sequenc
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Smartlead API key |
| `name` | string | Yes | Campaign name |
| `clientId` | number | No | Client to own the campaign \(agency accounts\) |
@@ -354,6 +358,8 @@ Starts, pauses, or stops a Smartlead campaign. START requires the campaign to al
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Smartlead API key |
| `campaignId` | number | Yes | Smartlead campaign ID |
| `status` | string | Yes | Target status: START, PAUSED, STOPPED |
#### Output
@@ -463,6 +469,8 @@ Sets the sending window, timezone, and throughput limits for a Smartlead campaig
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Smartlead API key |
| `campaignId` | number | Yes | Smartlead campaign ID |
| `timezone` | string | Yes | IANA timezone for the sending window, e.g. America/Los_Angeles |
| `daysOfTheWeek` | array | Yes | Sending days as ISO weekday numbers, where 1 is Monday and 7 is Sunday |
| `startHour` | string | Yes | Sending window start in 24-hour HH:MM format, e.g. 09:00 |
@@ -578,6 +586,8 @@ Updates tracking, stop-on-activity, and sending settings for a Smartlead campaig
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Smartlead API key |
| `campaignId` | number | Yes | Smartlead campaign ID |
| `trackSettings` | array | No | Tracking to disable. Allowed values: DONT_TRACK_EMAIL_OPEN, DONT_TRACK_LINK_CLICK, DONT_TRACK_REPLY_TO_AN_EMAIL |
| `stopLeadSettings` | string | No | Lead activity that stops the sequence: REPLY_TO_AN_EMAIL, CLICK_ON_A_LINK, OPEN_AN_EMAIL |
| `sendAsPlainText` | boolean | No | Send campaign emails as plain text |
@@ -692,6 +702,8 @@ Retrieves lifetime performance totals for a Smartlead campaign — sends, opens,
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Smartlead API key |
| `campaignId` | number | Yes | Smartlead campaign ID |
#### Output
@@ -800,6 +812,8 @@ Retrieves Smartlead campaign performance totals for a date range. Smartlead reje
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Smartlead API key |
| `campaignId` | number | Yes | Smartlead campaign ID |
| `startDate` | string | Yes | Range start date in YYYY-MM-DD format |
| `endDate` | string | Yes | Range end date in YYYY-MM-DD format |
@@ -910,6 +924,8 @@ Retrieves the email sequence steps for a Smartlead campaign, including subjects,
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Smartlead API key |
| `campaignId` | number | Yes | Smartlead campaign ID |
#### Output
@@ -1018,6 +1034,8 @@ Replaces the email sequence for a Smartlead campaign. Send every step in one cal
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Smartlead API key |
| `campaignId` | number | Yes | Smartlead campaign ID |
| `sequences` | array | Yes | Ordered sequence steps. Each entry accepts seq_number, delay_in_days, subject, and email_body \(HTML\). Personalize with \{\{first_name\}\} or \{\{company_name\}\}. |
#### Output
@@ -1127,6 +1145,8 @@ Retrieves per-email statistics rows for a Smartlead campaign, filterable by sequ
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Smartlead API key |
| `campaignId` | number | Yes | Smartlead campaign ID |
| `offset` | number | No | Pagination offset \(default 0\) |
| `limit` | number | No | Rows to return \(default 100\) |
| `emailSequenceNumber` | number | No | Only return rows for this sequence step |
@@ -1241,6 +1261,8 @@ Adds leads to a Smartlead campaign, up to 400 per call. Returns per-reason count
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Smartlead API key |
| `campaignId` | number | Yes | Smartlead campaign ID |
| `leads` | array | Yes | Leads to add \(max 400\). Each entry requires email and accepts first_name, last_name, phone_number, company_name, website, location, linkedin_profile, company_url, and custom_fields. |
| `ignoreGlobalBlockList` | boolean | No | Add leads even if they are on the global block list |
| `ignoreUnsubscribeList` | boolean | No | Add leads even if they previously unsubscribed |
@@ -1354,6 +1376,8 @@ Retrieves the leads in a Smartlead campaign with their per-campaign status.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Smartlead API key |
| `campaignId` | number | Yes | Smartlead campaign ID |
| `offset` | number | No | Pagination offset \(default 0\) |
| `limit` | number | No | Leads to return per page \(default 100\) |
@@ -1464,6 +1488,7 @@ Looks up a Smartlead lead by email address and returns the lead record plus ever
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Smartlead API key |
| `email` | string | Yes | Lead email address to look up |
#### Output
@@ -1573,6 +1598,9 @@ Updates a lead in a Smartlead campaign. Smartlead requires the email field even
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Smartlead API key |
| `campaignId` | number | Yes | Smartlead campaign ID |
| `leadId` | number | Yes | Smartlead lead ID — the nested lead.id from List Campaign Leads, NOT campaign_lead_map_id |
| `email` | string | Yes | Lead email address — required by Smartlead even when unchanged |
| `firstName` | string | No | Lead first name |
| `lastName` | string | No | Lead last name |
@@ -1691,6 +1719,9 @@ Sets the category of a lead in a Smartlead campaign, such as Interested or Not I
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Smartlead API key |
| `campaignId` | number | Yes | Smartlead campaign ID |
| `leadId` | number | Yes | Smartlead lead ID — the nested lead.id from List Campaign Leads, NOT campaign_lead_map_id |
| `categoryId` | number | Yes | Lead category ID to apply |
| `pauseLead` | boolean | No | Also pause the lead sequence when applying the category |
@@ -1801,6 +1832,9 @@ Pauses a lead in a Smartlead campaign so it stops receiving sequence emails.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Smartlead API key |
| `campaignId` | number | Yes | Smartlead campaign ID |
| `leadId` | number | Yes | Smartlead lead ID — the nested lead.id from List Campaign Leads, NOT campaign_lead_map_id |
#### Output
@@ -1909,6 +1943,9 @@ Resumes a paused lead in a Smartlead campaign, optionally after a delay.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Smartlead API key |
| `campaignId` | number | Yes | Smartlead campaign ID |
| `leadId` | number | Yes | Smartlead lead ID — the nested lead.id from List Campaign Leads, NOT campaign_lead_map_id |
| `resumeLeadWithDelayDays` | number | No | Days to wait before the next email; 0 resumes immediately |
#### Output
@@ -2018,6 +2055,7 @@ Retrieves the lead categories configured on the Smartlead account, with the cate
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Smartlead API key |
#### Output
@@ -2126,6 +2164,9 @@ Retrieves the sent-and-received message history for a lead in a Smartlead campai
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Smartlead API key |
| `campaignId` | number | Yes | Smartlead campaign ID |
| `leadId` | number | Yes | Smartlead lead ID — the nested lead.id from List Campaign Leads, NOT campaign_lead_map_id |
#### Output
@@ -2234,6 +2275,8 @@ Retrieves the webhooks registered on a Smartlead campaign.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Smartlead API key |
| `campaignId` | number | Yes | Smartlead campaign ID |
#### Output
@@ -2342,6 +2385,8 @@ Creates a webhook on a Smartlead campaign, or updates an existing one when a web
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Smartlead API key |
| `campaignId` | number | Yes | Smartlead campaign ID |
| `name` | string | Yes | Webhook name |
| `webhookUrl` | string | Yes | HTTPS URL Smartlead should post events to |
| `eventTypes` | array | Yes | Events to subscribe to. Allowed values: EMAIL_SENT, EMAIL_OPEN, EMAIL_LINK_CLICK, EMAIL_REPLY, EMAIL_BOUNCE, LEAD_UNSUBSCRIBED, LEAD_CATEGORY_UPDATED |
@@ -2455,6 +2500,8 @@ Deletes a webhook from a Smartlead campaign.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Smartlead API key |
| `campaignId` | number | Yes | Smartlead campaign ID |
| `webhookId` | number | Yes | ID of the webhook to delete, from List Campaign Webhooks |
#### Output
@@ -2564,6 +2611,8 @@ Retrieves webhook delivery counts for a Smartlead campaign over a time window, f
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Smartlead API key |
| `campaignId` | number | Yes | Smartlead campaign ID |
| `fromTime` | string | Yes | Start of the window as an ISO 8601 timestamp |
| `toTime` | string | Yes | End of the window as an ISO 8601 timestamp |
@@ -2674,6 +2723,8 @@ Copies a Smartlead campaign, including its sequences and settings. The copy is n
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Smartlead API key |
| `campaignId` | number | Yes | Smartlead campaign ID |
#### Output
@@ -2782,6 +2833,8 @@ Permanently deletes a Smartlead campaign along with its sequences, leads, and we
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Smartlead API key |
| `campaignId` | number | Yes | Smartlead campaign ID |
#### Output
@@ -2890,6 +2943,8 @@ Exports every lead in a Smartlead campaign as CSV, including engagement counts a
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Smartlead API key |
| `campaignId` | number | Yes | Smartlead campaign ID |
#### Output
@@ -2998,6 +3053,8 @@ Retrieves the sending email accounts attached to a Smartlead campaign.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Smartlead API key |
| `campaignId` | number | Yes | Smartlead campaign ID |
#### Output
@@ -3106,6 +3163,8 @@ Attaches sending email accounts to a Smartlead campaign. A campaign needs at lea
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Smartlead API key |
| `campaignId` | number | Yes | Smartlead campaign ID |
| `emailAccountIds` | array | Yes | IDs of the email accounts to attach, from List Email Accounts |
#### Output
@@ -3215,6 +3274,8 @@ Detaches sending email accounts from a Smartlead campaign.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Smartlead API key |
| `campaignId` | number | Yes | Smartlead campaign ID |
| `emailAccountIds` | array | Yes | IDs of the email accounts to detach |
#### Output
@@ -3324,6 +3385,7 @@ Retrieves the sending email accounts on the Smartlead account, including their I
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Smartlead API key |
| `offset` | number | No | Pagination offset \(default 0\) |
| `limit` | number | No | Accounts to return per page \(default 100\) |
| `clientId` | number | No | Only return accounts for this client \(agency accounts\) |
@@ -3435,6 +3497,8 @@ Retrieves per-lead engagement statistics for a Smartlead campaign.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Smartlead API key |
| `campaignId` | number | Yes | Smartlead campaign ID |
| `limit` | number | No | Rows to return \(default 100\) |
| `offset` | number | No | Rows to skip \(default 0\) |
@@ -3545,6 +3609,8 @@ Retrieves per-mailbox sending statistics for a Smartlead campaign, for spotting
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Smartlead API key |
| `campaignId` | number | Yes | Smartlead campaign ID |
#### Output
@@ -3653,6 +3719,8 @@ Retrieves top-level Smartlead campaign counts for a date range, including positi
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Smartlead API key |
| `campaignId` | number | Yes | Smartlead campaign ID |
| `startDate` | string | Yes | Range start date in YYYY-MM-DD format |
| `endDate` | string | Yes | Range end date in YYYY-MM-DD format |
@@ -3763,6 +3831,7 @@ Retrieves recent lead activity across all Smartlead campaigns — opens, clicks,
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Smartlead API key |
| `offset` | number | No | Pagination offset \(default 0\) |
| `limit` | number | No | Rows to return per page |
@@ -3873,6 +3942,7 @@ Looks up a Smartlead lead by its ID.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Smartlead API key |
| `leadId` | number | Yes | Smartlead lead ID — the nested lead.id from List Campaign Leads, NOT campaign_lead_map_id |
#### Output
@@ -3982,6 +4052,9 @@ Unsubscribes a lead from a single Smartlead campaign, leaving it active in other
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Smartlead API key |
| `campaignId` | number | Yes | Smartlead campaign ID |
| `leadId` | number | Yes | Smartlead lead ID — the nested lead.id from List Campaign Leads, NOT campaign_lead_map_id |
#### Output
@@ -4090,6 +4163,7 @@ Unsubscribes a lead across every Smartlead campaign and adds it to the account-w
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Smartlead API key |
| `leadId` | number | Yes | Smartlead lead ID — the nested lead.id from List Campaign Leads, NOT campaign_lead_map_id |
#### Output
@@ -4199,6 +4273,8 @@ Marks a lead as completed in a Smartlead campaign so it stops receiving further
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Smartlead API key |
| `campaignId` | number | Yes | Smartlead campaign ID |
| `campaignLeadMapId` | number | Yes | The campaign_lead_map_id from List Campaign Leads — this endpoint takes the map ID, NOT lead.id |
#### Output
@@ -4308,6 +4384,9 @@ Removes a lead from a Smartlead campaign. The lead record itself remains on the
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Smartlead API key |
| `campaignId` | number | Yes | Smartlead campaign ID |
| `leadId` | number | Yes | Smartlead lead ID — the nested lead.id from List Campaign Leads, NOT campaign_lead_map_id |
#### Output
@@ -4416,6 +4495,7 @@ Retrieves replies from the Smartlead master inbox across all campaigns, optional
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Smartlead API key |
| `unreadOnly` | boolean | No | Return only unread replies |
| `offset` | number | No | Pagination offset \(default 0\) |
| `limit` | number | No | Replies to return per page |
@@ -4527,6 +4607,7 @@ Retrieves the lead lists on the Smartlead account with their lead counts.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Smartlead API key |
| `offset` | number | No | Pagination offset \(default 0\) |
| `limit` | number | No | Lists to return per page |
@@ -4637,6 +4718,7 @@ Retrieves a single Smartlead lead list by ID.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Smartlead API key |
| `leadListId` | number | Yes | Lead list ID |
#### Output
@@ -4746,6 +4828,7 @@ Creates a lead list on the Smartlead account.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Smartlead API key |
| `listName` | string | Yes | Name for the new lead list |
#### Output
@@ -4855,6 +4938,7 @@ Renames a Smartlead lead list.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Smartlead API key |
| `leadListId` | number | Yes | Lead list ID to update |
| `listName` | string | Yes | New name for the lead list |
@@ -4965,6 +5049,7 @@ Deletes a Smartlead lead list.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Smartlead API key |
| `leadListId` | number | Yes | Lead list ID to delete |
#### Output
@@ -5074,6 +5159,7 @@ Retrieves the clients on a Smartlead agency account, including the client IDs us
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Smartlead API key |
#### Output
@@ -26,6 +26,8 @@ Execute one parameterized SQL statement through the Snowflake SQL API.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) |
| `domain` | string | No | Snowflake account host injected by the executor from the selected credential |
| `role` | string | No | Snowflake role to use for this statement |
| `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds |
| `warehouse` | string | No | Warehouse to use for this statement; defaults to the PAT user setting |
@@ -72,6 +74,8 @@ Check a running or completed statement and retrieve exactly one result partition
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) |
| `domain` | string | No | Snowflake account host injected by the executor from the selected credential |
| `statementHandle` | string | Yes | Statement handle returned by Snowflake |
| `partition` | number | No | Zero-based result partition to retrieve; defaults to 0 |
| `partitionCount` | number | No | Total number of result partitions, taken from the partitionCount of the first partition. Snowflake omits metadata from every later partition response, so supply this when fetching partition 1 or higher to keep truncated and nextPartition accurate |
@@ -112,6 +116,8 @@ Cancel a running Snowflake SQL API statement.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) |
| `domain` | string | No | Snowflake account host injected by the executor from the selected credential |
| `statementHandle` | string | Yes | Statement handle returned by Snowflake |
#### Output
@@ -150,6 +156,8 @@ Insert structured JSON rows using bound values.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) |
| `domain` | string | No | Snowflake account host injected by the executor from the selected credential |
| `role` | string | No | Snowflake role to use for this statement |
| `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds |
| `warehouse` | string | No | Warehouse to use for this statement; defaults to the PAT user setting |
@@ -194,6 +202,8 @@ Update matching rows with a bound MERGE statement without inserting new rows.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) |
| `domain` | string | No | Snowflake account host injected by the executor from the selected credential |
| `role` | string | No | Snowflake role to use for this statement |
| `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds |
| `warehouse` | string | No | Warehouse to use for this statement; defaults to the PAT user setting |
@@ -239,6 +249,8 @@ Update matching rows and insert unmatched rows with a bound MERGE statement.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) |
| `domain` | string | No | Snowflake account host injected by the executor from the selected credential |
| `role` | string | No | Snowflake role to use for this statement |
| `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds |
| `warehouse` | string | No | Warehouse to use for this statement; defaults to the PAT user setting |
@@ -284,6 +296,8 @@ Delete rows matching a required set of bound column filters.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) |
| `domain` | string | No | Snowflake account host injected by the executor from the selected credential |
| `role` | string | No | Snowflake role to use for this statement |
| `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds |
| `warehouse` | string | No | Warehouse to use for this statement; defaults to the PAT user setting |
@@ -328,6 +342,8 @@ Load files from an existing Snowflake stage with COPY INTO.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) |
| `domain` | string | No | Snowflake account host injected by the executor from the selected credential |
| `role` | string | No | Snowflake role to use for this statement |
| `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds |
| `warehouse` | string | No | Warehouse to use for this statement; defaults to the PAT user setting |
@@ -379,6 +395,8 @@ Export a Snowflake table to files in a stage with COPY INTO.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) |
| `domain` | string | No | Snowflake account host injected by the executor from the selected credential |
| `role` | string | No | Snowflake role to use for this statement |
| `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds |
| `warehouse` | string | No | Warehouse to use for this statement; defaults to the PAT user setting |
@@ -429,6 +447,8 @@ List the databases the credential can access.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) |
| `domain` | string | No | Snowflake account host injected by the executor from the selected credential |
| `role` | string | No | Snowflake role to use for this statement |
| `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds |
| `nameLike` | string | No | Optional SQL LIKE pattern for object names |
@@ -470,6 +490,8 @@ List the schemas in a Snowflake database.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) |
| `domain` | string | No | Snowflake account host injected by the executor from the selected credential |
| `role` | string | No | Snowflake role to use for this statement |
| `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds |
| `database` | string | Yes | Database name |
@@ -512,6 +534,8 @@ List the tables in a Snowflake schema.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) |
| `domain` | string | No | Snowflake account host injected by the executor from the selected credential |
| `role` | string | No | Snowflake role to use for this statement |
| `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds |
| `database` | string | Yes | Database name |
@@ -555,6 +579,8 @@ List warehouses visible to the active Snowflake role.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) |
| `domain` | string | No | Snowflake account host injected by the executor from the selected credential |
| `role` | string | No | Snowflake role to use for this statement |
| `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds |
| `maxRows` | number | No | Maximum result rows; defaults to 1000 with a Sim safety limit of 10000 |
@@ -596,6 +622,8 @@ Get the full details for a Snowflake virtual warehouse.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) |
| `domain` | string | No | Snowflake account host injected by the executor from the selected credential |
| `role` | string | No | Snowflake role to use for this statement |
| `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds |
| `warehouseName` | string | Yes | Warehouse name |
@@ -636,6 +664,8 @@ Resume a Snowflake virtual warehouse if it is suspended.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) |
| `domain` | string | No | Snowflake account host injected by the executor from the selected credential |
| `role` | string | No | Snowflake role to use for this statement |
| `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds |
| `warehouseName` | string | Yes | Warehouse name |
@@ -676,6 +706,8 @@ Suspend a Snowflake virtual warehouse.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) |
| `domain` | string | No | Snowflake account host injected by the executor from the selected credential |
| `role` | string | No | Snowflake role to use for this statement |
| `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds |
| `warehouseName` | string | Yes | Warehouse name |
@@ -716,6 +748,8 @@ Resize a Snowflake warehouse or change its auto-suspend and auto-resume settings
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) |
| `domain` | string | No | Snowflake account host injected by the executor from the selected credential |
| `role` | string | No | Snowflake role to use for this statement |
| `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds |
| `warehouseName` | string | Yes | Warehouse name |
@@ -759,6 +793,8 @@ List tasks in a Snowflake schema.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) |
| `domain` | string | No | Snowflake account host injected by the executor from the selected credential |
| `role` | string | No | Snowflake role to use for this statement |
| `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds |
| `database` | string | Yes | Database name |
@@ -802,6 +838,8 @@ Describe a Snowflake task.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) |
| `domain` | string | No | Snowflake account host injected by the executor from the selected credential |
| `role` | string | No | Snowflake role to use for this statement |
| `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds |
| `database` | string | Yes | Database name |
@@ -844,6 +882,8 @@ Run a Snowflake task immediately, optionally retrying its last failed graph.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) |
| `domain` | string | No | Snowflake account host injected by the executor from the selected credential |
| `role` | string | No | Snowflake role to use for this statement |
| `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds |
| `database` | string | Yes | Database name |
@@ -887,6 +927,8 @@ Resume a suspended Snowflake task so its schedule runs again.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) |
| `domain` | string | No | Snowflake account host injected by the executor from the selected credential |
| `role` | string | No | Snowflake role to use for this statement |
| `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds |
| `database` | string | Yes | Database name |
@@ -929,6 +971,8 @@ Suspend a Snowflake task so its schedule stops triggering runs.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) |
| `domain` | string | No | Snowflake account host injected by the executor from the selected credential |
| `role` | string | No | Snowflake role to use for this statement |
| `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds |
| `database` | string | Yes | Database name |
@@ -971,6 +1015,8 @@ Query up to seven days of Snowflake task history, capped at 10000 rows.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) |
| `domain` | string | No | Snowflake account host injected by the executor from the selected credential |
| `role` | string | No | Snowflake role to use for this statement |
| `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds |
| `warehouse` | string | No | Warehouse to use for this statement; defaults to the PAT user setting |
@@ -1016,6 +1062,8 @@ Find one task history record by query ID within Snowflakes seven-day window a
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) |
| `domain` | string | No | Snowflake account host injected by the executor from the selected credential |
| `role` | string | No | Snowflake role to use for this statement |
| `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds |
| `warehouse` | string | No | Warehouse to use for this statement; defaults to the PAT user setting |
@@ -1060,6 +1108,8 @@ Cancel one running task query by query ID with SYSTEM$CANCEL_QUERY. Task runs al
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) |
| `domain` | string | No | Snowflake account host injected by the executor from the selected credential |
| `role` | string | No | Snowflake role to use for this statement |
| `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds |
| `warehouse` | string | No | Warehouse to use for this statement; defaults to the PAT user setting |
@@ -1101,6 +1151,8 @@ Read a task query result with RESULT_SCAN during Snowflakes 24-hour retention
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) |
| `domain` | string | No | Snowflake account host injected by the executor from the selected credential |
| `role` | string | No | Snowflake role to use for this statement |
| `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds |
| `warehouse` | string | No | Warehouse to use for this statement; defaults to the PAT user setting |
@@ -1143,6 +1195,8 @@ List queries that completed in the last seven days, optionally filtered.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) |
| `domain` | string | No | Snowflake account host injected by the executor from the selected credential |
| `role` | string | No | Snowflake role to use for this statement |
| `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds |
| `warehouse` | string | No | Warehouse to use for this statement; defaults to the PAT user setting |
@@ -1189,6 +1243,8 @@ List staged-file load results for a table over the last fourteen days.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) |
| `domain` | string | No | Snowflake account host injected by the executor from the selected credential |
| `role` | string | No | Snowflake role to use for this statement |
| `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds |
| `warehouse` | string | No | Warehouse to use for this statement; defaults to the PAT user setting |
@@ -1235,6 +1291,8 @@ Inspect table and column metadata through Snowflake INFORMATION_SCHEMA views.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) |
| `domain` | string | No | Snowflake account host injected by the executor from the selected credential |
| `role` | string | No | Snowflake role to use for this statement |
| `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds |
| `warehouse` | string | No | Warehouse to use for this statement; defaults to the PAT user setting |
@@ -1280,6 +1338,8 @@ Call a stored procedure with explicitly typed Snowflake bindings.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `oauthCredential` | string | Yes | Snowflake credential \(account host and programmatic access token\) |
| `domain` | string | No | Snowflake account host injected by the executor from the selected credential |
| `role` | string | No | Snowflake role to use for this statement |
| `statementTimeoutSeconds` | number | No | Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds |
| `warehouse` | string | No | Warehouse to use for this statement; defaults to the PAT user setting |
@@ -1316,3 +1376,5 @@ Call a stored procedure with explicitly typed Snowflake bindings.
| ↳ `rowsDeleted` | number | Rows deleted by the statement |
| ↳ `duplicateRowsUpdated` | number | Duplicate rows updated by the statement |
| ↳ `rowsAffected` | number | Total inserted, updated, and deleted rows |
@@ -26,6 +26,12 @@ Run an SPL search synchronously and return its results in a single call (oneshot
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `baseUrl` | string | Yes | Splunk management URL including the management port \(e.g. https://splunk.example.com:8089\) |
| `authToken` | string | No | Splunk authentication token, sent as a bearer token. Preferred over a password. |
| `username` | string | No | Splunk username, used for basic authentication when no token is supplied |
| `password` | string | No | Splunk password, used for basic authentication when no token is supplied |
| `owner` | string | No | Namespace owner for /servicesNS requests \(e.g. admin, or nobody for app-shared objects\). Leave both this and the app empty to use the authenticated user context; set only one and the other becomes the - wildcard. |
| `app` | string | No | Namespace app context for /servicesNS requests \(e.g. search\). Leave both this and the owner empty to use the authenticated user context; set only one and the other becomes the - wildcard. |
| `search` | string | Yes | SPL search string \(e.g. index=main error \| stats count by host\). The leading "search" command is added automatically when omitted. |
| `earliestTime` | string | No | Earliest \(inclusive\) time bound — relative \(e.g. -24h, -7d@d\) or absolute epoch/formatted time |
| `latestTime` | string | No | Latest \(exclusive\) time bound — relative \(e.g. now\) or absolute time |
@@ -94,6 +100,12 @@ Start a Splunk search job and return its search ID (sid). The search runs asynch
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `baseUrl` | string | Yes | Splunk management URL including the management port \(e.g. https://splunk.example.com:8089\) |
| `authToken` | string | No | Splunk authentication token, sent as a bearer token. Preferred over a password. |
| `username` | string | No | Splunk username, used for basic authentication when no token is supplied |
| `password` | string | No | Splunk password, used for basic authentication when no token is supplied |
| `owner` | string | No | Namespace owner for /servicesNS requests \(e.g. admin, or nobody for app-shared objects\). Leave both this and the app empty to use the authenticated user context; set only one and the other becomes the - wildcard. |
| `app` | string | No | Namespace app context for /servicesNS requests \(e.g. search\). Leave both this and the owner empty to use the authenticated user context; set only one and the other becomes the - wildcard. |
| `search` | string | Yes | SPL search string \(e.g. index=main sourcetype=access_combined \| timechart count\). The leading "search" command is added automatically when omitted. |
| `earliestTime` | string | No | Earliest \(inclusive\) time bound — relative \(e.g. -24h\) or absolute time |
| `latestTime` | string | No | Latest \(exclusive\) time bound — relative \(e.g. now\) or absolute time |
@@ -121,6 +133,12 @@ Get the status and progress of a Splunk search job by search ID, including dispa
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `baseUrl` | string | Yes | Splunk management URL including the management port \(e.g. https://splunk.example.com:8089\) |
| `authToken` | string | No | Splunk authentication token, sent as a bearer token. Preferred over a password. |
| `username` | string | No | Splunk username, used for basic authentication when no token is supplied |
| `password` | string | No | Splunk password, used for basic authentication when no token is supplied |
| `owner` | string | No | Namespace owner for /servicesNS requests \(e.g. admin, or nobody for app-shared objects\). Leave both this and the app empty to use the authenticated user context; set only one and the other becomes the - wildcard. |
| `app` | string | No | Namespace app context for /servicesNS requests \(e.g. search\). Leave both this and the owner empty to use the authenticated user context; set only one and the other becomes the - wildcard. |
| `sid` | string | Yes | Search ID of the job to inspect \(e.g. 1457683115.100\) |
#### Output
@@ -161,6 +179,12 @@ Fetch the transformed results of a completed Splunk search job by search ID, wit
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `baseUrl` | string | Yes | Splunk management URL including the management port \(e.g. https://splunk.example.com:8089\) |
| `authToken` | string | No | Splunk authentication token, sent as a bearer token. Preferred over a password. |
| `username` | string | No | Splunk username, used for basic authentication when no token is supplied |
| `password` | string | No | Splunk password, used for basic authentication when no token is supplied |
| `owner` | string | No | Namespace owner for /servicesNS requests \(e.g. admin, or nobody for app-shared objects\). Leave both this and the app empty to use the authenticated user context; set only one and the other becomes the - wildcard. |
| `app` | string | No | Namespace app context for /servicesNS requests \(e.g. search\). Leave both this and the owner empty to use the authenticated user context; set only one and the other becomes the - wildcard. |
| `sid` | string | Yes | Search ID of the job whose results to fetch \(e.g. 1457683115.100\) |
| `count` | number | No | Maximum number of result rows to return. Defaults to 100. Page through larger result sets with offset rather than raising this — a completed job can hold millions of rows. |
| `offset` | number | No | First result row \(0-indexed\) from which to begin returning data |
@@ -228,6 +252,12 @@ Cancel a running Splunk search job and delete its result cache.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `baseUrl` | string | Yes | Splunk management URL including the management port \(e.g. https://splunk.example.com:8089\) |
| `authToken` | string | No | Splunk authentication token, sent as a bearer token. Preferred over a password. |
| `username` | string | No | Splunk username, used for basic authentication when no token is supplied |
| `password` | string | No | Splunk password, used for basic authentication when no token is supplied |
| `owner` | string | No | Namespace owner for /servicesNS requests \(e.g. admin, or nobody for app-shared objects\). Leave both this and the app empty to use the authenticated user context; set only one and the other becomes the - wildcard. |
| `app` | string | No | Namespace app context for /servicesNS requests \(e.g. search\). Leave both this and the owner empty to use the authenticated user context; set only one and the other becomes the - wildcard. |
| `sid` | string | Yes | Search ID of the job to cancel \(e.g. 1457683115.100\) |
#### Output
@@ -244,6 +274,12 @@ List saved searches and reports configured in Splunk, including their SPL, sched
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `baseUrl` | string | Yes | Splunk management URL including the management port \(e.g. https://splunk.example.com:8089\) |
| `authToken` | string | No | Splunk authentication token, sent as a bearer token. Preferred over a password. |
| `username` | string | No | Splunk username, used for basic authentication when no token is supplied |
| `password` | string | No | Splunk password, used for basic authentication when no token is supplied |
| `owner` | string | No | Namespace owner for /servicesNS requests \(e.g. admin, or nobody for app-shared objects\). Leave both this and the app empty to use the authenticated user context; set only one and the other becomes the - wildcard. |
| `app` | string | No | Namespace app context for /servicesNS requests \(e.g. search\). Leave both this and the owner empty to use the authenticated user context; set only one and the other becomes the - wildcard. |
| `search` | string | No | Filter saved searches. A bare term matches as a substring across fields \(e.g. Errors\); field_name=field_value matches one field \(e.g. is_scheduled=1\). |
| `count` | number | No | Maximum number of saved searches to return \(e.g. 50\). 0 returns all. |
| `offset` | number | No | Index of the first saved search to return, for pagination |
@@ -277,6 +313,12 @@ Get the configuration of a single Splunk saved search by name, including its SPL
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `baseUrl` | string | Yes | Splunk management URL including the management port \(e.g. https://splunk.example.com:8089\) |
| `authToken` | string | No | Splunk authentication token, sent as a bearer token. Preferred over a password. |
| `username` | string | No | Splunk username, used for basic authentication when no token is supplied |
| `password` | string | No | Splunk password, used for basic authentication when no token is supplied |
| `owner` | string | No | Namespace owner for /servicesNS requests \(e.g. admin, or nobody for app-shared objects\). Leave both this and the app empty to use the authenticated user context; set only one and the other becomes the - wildcard. |
| `app` | string | No | Namespace app context for /servicesNS requests \(e.g. search\). Leave both this and the owner empty to use the authenticated user context; set only one and the other becomes the - wildcard. |
| `name` | string | Yes | Name of the saved search \(e.g. Errors in the last 24 hours\) |
#### Output
@@ -307,6 +349,12 @@ Run a Splunk saved search immediately and return the search ID (sid) of the disp
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `baseUrl` | string | Yes | Splunk management URL including the management port \(e.g. https://splunk.example.com:8089\) |
| `authToken` | string | No | Splunk authentication token, sent as a bearer token. Preferred over a password. |
| `username` | string | No | Splunk username, used for basic authentication when no token is supplied |
| `password` | string | No | Splunk password, used for basic authentication when no token is supplied |
| `owner` | string | No | Namespace owner for /servicesNS requests \(e.g. admin, or nobody for app-shared objects\). Leave both this and the app empty to use the authenticated user context; set only one and the other becomes the - wildcard. |
| `app` | string | No | Namespace app context for /servicesNS requests \(e.g. search\). Leave both this and the owner empty to use the authenticated user context; set only one and the other becomes the - wildcard. |
| `name` | string | Yes | Name of the saved search to run \(e.g. Errors in the last 24 hours\) |
| `triggerActions` | boolean | No | Whether to trigger the saved search alert actions on this run |
| `dispatchEarliestTime` | string | No | Override the earliest time bound for this run — relative \(e.g. -24h\) or absolute time |
@@ -330,6 +378,12 @@ List the saved searches with currently triggered (unexpired) Splunk alerts and h
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `baseUrl` | string | Yes | Splunk management URL including the management port \(e.g. https://splunk.example.com:8089\) |
| `authToken` | string | No | Splunk authentication token, sent as a bearer token. Preferred over a password. |
| `username` | string | No | Splunk username, used for basic authentication when no token is supplied |
| `password` | string | No | Splunk password, used for basic authentication when no token is supplied |
| `owner` | string | No | Namespace owner for /servicesNS requests \(e.g. admin, or nobody for app-shared objects\). Leave both this and the app empty to use the authenticated user context; set only one and the other becomes the - wildcard. |
| `app` | string | No | Namespace app context for /servicesNS requests \(e.g. search\). Leave both this and the owner empty to use the authenticated user context; set only one and the other becomes the - wildcard. |
| `count` | number | No | Maximum number of entries to return \(e.g. 50\). 0 returns all. |
| `offset` | number | No | Index of the first entry to return, for pagination |
@@ -351,6 +405,12 @@ List the unexpired triggered instances of a Splunk alert by saved search name, i
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `baseUrl` | string | Yes | Splunk management URL including the management port \(e.g. https://splunk.example.com:8089\) |
| `authToken` | string | No | Splunk authentication token, sent as a bearer token. Preferred over a password. |
| `username` | string | No | Splunk username, used for basic authentication when no token is supplied |
| `password` | string | No | Splunk password, used for basic authentication when no token is supplied |
| `owner` | string | No | Namespace owner for /servicesNS requests \(e.g. admin, or nobody for app-shared objects\). Leave both this and the app empty to use the authenticated user context; set only one and the other becomes the - wildcard. |
| `app` | string | No | Namespace app context for /servicesNS requests \(e.g. search\). Leave both this and the owner empty to use the authenticated user context; set only one and the other becomes the - wildcard. |
| `name` | string | Yes | Name of the alerting saved search \(e.g. Errors in the last 24 hours\). Use - to return the fired alerts of every saved search. |
#### Output
@@ -379,6 +439,12 @@ List the indexes configured on the Splunk instance with their size, event count,
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `baseUrl` | string | Yes | Splunk management URL including the management port \(e.g. https://splunk.example.com:8089\) |
| `authToken` | string | No | Splunk authentication token, sent as a bearer token. Preferred over a password. |
| `username` | string | No | Splunk username, used for basic authentication when no token is supplied |
| `password` | string | No | Splunk password, used for basic authentication when no token is supplied |
| `owner` | string | No | Namespace owner for /servicesNS requests \(e.g. admin, or nobody for app-shared objects\). Leave both this and the app empty to use the authenticated user context; set only one and the other becomes the - wildcard. |
| `app` | string | No | Namespace app context for /servicesNS requests \(e.g. search\). Leave both this and the owner empty to use the authenticated user context; set only one and the other becomes the - wildcard. |
| `datatype` | string | No | Filter indexes by type: all, event, or metric. Splunk defaults to event, so pass all to include metric indexes. |
| `count` | number | No | Maximum number of indexes to return \(e.g. 50\). 0 returns all. |
| `offset` | number | No | Index of the first entry to return, for pagination |
@@ -412,6 +478,12 @@ List the apps installed on the Splunk instance with their label, version, author
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `baseUrl` | string | Yes | Splunk management URL including the management port \(e.g. https://splunk.example.com:8089\) |
| `authToken` | string | No | Splunk authentication token, sent as a bearer token. Preferred over a password. |
| `username` | string | No | Splunk username, used for basic authentication when no token is supplied |
| `password` | string | No | Splunk password, used for basic authentication when no token is supplied |
| `owner` | string | No | Namespace owner for /servicesNS requests \(e.g. admin, or nobody for app-shared objects\). Leave both this and the app empty to use the authenticated user context; set only one and the other becomes the - wildcard. |
| `app` | string | No | Namespace app context for /servicesNS requests \(e.g. search\). Leave both this and the owner empty to use the authenticated user context; set only one and the other becomes the - wildcard. |
| `count` | number | No | Maximum number of apps to return \(e.g. 50\). 0 returns all. |
| `offset` | number | No | Index of the first app to return, for pagination |
File diff suppressed because it is too large Load Diff
+109 -1
View File
@@ -18150,6 +18150,114 @@
"iconName": "ServiceNowIcon",
"docsUrl": "https://docs.sim.ai/integrations/servicenow",
"operations": [
{
"name": "Create Incident",
"description": "Create an incident in ServiceNow. Reference fields (caller, assignment group, assigned to, configuration item) take sys_ids unless input display value is enabled."
},
{
"name": "Get Incident",
"description": "Retrieve a single ServiceNow incident by number (e.g., INC0010001) or sys_id. Reference fields are returned with both their sys_id and their label by default."
},
{
"name": "List Incidents",
"description": "Search ServiceNow incidents by state, priority, assignment, caller, or text. All filters are ANDed together."
},
{
"name": "Update Incident",
"description": "Update fields on an existing ServiceNow incident. Only the fields you supply are changed. Reference fields take sys_ids unless input display value is enabled."
},
{
"name": "Resolve Incident",
"description": "Move a ServiceNow incident to Resolved (state 6) with a resolution code and resolution notes."
},
{
"name": "Close Incident",
"description": "Move a ServiceNow incident to Closed (state 7) with a resolution code and resolution notes. Which roles may close an incident is instance-configurable, so the credential may need elevated rights."
},
{
"name": "Add Incident Comment",
"description": "Append an internal work note or a customer-visible additional comment to a ServiceNow incident. Both are journal fields, so the text is appended rather than replacing earlier entries."
},
{
"name": "Create Change Request",
"description": "Create a change request in ServiceNow. Reference fields (assignment group, assigned to, requested by, configuration item) take sys_ids unless input display value is enabled."
},
{
"name": "Get Change Request",
"description": "Retrieve a single ServiceNow change request by number (e.g., CHG0030001) or sys_id. Reference fields are returned with both their sys_id and their label by default."
},
{
"name": "List Change Requests",
"description": "Search ServiceNow change requests by state, type, risk, assignment, or text. All filters are ANDed together."
},
{
"name": "Update Change Request",
"description": "Update fields on an existing ServiceNow change request. Only the fields you supply are changed. Use Move ServiceNow Change State for state transitions."
},
{
"name": "Move Change State",
"description": "Move a ServiceNow change request to another state. Base-system change model states are -5=New, -4=Assess, -3=Authorize, -2=Scheduled, -1=Implement, 0=Review, 3=Closed, 4=Canceled. The state machine rejects transitions whose conditions are not met."
},
{
"name": "List Change Tasks",
"description": "List the change tasks belonging to a ServiceNow change request, via the Change Management API. Every field is returned as {value, display_value}, so a reference field carries both its sys_id and its label. This endpoint is not the Table API: the shape is fixed with no display-value option, and the results come back under `tasks` rather than the `records` the other list operations use."
},
{
"name": "Get Change Next States",
"description": "Read the states a ServiceNow change request can actually move to next, with the instance's own state-to-label map and the conditions each transition still has to meet. Use this instead of assuming the base-system state codes, which a customized change model can change."
},
{
"name": "List Catalog Items",
"description": "Browse or search the ServiceNow service catalog. Returns each item with its sys_id, name, description, type, category, and catalogs, which is what Order ServiceNow Catalog Item needs."
},
{
"name": "Order Catalog Item",
"description": "Submit a service catalog request for a catalog item using the Service Catalog API order_now endpoint. Returns the generated request number and sys_id."
},
{
"name": "List Requested Items",
"description": "List requested items (RITMs) from the ServiceNow Requested Item [sc_req_item] table, optionally scoped to a parent request or a catalog item. To scope by requester, pass an encoded query against the parent request, for example \"request.requested_for=<sys_id>\"."
},
{
"name": "Get Requested Item",
"description": "Retrieve a single ServiceNow requested item (RITM) by number (e.g., RITM0010001) or sys_id from the Requested Item [sc_req_item] table."
},
{
"name": "List Approvals",
"description": "List approval records from the ServiceNow Approval [sysapproval_approver] table. Defaults to the \"requested\" state, which is what a user's pending approvals look like."
},
{
"name": "Approve or Reject",
"description": "Approve or reject a ServiceNow approval record by setting its state on the Approval [sysapproval_approver] table."
},
{
"name": "Search Configuration Items",
"description": "Search the ServiceNow CMDB for configuration items. Defaults to the base cmdb_ci table, which returns CIs of every class; pass a CI class to scope the search to a subclass such as cmdb_ci_linux_server."
},
{
"name": "Get Configuration Item",
"description": "Retrieve a configuration item and its CMDB relationships through the CMDB Instance API. Returns the CI attributes plus its inbound and outbound relations, each carrying the related CI and the relationship type."
},
{
"name": "List CI Relationships",
"description": "List rows from the CI Relationship [cmdb_rel_ci] table for a configuration item. Each row carries the parent CI, the child CI, and the relationship type."
},
{
"name": "Search Knowledge",
"description": "Search ServiceNow knowledge base articles through the Knowledge Management API. Returns ranked results with a snippet and the article number, which Get ServiceNow Knowledge Article accepts to fetch the full body."
},
{
"name": "Get Knowledge Article",
"description": "Retrieve the full content of a ServiceNow knowledge article by sys_id or KB number through the Knowledge Management API."
},
{
"name": "Find User",
"description": "Look up ServiceNow users by email, user name, or display name. Use this to resolve the sys_id needed by reference fields such as caller_id, assigned_to, and approver."
},
{
"name": "List Group Members",
"description": "List the members of a ServiceNow group from the Group Member [sys_user_grmember] table. Each row links a user to a group, so use it to find who can be assigned work for an assignment group."
},
{
"name": "Create Record",
"description": "Create a new record in a ServiceNow table"
@@ -18183,7 +18291,7 @@
"description": "Attach a file to a ServiceNow record"
}
],
"operationCount": 8,
"operationCount": 35,
"triggers": [
{
"id": "servicenow_incident_created",
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+54
View File
@@ -3803,12 +3803,39 @@ import {
} from '@/tools/sentry'
import { serperSearchTool } from '@/tools/serper'
import {
servicenowAddIncidentCommentTool,
servicenowAggregateTool,
servicenowCloseIncidentTool,
servicenowCreateChangeRequestTool,
servicenowCreateIncidentTool,
servicenowCreateRecordTool,
servicenowDeleteRecordTool,
servicenowDownloadAttachmentTool,
servicenowFindUserTool,
servicenowGetChangeNextStatesTool,
servicenowGetChangeRequestTool,
servicenowGetCiTool,
servicenowGetIncidentTool,
servicenowGetKnowledgeArticleTool,
servicenowGetRequestedItemTool,
servicenowListApprovalsTool,
servicenowListAttachmentsTool,
servicenowListCatalogItemsTool,
servicenowListChangeRequestsTool,
servicenowListChangeTasksTool,
servicenowListCiRelationshipsTool,
servicenowListGroupMembersTool,
servicenowListIncidentsTool,
servicenowListRequestedItemsTool,
servicenowOrderCatalogItemTool,
servicenowReadRecordTool,
servicenowResolveIncidentTool,
servicenowSearchCisTool,
servicenowSearchKnowledgeTool,
servicenowUpdateApprovalTool,
servicenowUpdateChangeRequestTool,
servicenowUpdateChangeStateTool,
servicenowUpdateIncidentTool,
servicenowUpdateRecordTool,
servicenowUploadAttachmentTool,
} from '@/tools/servicenow'
@@ -6258,6 +6285,33 @@ export const tools: Record<string, ToolConfig> = {
servicenow_list_attachments: servicenowListAttachmentsTool,
servicenow_download_attachment: servicenowDownloadAttachmentTool,
servicenow_upload_attachment: servicenowUploadAttachmentTool,
servicenow_create_incident: servicenowCreateIncidentTool,
servicenow_get_incident: servicenowGetIncidentTool,
servicenow_list_incidents: servicenowListIncidentsTool,
servicenow_update_incident: servicenowUpdateIncidentTool,
servicenow_resolve_incident: servicenowResolveIncidentTool,
servicenow_close_incident: servicenowCloseIncidentTool,
servicenow_add_incident_comment: servicenowAddIncidentCommentTool,
servicenow_create_change_request: servicenowCreateChangeRequestTool,
servicenow_get_change_request: servicenowGetChangeRequestTool,
servicenow_list_change_requests: servicenowListChangeRequestsTool,
servicenow_update_change_request: servicenowUpdateChangeRequestTool,
servicenow_update_change_state: servicenowUpdateChangeStateTool,
servicenow_list_change_tasks: servicenowListChangeTasksTool,
servicenow_get_change_next_states: servicenowGetChangeNextStatesTool,
servicenow_list_catalog_items: servicenowListCatalogItemsTool,
servicenow_order_catalog_item: servicenowOrderCatalogItemTool,
servicenow_list_requested_items: servicenowListRequestedItemsTool,
servicenow_get_requested_item: servicenowGetRequestedItemTool,
servicenow_list_approvals: servicenowListApprovalsTool,
servicenow_update_approval: servicenowUpdateApprovalTool,
servicenow_search_cis: servicenowSearchCisTool,
servicenow_get_ci: servicenowGetCiTool,
servicenow_list_ci_relationships: servicenowListCiRelationshipsTool,
servicenow_search_knowledge: servicenowSearchKnowledgeTool,
servicenow_get_knowledge_article: servicenowGetKnowledgeArticleTool,
servicenow_find_user: servicenowFindUserTool,
servicenow_list_group_members: servicenowListGroupMembersTool,
sixtyfour_find_phone: sixtyfourFindPhoneTool,
sixtyfour_find_email: sixtyfourFindEmailTool,
sixtyfour_enrich_lead: sixtyfourEnrichLeadTool,
@@ -0,0 +1,65 @@
import { SERVICENOW_TABLES } from '@/tools/servicenow/constants'
import {
authParams,
recordOutputs,
requiredSysIdParam,
writeParams,
} from '@/tools/servicenow/params'
import type {
ServiceNowAddCommentParams,
ServiceNowSingleRecordResponse,
} from '@/tools/servicenow/types'
import {
buildServiceNowHeaders,
buildTableRecordUrl,
transformRecordResponse,
} from '@/tools/servicenow/utils'
import type { ToolConfig } from '@/tools/types'
export const addIncidentCommentTool: ToolConfig<
ServiceNowAddCommentParams,
ServiceNowSingleRecordResponse
> = {
id: 'servicenow_add_incident_comment',
name: 'Add ServiceNow Incident Comment',
description:
'Append an internal work note or a customer-visible additional comment to a ServiceNow incident. Both are journal fields, so the text is appended rather than replacing earlier entries.',
version: '1.0.0',
params: {
...authParams,
...requiredSysIdParam,
comment: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Text to append to the journal field.',
},
commentField: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Which journal field to write to: "work_notes" for an internal note (default) or "comments" for a customer-visible additional comment.',
},
...writeParams,
},
request: {
url: (params) => buildTableRecordUrl(params, SERVICENOW_TABLES.INCIDENT),
method: 'PATCH',
headers: (params) => buildServiceNowHeaders(params, { json: true }),
body: (params) => {
const comment = params.comment?.trim()
if (!comment) {
throw new Error('A comment is required')
}
const field = params.commentField === 'comments' ? 'comments' : 'work_notes'
return { [field]: comment }
},
},
transformResponse: transformRecordResponse,
outputs: recordOutputs,
}
+3 -14
View File
@@ -3,7 +3,7 @@ import type {
ServiceNowAggregateParams,
ServiceNowAggregateResponse,
} from '@/tools/servicenow/types'
import { createBasicAuthHeader } from '@/tools/servicenow/utils'
import { buildServiceNowHeaders, normalizeInstanceUrl } from '@/tools/servicenow/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('ServiceNowAggregateTool')
@@ -99,10 +99,7 @@ export const aggregateTool: ToolConfig<ServiceNowAggregateParams, ServiceNowAggr
request: {
url: (params) => {
const baseUrl = params.instanceUrl.trim().replace(/\/$/, '')
if (!baseUrl) {
throw new Error('ServiceNow instance URL is required')
}
const baseUrl = normalizeInstanceUrl(params.instanceUrl)
const url = `${baseUrl}/api/now/stats/${params.tableName.trim()}`
const queryParams = new URLSearchParams()
@@ -139,15 +136,7 @@ export const aggregateTool: ToolConfig<ServiceNowAggregateParams, ServiceNowAggr
return queryString ? `${url}?${queryString}` : url
},
method: 'GET',
headers: (params) => {
if (!params.username || !params.password) {
throw new Error('ServiceNow username and password are required')
}
return {
Authorization: createBasicAuthHeader(params.username, params.password),
Accept: 'application/json',
}
},
headers: (params) => buildServiceNowHeaders(params),
},
transformResponse: async (response: Response) => {
@@ -0,0 +1,76 @@
import { INCIDENT_STATE, SERVICENOW_TABLES } from '@/tools/servicenow/constants'
import {
additionalFieldsParam,
authParams,
recordOutputs,
requiredSysIdParam,
writeParams,
} from '@/tools/servicenow/params'
import type {
ServiceNowResolveIncidentParams,
ServiceNowSingleRecordResponse,
} from '@/tools/servicenow/types'
import {
buildFieldPayload,
buildServiceNowHeaders,
buildTableRecordUrl,
transformRecordResponse,
} from '@/tools/servicenow/utils'
import type { ToolConfig } from '@/tools/types'
export const closeIncidentTool: ToolConfig<
ServiceNowResolveIncidentParams,
ServiceNowSingleRecordResponse
> = {
id: 'servicenow_close_incident',
name: 'Close ServiceNow Incident',
description:
'Move a ServiceNow incident to Closed (state 7) with a resolution code and resolution notes. Which roles may close an incident is instance-configurable, so the credential may need elevated rights.',
version: '1.0.0',
params: {
...authParams,
...requiredSysIdParam,
closeCode: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Resolution code (close_code). This is a choice field whose values are configured per instance — read the choice list on your incident table and pass one of its values.',
},
closeNotes: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Resolution notes (close_notes) documenting how the incident was resolved.',
},
workNotes: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Additional internal work note to append.',
},
...additionalFieldsParam,
...writeParams,
},
request: {
url: (params) => buildTableRecordUrl(params, SERVICENOW_TABLES.INCIDENT),
method: 'PATCH',
headers: (params) => buildServiceNowHeaders(params, { json: true }),
body: (params) =>
buildFieldPayload(
{
state: INCIDENT_STATE.CLOSED,
close_code: params.closeCode,
close_notes: params.closeNotes,
work_notes: params.workNotes,
},
params.additionalFields
),
},
transformResponse: transformRecordResponse,
outputs: recordOutputs,
}
+158
View File
@@ -0,0 +1,158 @@
/**
* Out-of-box coded values for the ServiceNow tables the semantic tools target.
*
* ServiceNow stores choice fields as coded values, not labels `state` on an
* incident is `"6"`, not `"Resolved"`. Every value below is the base-system
* value documented by ServiceNow. Instances with a customized state model or
* added choices can differ, so each tool also accepts a raw value.
*/
/**
* Incident `state` values.
*
* The state labels and their order come from the incident life cycle:
* https://www.servicenow.com/docs/bundle/australia-it-service-management/page/product/incident-management/concept/c_IncidentManagementStateModel.html
*
* That page names the states but publishes no coded values, and ServiceNow does
* not publish them anywhere else either the scripting examples at
* https://www.servicenow.com/docs/r/api-reference/scripts/r_UsefulFieldScripts.html
* compare `6` and `7` but against the legacy `incident_state` field and without
* naming either state. ServiceNow instead directs you to read the codes off your
* own instance (right-click the field label, "Show Choice List"):
* https://www.servicenow.com/docs/r/platform-administration/c_DetermValsAssocWChoicesScripting.html
*
* The values below are the long-standing base-system codes. Treat them as
* defaults rather than guarantees; every control that offers them also accepts a
* raw value so a customized state model stays reachable.
*/
export const INCIDENT_STATE = {
NEW: '1',
IN_PROGRESS: '2',
ON_HOLD: '3',
RESOLVED: '6',
CLOSED: '7',
CANCELED: '8',
} as const
export const INCIDENT_STATE_OPTIONS = [
{ label: 'New (1)', id: INCIDENT_STATE.NEW },
{ label: 'In Progress (2)', id: INCIDENT_STATE.IN_PROGRESS },
{ label: 'On Hold (3)', id: INCIDENT_STATE.ON_HOLD },
{ label: 'Resolved (6)', id: INCIDENT_STATE.RESOLVED },
{ label: 'Closed (7)', id: INCIDENT_STATE.CLOSED },
{ label: 'Canceled (8)', id: INCIDENT_STATE.CANCELED },
] as const
/** Incident and change `impact` / `urgency` values. */
export const IMPACT_URGENCY_OPTIONS = [
{ label: '1 - High', id: '1' },
{ label: '2 - Medium', id: '2' },
{ label: '3 - Low', id: '3' },
] as const
/** Task `priority` values. Priority is normally derived from impact and urgency. */
export const PRIORITY_OPTIONS = [
{ label: '1 - Critical', id: '1' },
{ label: '2 - High', id: '2' },
{ label: '3 - Moderate', id: '3' },
{ label: '4 - Low', id: '4' },
{ label: '5 - Planning', id: '5' },
] as const
/**
* Change request `state` values for the change model state machine.
* Source: https://www.servicenow.com/docs/bundle/australia-it-service-management/page/product/change-management/task/state-model-activate-tasks.html
*/
export const CHANGE_STATE = {
NEW: '-5',
ASSESS: '-4',
AUTHORIZE: '-3',
SCHEDULED: '-2',
IMPLEMENT: '-1',
REVIEW: '0',
CLOSED: '3',
CANCELED: '4',
} as const
export const CHANGE_STATE_OPTIONS = [
{ label: 'New (-5)', id: CHANGE_STATE.NEW },
{ label: 'Assess (-4)', id: CHANGE_STATE.ASSESS },
{ label: 'Authorize (-3)', id: CHANGE_STATE.AUTHORIZE },
{ label: 'Scheduled (-2)', id: CHANGE_STATE.SCHEDULED },
{ label: 'Implement (-1)', id: CHANGE_STATE.IMPLEMENT },
{ label: 'Review (0)', id: CHANGE_STATE.REVIEW },
{ label: 'Closed (3)', id: CHANGE_STATE.CLOSED },
{ label: 'Canceled (4)', id: CHANGE_STATE.CANCELED },
] as const
/** Change request `type` values. */
export const CHANGE_TYPE_OPTIONS = [
{ label: 'Normal', id: 'normal' },
{ label: 'Standard', id: 'standard' },
{ label: 'Emergency', id: 'emergency' },
] as const
/**
* Change request `close_code` values, as assigned by the state-model upgrade
* script in the change state model documentation.
* Source: https://www.servicenow.com/docs/bundle/australia-it-service-management/page/product/change-management/task/state-model-activate-tasks.html
*/
export const CHANGE_CLOSE_CODE_OPTIONS = [
{ label: 'Successful', id: 'successful' },
{ label: 'Successful with issues', id: 'successful_issues' },
{ label: 'Unsuccessful', id: 'unsuccessful' },
] as const
/**
* Approval record `state` values on the Approval [sysapproval_approver] table.
*
* ServiceNow documents four approval statuses Requested, Approved, Rejected,
* and Not Yet Requested at
* https://www.servicenow.com/docs/r/build-workflows/approvals/c_ApprovalStatus.html
* but publishes coded values for only the three a caller acts on, which are the
* three listed here. The fourth status has no published code, so it is not
* asserted; a caller who knows their instance's value can still pass it as a raw
* state.
*/
export const APPROVAL_STATE = {
REQUESTED: 'requested',
APPROVED: 'approved',
REJECTED: 'rejected',
} as const
export const APPROVAL_DECISION_OPTIONS = [
{ label: 'Approve', id: APPROVAL_STATE.APPROVED },
{ label: 'Reject', id: APPROVAL_STATE.REJECTED },
] as const
/**
* `sysparm_display_value` modes. The semantic tools default to `all` so a
* reference field returns both its sys_id and its human-readable label.
*/
export const DISPLAY_VALUE_OPTIONS = [
{ label: 'Both value and display value (all)', id: 'all' },
{ label: 'Display values only (true)', id: 'true' },
{ label: 'Raw database values only (false)', id: 'false' },
] as const
/** Default `sysparm_display_value` applied by every semantic ServiceNow tool. */
export const DEFAULT_DISPLAY_VALUE = 'all'
/** Journal field a comment is written to. */
export const COMMENT_FIELD_OPTIONS = [
{ label: 'Work notes (internal)', id: 'work_notes' },
{ label: 'Additional comments (customer visible)', id: 'comments' },
] as const
export const SERVICENOW_TABLES = {
INCIDENT: 'incident',
CHANGE_REQUEST: 'change_request',
CATALOG_ITEM: 'sc_cat_item',
REQUESTED_ITEM: 'sc_req_item',
APPROVAL: 'sysapproval_approver',
CI: 'cmdb_ci',
CI_RELATIONSHIP: 'cmdb_rel_ci',
USER: 'sys_user',
GROUP: 'sys_user_group',
GROUP_MEMBER: 'sys_user_grmember',
} as const
@@ -0,0 +1,180 @@
import { DEFAULT_DISPLAY_VALUE, SERVICENOW_TABLES } from '@/tools/servicenow/constants'
import {
additionalFieldsParam,
authParams,
recordOutputs,
writeParams,
} from '@/tools/servicenow/params'
import type {
ServiceNowCreateChangeParams,
ServiceNowSingleRecordResponse,
} from '@/tools/servicenow/types'
import {
appendWriteParams,
buildFieldPayload,
buildServiceNowHeaders,
normalizeInstanceUrl,
transformRecordResponse,
withQueryString,
} from '@/tools/servicenow/utils'
import type { ToolConfig } from '@/tools/types'
export const createChangeRequestTool: ToolConfig<
ServiceNowCreateChangeParams,
ServiceNowSingleRecordResponse
> = {
id: 'servicenow_create_change_request',
name: 'Create ServiceNow Change Request',
description:
'Create a change request in ServiceNow. Reference fields (assignment group, assigned to, requested by, configuration item) take sys_ids unless input display value is enabled.',
version: '1.0.0',
params: {
...authParams,
shortDescription: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Short description — the one-line summary of the change.',
},
description: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Detailed description of the change.',
},
type: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Change type: "normal", "standard", or "emergency".',
},
category: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Change category.',
},
risk: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Risk coded value. The choice list is configured per instance.',
},
impact: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Impact: 1 (High), 2 (Medium), or 3 (Low).',
},
priority: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Priority coded value 1-5 (1 Critical … 5 Planning).',
},
assignmentGroup: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Assignment group (assignment_group) — sys_id of the sys_user_group.',
},
assignedTo: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Assigned to (assigned_to) — sys_id of the sys_user.',
},
requestedBy: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Requested by (requested_by) — sys_id of the sys_user.',
},
cmdbCi: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Configuration item (cmdb_ci) — sys_id of the CI being changed.',
},
startDate: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Planned start date (start_date) as "YYYY-MM-DD HH:mm:ss" in UTC.',
},
endDate: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Planned end date (end_date) as "YYYY-MM-DD HH:mm:ss" in UTC.',
},
justification: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Justification for making the change.',
},
implementationPlan: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Implementation plan.',
},
backoutPlan: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Backout plan.',
},
testPlan: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Test plan.',
},
...additionalFieldsParam,
...writeParams,
},
request: {
url: (params) => {
const baseUrl = normalizeInstanceUrl(params.instanceUrl)
const searchParams = new URLSearchParams()
appendWriteParams(searchParams, { ...params, defaultDisplayValue: DEFAULT_DISPLAY_VALUE })
return withQueryString(
`${baseUrl}/api/now/table/${SERVICENOW_TABLES.CHANGE_REQUEST}`,
searchParams
)
},
method: 'POST',
headers: (params) => buildServiceNowHeaders(params, { json: true }),
body: (params) =>
buildFieldPayload(
{
short_description: params.shortDescription,
description: params.description,
type: params.type,
category: params.category,
risk: params.risk,
impact: params.impact,
priority: params.priority,
assignment_group: params.assignmentGroup,
assigned_to: params.assignedTo,
requested_by: params.requestedBy,
cmdb_ci: params.cmdbCi,
start_date: params.startDate,
end_date: params.endDate,
justification: params.justification,
implementation_plan: params.implementationPlan,
backout_plan: params.backoutPlan,
test_plan: params.testPlan,
},
params.additionalFields
),
},
transformResponse: transformRecordResponse,
outputs: recordOutputs,
}
@@ -0,0 +1,173 @@
import { DEFAULT_DISPLAY_VALUE, SERVICENOW_TABLES } from '@/tools/servicenow/constants'
import {
additionalFieldsParam,
authParams,
recordOutputs,
writeParams,
} from '@/tools/servicenow/params'
import type {
ServiceNowCreateIncidentParams,
ServiceNowSingleRecordResponse,
} from '@/tools/servicenow/types'
import {
appendWriteParams,
buildFieldPayload,
buildServiceNowHeaders,
normalizeInstanceUrl,
transformRecordResponse,
withQueryString,
} from '@/tools/servicenow/utils'
import type { ToolConfig } from '@/tools/types'
export const createIncidentTool: ToolConfig<
ServiceNowCreateIncidentParams,
ServiceNowSingleRecordResponse
> = {
id: 'servicenow_create_incident',
name: 'Create ServiceNow Incident',
description:
'Create an incident in ServiceNow. Reference fields (caller, assignment group, assigned to, configuration item) take sys_ids unless input display value is enabled.',
version: '1.0.0',
params: {
...authParams,
shortDescription: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Short description — the one-line summary of the incident.',
},
description: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Detailed description of the incident.',
},
callerId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Caller (caller_id) — sys_id of the sys_user who reported the incident. Use Find ServiceNow User to resolve an email address to a sys_id.',
},
category: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Category (e.g., inquiry, software, hardware, network, database).',
},
subcategory: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Subcategory, valid for the selected category.',
},
impact: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Impact: 1 (High), 2 (Medium), or 3 (Low).',
},
urgency: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Urgency: 1 (High), 2 (Medium), or 3 (Low).',
},
priority: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Priority 1-5 (1 Critical … 5 Planning). Normally derived from impact and urgency, so prefer setting those.',
},
state: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Incident state coded value. Base system: 1=New, 2=In Progress, 3=On Hold, 6=Resolved, 7=Closed, 8=Canceled. Defaults to New.',
},
assignmentGroup: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Assignment group (assignment_group) — sys_id of the sys_user_group.',
},
assignedTo: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Assigned to (assigned_to) — sys_id of the sys_user.',
},
cmdbCi: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Configuration item (cmdb_ci) — sys_id of the affected CI.',
},
businessService: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Service (business_service) — sys_id of the affected business service.',
},
contactType: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Channel (contact_type), e.g., email, phone, self-service, chat.',
},
workNotes: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Internal work note to record on creation.',
},
comments: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Customer-visible additional comment to record on creation.',
},
...additionalFieldsParam,
...writeParams,
},
request: {
url: (params) => {
const baseUrl = normalizeInstanceUrl(params.instanceUrl)
const searchParams = new URLSearchParams()
appendWriteParams(searchParams, { ...params, defaultDisplayValue: DEFAULT_DISPLAY_VALUE })
return withQueryString(`${baseUrl}/api/now/table/${SERVICENOW_TABLES.INCIDENT}`, searchParams)
},
method: 'POST',
headers: (params) => buildServiceNowHeaders(params, { json: true }),
body: (params) =>
buildFieldPayload(
{
short_description: params.shortDescription,
description: params.description,
caller_id: params.callerId,
category: params.category,
subcategory: params.subcategory,
impact: params.impact,
urgency: params.urgency,
priority: params.priority,
state: params.state,
assignment_group: params.assignmentGroup,
assigned_to: params.assignedTo,
cmdb_ci: params.cmdbCi,
business_service: params.businessService,
contact_type: params.contactType,
work_notes: params.workNotes,
comments: params.comments,
},
params.additionalFields
),
},
transformResponse: transformRecordResponse,
outputs: recordOutputs,
}
+3 -15
View File
@@ -1,6 +1,6 @@
import { createLogger } from '@sim/logger'
import type { ServiceNowCreateParams, ServiceNowCreateResponse } from '@/tools/servicenow/types'
import { createBasicAuthHeader } from '@/tools/servicenow/utils'
import { buildServiceNowHeaders, normalizeInstanceUrl } from '@/tools/servicenow/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('ServiceNowCreateRecordTool')
@@ -47,23 +47,11 @@ export const createRecordTool: ToolConfig<ServiceNowCreateParams, ServiceNowCrea
request: {
url: (params) => {
const baseUrl = params.instanceUrl.trim().replace(/\/$/, '')
if (!baseUrl) {
throw new Error('ServiceNow instance URL is required')
}
const baseUrl = normalizeInstanceUrl(params.instanceUrl)
return `${baseUrl}/api/now/table/${params.tableName.trim()}`
},
method: 'POST',
headers: (params) => {
if (!params.username || !params.password) {
throw new Error('ServiceNow username and password are required')
}
return {
Authorization: createBasicAuthHeader(params.username, params.password),
'Content-Type': 'application/json',
Accept: 'application/json',
}
},
headers: (params) => buildServiceNowHeaders(params, { json: true }),
body: (params) => {
if (!params.fields || typeof params.fields !== 'object') {
throw new Error('Fields must be a JSON object')
+3 -14
View File
@@ -1,6 +1,6 @@
import { createLogger } from '@sim/logger'
import type { ServiceNowDeleteParams, ServiceNowDeleteResponse } from '@/tools/servicenow/types'
import { createBasicAuthHeader } from '@/tools/servicenow/utils'
import { buildServiceNowHeaders, normalizeInstanceUrl } from '@/tools/servicenow/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('ServiceNowDeleteRecordTool')
@@ -46,22 +46,11 @@ export const deleteRecordTool: ToolConfig<ServiceNowDeleteParams, ServiceNowDele
request: {
url: (params) => {
const baseUrl = params.instanceUrl.trim().replace(/\/$/, '')
if (!baseUrl) {
throw new Error('ServiceNow instance URL is required')
}
const baseUrl = normalizeInstanceUrl(params.instanceUrl)
return `${baseUrl}/api/now/table/${params.tableName.trim()}/${params.sysId.trim()}`
},
method: 'DELETE',
headers: (params) => {
if (!params.username || !params.password) {
throw new Error('ServiceNow username and password are required')
}
return {
Authorization: createBasicAuthHeader(params.username, params.password),
Accept: 'application/json',
}
},
headers: (params) => buildServiceNowHeaders(params),
},
transformResponse: async (response: Response, params?: ServiceNowDeleteParams) => {
@@ -3,7 +3,7 @@ import type {
ServiceNowDownloadAttachmentParams,
ServiceNowDownloadAttachmentResponse,
} from '@/tools/servicenow/types'
import { createBasicAuthHeader } from '@/tools/servicenow/utils'
import { buildServiceNowHeaders, normalizeInstanceUrl } from '@/tools/servicenow/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('ServiceNowDownloadAttachmentTool')
@@ -46,22 +46,14 @@ export const downloadAttachmentTool: ToolConfig<
request: {
url: (params) => {
const baseUrl = params.instanceUrl.trim().replace(/\/$/, '')
if (!baseUrl) {
throw new Error('ServiceNow instance URL is required')
}
const baseUrl = normalizeInstanceUrl(params.instanceUrl)
return `${baseUrl}/api/now/attachment/${params.attachmentSysId.trim()}/file`
},
method: 'GET',
headers: (params) => {
if (!params.username || !params.password) {
throw new Error('ServiceNow username and password are required')
}
return {
Authorization: createBasicAuthHeader(params.username, params.password),
Accept: '*/*',
}
},
headers: (params) => ({
...buildServiceNowHeaders(params),
Accept: '*/*',
}),
},
transformResponse: async (response: Response) => {
+89
View File
@@ -0,0 +1,89 @@
import { DEFAULT_DISPLAY_VALUE, SERVICENOW_TABLES } from '@/tools/servicenow/constants'
import { authParams, listParams, recordListOutputs } from '@/tools/servicenow/params'
import type {
ServiceNowFindUserParams,
ServiceNowRecordListResponse,
} from '@/tools/servicenow/types'
import {
appendReadParams,
buildEncodedQuery,
buildServiceNowHeaders,
normalizeInstanceUrl,
transformRecordListResponse,
withQueryString,
} from '@/tools/servicenow/utils'
import type { ToolConfig } from '@/tools/types'
export const findUserTool: ToolConfig<ServiceNowFindUserParams, ServiceNowRecordListResponse> = {
id: 'servicenow_find_user',
name: 'Find ServiceNow User',
description:
'Look up ServiceNow users by email, user name, or display name. Use this to resolve the sys_id needed by reference fields such as caller_id, assigned_to, and approver.',
version: '1.0.0',
params: {
...authParams,
email: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Exact email address to match.',
},
userName: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Exact user name (user_name) to match.',
},
name: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Text to match against the display name using the ServiceNow LIKE operator, which matches anywhere in the field.',
},
active: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Restrict to active ("true") or inactive ("false") users.',
},
...listParams,
},
request: {
url: (params) => {
const baseUrl = normalizeInstanceUrl(params.instanceUrl)
const query = buildEncodedQuery(
[
['email', '=', params.email],
['user_name', '=', params.userName],
['name', 'LIKE', params.name],
['active', '=', params.active],
],
params.query
)
if (!query) {
throw new Error('At least one of email, userName, name, active, or query is required')
}
const searchParams = new URLSearchParams()
appendReadParams(searchParams, {
query,
limit: params.limit,
offset: params.offset,
fields: params.fields,
displayValue: params.displayValue,
defaultDisplayValue: DEFAULT_DISPLAY_VALUE,
})
return withQueryString(`${baseUrl}/api/now/table/${SERVICENOW_TABLES.USER}`, searchParams)
},
method: 'GET',
headers: (params) => buildServiceNowHeaders(params),
},
transformResponse: transformRecordListResponse,
outputs: recordListOutputs,
}
@@ -0,0 +1,161 @@
import { authParams } from '@/tools/servicenow/params'
import type {
ServiceNowGetChangeNextStatesParams,
ServiceNowGetChangeNextStatesResponse,
} from '@/tools/servicenow/types'
import {
buildServiceNowHeaders,
isRecord,
normalizeInstanceUrl,
parseServiceNowResponse,
toRecordObject,
} from '@/tools/servicenow/utils'
import type { ToolConfig } from '@/tools/types'
export const getChangeNextStatesTool: ToolConfig<
ServiceNowGetChangeNextStatesParams,
ServiceNowGetChangeNextStatesResponse
> = {
id: 'servicenow_get_change_next_states',
name: 'Get ServiceNow Change Next States',
description:
"Read the states a ServiceNow change request can actually move to next, with the instance's own state-to-label map and the conditions each transition still has to meet. Use this instead of assuming the base-system state codes, which a customized change model can change.",
version: '1.0.0',
params: {
...authParams,
changeSysId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'sys_id of the change request. Use Get ServiceNow Change Request to resolve a CHG number to its sys_id.',
},
},
request: {
url: (params) => {
const baseUrl = normalizeInstanceUrl(params.instanceUrl)
const changeSysId = params.changeSysId?.trim()
if (!changeSysId) {
throw new Error('A change request sys_id is required')
}
return `${baseUrl}/api/sn_chg_rest/change/${changeSysId}/nextstates`
},
method: 'GET',
headers: (params) => buildServiceNowHeaders(params),
},
transformResponse: async (response: Response) => {
const data = await parseServiceNowResponse(response)
const result = toRecordObject(data.result)
const availableStates: string[] = Array.isArray(result.available_states)
? result.available_states.map((state: unknown) => String(state))
: []
const stateLabels: Record<string, string> = {}
for (const [state, label] of Object.entries(toRecordObject(result.state_label))) {
if (typeof label === 'string') stateLabels[state] = label
}
/**
* ServiceNow groups `state_transitions` by target state, so it arrives as an
* array of arrays. Each entry already carries its own `from_state` and
* `to_state`, so flattening loses nothing and is far easier to consume.
*/
const stateTransitions = (
Array.isArray(result.state_transitions) ? result.state_transitions.flat() : []
).filter(isRecord)
const allowedStates = [
...new Set(
stateTransitions
.filter((transition) => transition?.transition_available === true)
.map((transition) => String(transition.to_state))
),
]
return {
success: true as const,
output: {
availableStates,
allowedStates,
stateLabels,
stateTransitions,
metadata: { transitionCount: stateTransitions.length },
},
}
},
outputs: {
availableStates: {
type: 'array',
description:
'Every state coded value reachable from the change request, including its current state',
items: { type: 'string', description: 'State coded value' },
},
allowedStates: {
type: 'array',
description:
'The subset of state coded values whose transition currently reports transition_available, meaning the change request already meets that transition conditions',
items: { type: 'string', description: 'State coded value' },
},
stateLabels: {
type: 'json',
description:
"Map of state coded value to the label this instance uses, e.g. {'0': 'Review'}. Read the state model from here rather than assuming the base-system codes",
},
stateTransitions: {
type: 'array',
description:
'Available transitions, flattened from the per-target-state grouping ServiceNow returns. Empty for type-driven and legacy change requests, which do not report conditions',
items: {
type: 'object',
properties: {
sys_id: {
type: 'string',
description: 'Sys_id of the state transition record on sttrm_state_transition',
},
display_value: {
type: 'string',
description: 'Human-readable transition name, e.g. "Implement to Review"',
},
from_state: { type: 'string', description: 'State coded value moved from' },
to_state: { type: 'string', description: 'State coded value moved to' },
transition_available: {
type: 'boolean',
description: 'Whether the change request can move to this state right now',
},
automatic_transition: {
type: 'boolean',
description: 'Whether the change request moves to this state automatically',
},
conditions: {
type: 'array',
description: 'Conditions gating the transition, each with whether it has passed',
items: {
type: 'object',
properties: {
passed: {
type: 'boolean',
description: 'Whether the change request met this condition',
},
condition: {
type: 'json',
description: 'The condition as {name, description, sys_id}',
},
},
},
},
},
},
},
metadata: {
type: 'json',
description: 'Operation metadata',
properties: {
transitionCount: { type: 'number', description: 'Number of transitions returned' },
},
},
},
}
@@ -0,0 +1,63 @@
import { DEFAULT_DISPLAY_VALUE, SERVICENOW_TABLES } from '@/tools/servicenow/constants'
import {
authParams,
displayValueParam,
fieldsParam,
recordIdentifierParams,
recordOutputs,
} from '@/tools/servicenow/params'
import type {
ServiceNowGetChangeParams,
ServiceNowSingleRecordResponse,
} from '@/tools/servicenow/types'
import {
appendReadParams,
buildIdentifierQuery,
buildServiceNowHeaders,
normalizeInstanceUrl,
transformRecordResponse,
withQueryString,
} from '@/tools/servicenow/utils'
import type { ToolConfig } from '@/tools/types'
export const getChangeRequestTool: ToolConfig<
ServiceNowGetChangeParams,
ServiceNowSingleRecordResponse
> = {
id: 'servicenow_get_change_request',
name: 'Get ServiceNow Change Request',
description:
'Retrieve a single ServiceNow change request by number (e.g., CHG0030001) or sys_id. Reference fields are returned with both their sys_id and their label by default.',
version: '1.0.0',
params: {
...authParams,
...recordIdentifierParams,
...fieldsParam,
...displayValueParam,
},
request: {
url: (params) => {
const baseUrl = normalizeInstanceUrl(params.instanceUrl)
const searchParams = new URLSearchParams()
appendReadParams(searchParams, {
query: buildIdentifierQuery(params),
limit: 1,
fields: params.fields,
displayValue: params.displayValue,
defaultDisplayValue: DEFAULT_DISPLAY_VALUE,
})
return withQueryString(
`${baseUrl}/api/now/table/${SERVICENOW_TABLES.CHANGE_REQUEST}`,
searchParams
)
},
method: 'GET',
headers: (params) => buildServiceNowHeaders(params),
},
transformResponse: transformRecordResponse,
outputs: recordOutputs,
}
+128
View File
@@ -0,0 +1,128 @@
import { authParams } from '@/tools/servicenow/params'
import type { ServiceNowGetCiParams, ServiceNowGetCiResponse } from '@/tools/servicenow/types'
import {
buildServiceNowHeaders,
normalizeInstanceUrl,
parseServiceNowResponse,
readRecord,
readRecordArray,
toRecordObject,
} from '@/tools/servicenow/utils'
import type { ToolConfig } from '@/tools/types'
export const getCiTool: ToolConfig<ServiceNowGetCiParams, ServiceNowGetCiResponse> = {
id: 'servicenow_get_ci',
name: 'Get ServiceNow Configuration Item',
description:
'Retrieve a configuration item and its CMDB relationships through the CMDB Instance API. Returns the CI attributes plus its inbound and outbound relations, each carrying the related CI and the relationship type.',
version: '1.0.0',
params: {
...authParams,
ciClass: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'CMDB class (table) the CI belongs to, e.g., cmdb_ci_linux_server. Read it from the sys_class_name field returned by Search ServiceNow Configuration Items.',
},
sysId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'sys_id of the configuration item to retrieve.',
},
},
request: {
url: (params) => {
const baseUrl = normalizeInstanceUrl(params.instanceUrl)
const ciClass = params.ciClass?.trim()
const sysId = params.sysId?.trim()
if (!ciClass) throw new Error('A CMDB class name is required')
if (!sysId) throw new Error('A configuration item sys_id is required')
return `${baseUrl}/api/now/cmdb/instance/${ciClass}/${sysId}`
},
method: 'GET',
headers: (params) => buildServiceNowHeaders(params),
},
transformResponse: async (response: Response) => {
const data = await parseServiceNowResponse(response)
const result = toRecordObject(data.result)
const inboundRelations = readRecordArray(result, 'inbound_relations')
const outboundRelations = readRecordArray(result, 'outbound_relations')
return {
success: true,
output: {
attributes: readRecord(result, 'attributes'),
inboundRelations,
outboundRelations,
metadata: {
inboundCount: inboundRelations.length,
outboundCount: outboundRelations.length,
},
},
}
},
outputs: {
attributes: {
type: 'json',
description:
'Data attributes on the CI record. The available attributes depend on the class.',
nullable: true,
},
inboundRelations: {
type: 'array',
description: 'Inbound CI relationships',
items: {
type: 'object',
properties: {
sys_id: {
type: 'string',
description: 'Sys_id of the relationship record on cmdb_rel_ci',
},
target: {
type: 'json',
description: 'Related CI as {value (sys_id), display_value, link}',
},
type: {
type: 'json',
description: 'Relationship type as {value (sys_id), display_value, link}',
},
},
},
},
outboundRelations: {
type: 'array',
description: 'Outbound CI relationships',
items: {
type: 'object',
properties: {
sys_id: {
type: 'string',
description: 'Sys_id of the relationship record on cmdb_rel_ci',
},
target: {
type: 'json',
description: 'Related CI as {value (sys_id), display_value, link}',
},
type: {
type: 'json',
description: 'Relationship type as {value (sys_id), display_value, link}',
},
},
},
},
metadata: {
type: 'json',
description: 'Operation metadata',
properties: {
inboundCount: { type: 'number', description: 'Number of inbound relations' },
outboundCount: { type: 'number', description: 'Number of outbound relations' },
},
},
},
}
+60
View File
@@ -0,0 +1,60 @@
import { DEFAULT_DISPLAY_VALUE, SERVICENOW_TABLES } from '@/tools/servicenow/constants'
import {
authParams,
displayValueParam,
fieldsParam,
recordIdentifierParams,
recordOutputs,
} from '@/tools/servicenow/params'
import type {
ServiceNowGetIncidentParams,
ServiceNowSingleRecordResponse,
} from '@/tools/servicenow/types'
import {
appendReadParams,
buildIdentifierQuery,
buildServiceNowHeaders,
normalizeInstanceUrl,
transformRecordResponse,
withQueryString,
} from '@/tools/servicenow/utils'
import type { ToolConfig } from '@/tools/types'
export const getIncidentTool: ToolConfig<
ServiceNowGetIncidentParams,
ServiceNowSingleRecordResponse
> = {
id: 'servicenow_get_incident',
name: 'Get ServiceNow Incident',
description:
'Retrieve a single ServiceNow incident by number (e.g., INC0010001) or sys_id. Reference fields are returned with both their sys_id and their label by default.',
version: '1.0.0',
params: {
...authParams,
...recordIdentifierParams,
...fieldsParam,
...displayValueParam,
},
request: {
url: (params) => {
const baseUrl = normalizeInstanceUrl(params.instanceUrl)
const searchParams = new URLSearchParams()
appendReadParams(searchParams, {
query: buildIdentifierQuery(params),
limit: 1,
fields: params.fields,
displayValue: params.displayValue,
defaultDisplayValue: DEFAULT_DISPLAY_VALUE,
})
return withQueryString(`${baseUrl}/api/now/table/${SERVICENOW_TABLES.INCIDENT}`, searchParams)
},
method: 'GET',
headers: (params) => buildServiceNowHeaders(params),
},
transformResponse: transformRecordResponse,
outputs: recordOutputs,
}
@@ -0,0 +1,133 @@
import { authParams } from '@/tools/servicenow/params'
import type {
ServiceNowGetKnowledgeArticleParams,
ServiceNowGetKnowledgeArticleResponse,
} from '@/tools/servicenow/types'
import {
buildServiceNowHeaders,
normalizeInstanceUrl,
parseServiceNowResponse,
readRecord,
readRecordArray,
readString,
toRecordObject,
withQueryString,
} from '@/tools/servicenow/utils'
import type { ToolConfig } from '@/tools/types'
export const getKnowledgeArticleTool: ToolConfig<
ServiceNowGetKnowledgeArticleParams,
ServiceNowGetKnowledgeArticleResponse
> = {
id: 'servicenow_get_knowledge_article',
name: 'Get ServiceNow Knowledge Article',
description:
'Retrieve the full content of a ServiceNow knowledge article by sys_id or KB number through the Knowledge Management API.',
version: '1.0.0',
params: {
...authParams,
articleId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'sys_id or KB number of the knowledge article (e.g., KB0000011).',
},
fields: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Comma-separated kb_knowledge fields to include alongside the article content (e.g., workflow_state,author).',
},
language: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Two-letter ISO 639-1 language code. Only applies when the article is addressed by KB number and a translation exists.',
},
updateView: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description:
'Set to true to increment the article view count and record an entry in the Knowledge Use [kb_use] table.',
},
},
request: {
url: (params) => {
const baseUrl = normalizeInstanceUrl(params.instanceUrl)
const articleId = params.articleId?.trim()
if (!articleId) {
throw new Error('A knowledge article sys_id or KB number is required')
}
const searchParams = new URLSearchParams()
if (params.fields) searchParams.append('fields', params.fields)
if (params.language) searchParams.append('language', params.language)
if (params.updateView === true || params.updateView === 'true') {
searchParams.append('update_view', 'true')
}
return withQueryString(
`${baseUrl}/api/sn_km_api/knowledge/articles/${encodeURIComponent(articleId)}`,
searchParams
)
},
method: 'GET',
headers: (params) => buildServiceNowHeaders(params),
},
transformResponse: async (response: Response) => {
const data = await parseServiceNowResponse(response)
const result = toRecordObject(data.result)
return {
success: true,
output: {
sysId: readString(result, 'sys_id'),
number: readString(result, 'number'),
title: readString(result, 'short_description'),
content: readString(result, 'content'),
fields: readRecord(result, 'fields'),
attachments: readRecordArray(result, 'attachments'),
},
}
},
outputs: {
sysId: { type: 'string', description: 'Article sys_id on kb_knowledge', nullable: true },
number: { type: 'string', description: 'Knowledge article number', nullable: true },
title: {
type: 'string',
description: 'Article title (short description)',
nullable: true,
},
content: { type: 'string', description: 'Full HTML content of the article', nullable: true },
fields: {
type: 'json',
description: 'Requested kb_knowledge fields, each {name, label, type, value, display_value}',
nullable: true,
},
attachments: {
type: 'array',
description:
'Article attachments, returned only when display_attachments is active on the article',
items: {
type: 'object',
properties: {
sys_id: { type: 'string', description: 'Attachment sys_id' },
file_name: { type: 'string', description: 'Attachment file name' },
size_bytes: { type: 'string', description: 'Attachment size in bytes' },
state: {
type: 'string',
description:
'Attachment state: available, available_conditionally, not_available, or pending',
},
},
},
},
},
}
@@ -0,0 +1,63 @@
import { DEFAULT_DISPLAY_VALUE, SERVICENOW_TABLES } from '@/tools/servicenow/constants'
import {
authParams,
displayValueParam,
fieldsParam,
recordIdentifierParams,
recordOutputs,
} from '@/tools/servicenow/params'
import type {
ServiceNowGetRequestedItemParams,
ServiceNowSingleRecordResponse,
} from '@/tools/servicenow/types'
import {
appendReadParams,
buildIdentifierQuery,
buildServiceNowHeaders,
normalizeInstanceUrl,
transformRecordResponse,
withQueryString,
} from '@/tools/servicenow/utils'
import type { ToolConfig } from '@/tools/types'
export const getRequestedItemTool: ToolConfig<
ServiceNowGetRequestedItemParams,
ServiceNowSingleRecordResponse
> = {
id: 'servicenow_get_requested_item',
name: 'Get ServiceNow Requested Item',
description:
'Retrieve a single ServiceNow requested item (RITM) by number (e.g., RITM0010001) or sys_id from the Requested Item [sc_req_item] table.',
version: '1.0.0',
params: {
...authParams,
...recordIdentifierParams,
...fieldsParam,
...displayValueParam,
},
request: {
url: (params) => {
const baseUrl = normalizeInstanceUrl(params.instanceUrl)
const searchParams = new URLSearchParams()
appendReadParams(searchParams, {
query: buildIdentifierQuery(params),
limit: 1,
fields: params.fields,
displayValue: params.displayValue,
defaultDisplayValue: DEFAULT_DISPLAY_VALUE,
})
return withQueryString(
`${baseUrl}/api/now/table/${SERVICENOW_TABLES.REQUESTED_ITEM}`,
searchParams
)
},
method: 'GET',
headers: (params) => buildServiceNowHeaders(params),
},
transformResponse: transformRecordResponse,
outputs: recordOutputs,
}
+54
View File
@@ -1,9 +1,36 @@
import { addIncidentCommentTool } from '@/tools/servicenow/add_incident_comment'
import { aggregateTool } from '@/tools/servicenow/aggregate'
import { closeIncidentTool } from '@/tools/servicenow/close_incident'
import { createChangeRequestTool } from '@/tools/servicenow/create_change_request'
import { createIncidentTool } from '@/tools/servicenow/create_incident'
import { createRecordTool } from '@/tools/servicenow/create_record'
import { deleteRecordTool } from '@/tools/servicenow/delete_record'
import { downloadAttachmentTool } from '@/tools/servicenow/download_attachment'
import { findUserTool } from '@/tools/servicenow/find_user'
import { getChangeNextStatesTool } from '@/tools/servicenow/get_change_next_states'
import { getChangeRequestTool } from '@/tools/servicenow/get_change_request'
import { getCiTool } from '@/tools/servicenow/get_ci'
import { getIncidentTool } from '@/tools/servicenow/get_incident'
import { getKnowledgeArticleTool } from '@/tools/servicenow/get_knowledge_article'
import { getRequestedItemTool } from '@/tools/servicenow/get_requested_item'
import { listApprovalsTool } from '@/tools/servicenow/list_approvals'
import { listAttachmentsTool } from '@/tools/servicenow/list_attachments'
import { listCatalogItemsTool } from '@/tools/servicenow/list_catalog_items'
import { listChangeRequestsTool } from '@/tools/servicenow/list_change_requests'
import { listChangeTasksTool } from '@/tools/servicenow/list_change_tasks'
import { listCiRelationshipsTool } from '@/tools/servicenow/list_ci_relationships'
import { listGroupMembersTool } from '@/tools/servicenow/list_group_members'
import { listIncidentsTool } from '@/tools/servicenow/list_incidents'
import { listRequestedItemsTool } from '@/tools/servicenow/list_requested_items'
import { orderCatalogItemTool } from '@/tools/servicenow/order_catalog_item'
import { readRecordTool } from '@/tools/servicenow/read_record'
import { resolveIncidentTool } from '@/tools/servicenow/resolve_incident'
import { searchCisTool } from '@/tools/servicenow/search_cis'
import { searchKnowledgeTool } from '@/tools/servicenow/search_knowledge'
import { updateApprovalTool } from '@/tools/servicenow/update_approval'
import { updateChangeRequestTool } from '@/tools/servicenow/update_change_request'
import { updateChangeStateTool } from '@/tools/servicenow/update_change_state'
import { updateIncidentTool } from '@/tools/servicenow/update_incident'
import { updateRecordTool } from '@/tools/servicenow/update_record'
import { uploadAttachmentTool } from '@/tools/servicenow/upload_attachment'
@@ -16,4 +43,31 @@ export {
listAttachmentsTool as servicenowListAttachmentsTool,
downloadAttachmentTool as servicenowDownloadAttachmentTool,
uploadAttachmentTool as servicenowUploadAttachmentTool,
createIncidentTool as servicenowCreateIncidentTool,
getIncidentTool as servicenowGetIncidentTool,
listIncidentsTool as servicenowListIncidentsTool,
updateIncidentTool as servicenowUpdateIncidentTool,
resolveIncidentTool as servicenowResolveIncidentTool,
closeIncidentTool as servicenowCloseIncidentTool,
addIncidentCommentTool as servicenowAddIncidentCommentTool,
createChangeRequestTool as servicenowCreateChangeRequestTool,
getChangeRequestTool as servicenowGetChangeRequestTool,
listChangeRequestsTool as servicenowListChangeRequestsTool,
updateChangeRequestTool as servicenowUpdateChangeRequestTool,
updateChangeStateTool as servicenowUpdateChangeStateTool,
listChangeTasksTool as servicenowListChangeTasksTool,
getChangeNextStatesTool as servicenowGetChangeNextStatesTool,
listCatalogItemsTool as servicenowListCatalogItemsTool,
orderCatalogItemTool as servicenowOrderCatalogItemTool,
listRequestedItemsTool as servicenowListRequestedItemsTool,
getRequestedItemTool as servicenowGetRequestedItemTool,
listApprovalsTool as servicenowListApprovalsTool,
updateApprovalTool as servicenowUpdateApprovalTool,
searchCisTool as servicenowSearchCisTool,
getCiTool as servicenowGetCiTool,
listCiRelationshipsTool as servicenowListCiRelationshipsTool,
searchKnowledgeTool as servicenowSearchKnowledgeTool,
getKnowledgeArticleTool as servicenowGetKnowledgeArticleTool,
findUserTool as servicenowFindUserTool,
listGroupMembersTool as servicenowListGroupMembersTool,
}
@@ -0,0 +1,86 @@
import {
APPROVAL_STATE,
DEFAULT_DISPLAY_VALUE,
SERVICENOW_TABLES,
} from '@/tools/servicenow/constants'
import { authParams, listParams, recordListOutputs } from '@/tools/servicenow/params'
import type {
ServiceNowListApprovalsParams,
ServiceNowRecordListResponse,
} from '@/tools/servicenow/types'
import {
appendReadParams,
buildEncodedQuery,
buildServiceNowHeaders,
normalizeInstanceUrl,
transformRecordListResponse,
withQueryString,
} from '@/tools/servicenow/utils'
import type { ToolConfig } from '@/tools/types'
export const listApprovalsTool: ToolConfig<
ServiceNowListApprovalsParams,
ServiceNowRecordListResponse
> = {
id: 'servicenow_list_approvals',
name: 'List ServiceNow Approvals',
description: `List approval records from the ServiceNow Approval [sysapproval_approver] table. Defaults to the "requested" state, which is what a user's pending approvals look like.`,
version: '1.0.0',
params: {
...authParams,
approverSysId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'sys_id of the approver (sys_user) whose approvals should be listed. Use Find ServiceNow User to resolve an email address to a sys_id.',
},
state: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Approval state: "requested" (pending, the default), "approved", or "rejected". Pass an empty string with a custom query to list every state.',
},
approvalFor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'sys_id of the record being approved, matched against the sysapproval reference field.',
},
...listParams,
},
request: {
url: (params) => {
const baseUrl = normalizeInstanceUrl(params.instanceUrl)
const searchParams = new URLSearchParams()
const state = params.state === undefined ? APPROVAL_STATE.REQUESTED : params.state
appendReadParams(searchParams, {
query: buildEncodedQuery(
[
['approver', '=', params.approverSysId],
['state', '=', state],
['sysapproval', '=', params.approvalFor],
],
params.query
),
limit: params.limit,
offset: params.offset,
fields: params.fields,
displayValue: params.displayValue,
defaultDisplayValue: DEFAULT_DISPLAY_VALUE,
})
return withQueryString(`${baseUrl}/api/now/table/${SERVICENOW_TABLES.APPROVAL}`, searchParams)
},
method: 'GET',
headers: (params) => buildServiceNowHeaders(params),
},
transformResponse: transformRecordListResponse,
outputs: recordListOutputs,
}
+3 -14
View File
@@ -3,7 +3,7 @@ import type {
ServiceNowListAttachmentsParams,
ServiceNowListAttachmentsResponse,
} from '@/tools/servicenow/types'
import { createBasicAuthHeader } from '@/tools/servicenow/utils'
import { buildServiceNowHeaders, normalizeInstanceUrl } from '@/tools/servicenow/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('ServiceNowListAttachmentsTool')
@@ -58,10 +58,7 @@ export const listAttachmentsTool: ToolConfig<
request: {
url: (params) => {
const baseUrl = params.instanceUrl.trim().replace(/\/$/, '')
if (!baseUrl) {
throw new Error('ServiceNow instance URL is required')
}
const baseUrl = normalizeInstanceUrl(params.instanceUrl)
const queryParams = new URLSearchParams()
queryParams.append(
@@ -75,15 +72,7 @@ export const listAttachmentsTool: ToolConfig<
return `${baseUrl}/api/now/attachment?${queryParams.toString()}`
},
method: 'GET',
headers: (params) => {
if (!params.username || !params.password) {
throw new Error('ServiceNow username and password are required')
}
return {
Authorization: createBasicAuthHeader(params.username, params.password),
Accept: 'application/json',
}
},
headers: (params) => buildServiceNowHeaders(params),
},
transformResponse: async (response: Response) => {
@@ -0,0 +1,162 @@
import { authParams } from '@/tools/servicenow/params'
import type {
ServiceNowCatalogItem,
ServiceNowListCatalogItemsParams,
ServiceNowListCatalogItemsResponse,
} from '@/tools/servicenow/types'
import {
buildServiceNowHeaders,
normalizeInstanceUrl,
parseServiceNowResponse,
toRecordArray,
withQueryString,
} from '@/tools/servicenow/utils'
import type { ToolConfig } from '@/tools/types'
export const listCatalogItemsTool: ToolConfig<
ServiceNowListCatalogItemsParams,
ServiceNowListCatalogItemsResponse
> = {
id: 'servicenow_list_catalog_items',
name: 'List ServiceNow Catalog Items',
description:
'Browse or search the ServiceNow service catalog. Returns each item with its sys_id, name, description, type, category, and catalogs, which is what Order ServiceNow Catalog Item needs.',
version: '1.0.0',
params: {
...authParams,
searchText: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Text to search for in the catalog items (e.g., "iPhone").',
},
catalogSysId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Restrict results to a specific catalog, by catalog sys_id.',
},
categorySysId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Restrict results to a specific category, by category sys_id.',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of items to return (sysparm_limit).',
},
offset: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of items to skip for pagination (sysparm_offset).',
},
},
request: {
url: (params) => {
const baseUrl = normalizeInstanceUrl(params.instanceUrl)
const searchParams = new URLSearchParams()
if (params.searchText) searchParams.append('sysparm_text', params.searchText)
if (params.catalogSysId) searchParams.append('sysparm_catalog', params.catalogSysId)
if (params.categorySysId) searchParams.append('sysparm_category', params.categorySysId)
if (params.limit !== undefined && params.limit !== null) {
searchParams.append('sysparm_limit', String(params.limit))
}
if (params.offset !== undefined && params.offset !== null) {
searchParams.append('sysparm_offset', String(params.offset))
}
return withQueryString(`${baseUrl}/api/sn_sc/servicecatalog/items`, searchParams)
},
method: 'GET',
headers: (params) => buildServiceNowHeaders(params),
},
transformResponse: async (response: Response) => {
const data = await parseServiceNowResponse(response)
const items = toRecordArray(data.result) as ServiceNowCatalogItem[]
return {
success: true,
output: {
items,
metadata: { recordCount: items.length },
},
}
},
outputs: {
items: {
type: 'array',
description: 'Catalog items',
items: {
type: 'object',
properties: {
sys_id: { type: 'string', description: 'Catalog item sys_id, used to order the item' },
name: { type: 'string', description: 'Catalog item name' },
short_description: {
type: 'string',
description: 'Short description',
nullable: true,
},
description: { type: 'string', description: 'HTML description', optional: true },
type: {
type: 'string',
description: 'Item type, e.g., record_producer or catalog_item',
optional: true,
},
sys_class_name: { type: 'string', description: 'Item table', optional: true },
category: {
type: 'json',
description: 'Category {sys_id, title}',
optional: true,
nullable: true,
},
catalogs: {
type: 'json',
description: 'Catalogs the item belongs to, each {sys_id, title}',
optional: true,
},
picture: { type: 'string', description: 'Item picture reference', optional: true },
icon: { type: 'string', description: 'Item icon reference', optional: true },
order: { type: 'number', description: 'Display order', optional: true },
price: { type: 'string', description: 'Item price', optional: true },
show_price: {
type: 'boolean',
description: 'Whether the price is shown',
optional: true,
},
show_quantity: {
type: 'boolean',
description: 'Whether a quantity can be chosen when ordering',
optional: true,
},
content_type: {
type: 'string',
description: 'Content type for content items',
optional: true,
},
url: { type: 'string', description: 'Target URL for content items', optional: true },
kb_article: {
type: 'string',
description: 'sys_id of the knowledge article backing the item',
optional: true,
},
},
},
},
metadata: {
type: 'json',
description: 'Operation metadata',
properties: {
recordCount: { type: 'number', description: 'Number of catalog items returned' },
},
},
},
}
@@ -0,0 +1,111 @@
import { DEFAULT_DISPLAY_VALUE, SERVICENOW_TABLES } from '@/tools/servicenow/constants'
import { authParams, listParams, recordListOutputs } from '@/tools/servicenow/params'
import type {
ServiceNowListChangesParams,
ServiceNowRecordListResponse,
} from '@/tools/servicenow/types'
import {
appendReadParams,
buildEncodedQuery,
buildServiceNowHeaders,
normalizeInstanceUrl,
transformRecordListResponse,
withQueryString,
} from '@/tools/servicenow/utils'
import type { ToolConfig } from '@/tools/types'
export const listChangeRequestsTool: ToolConfig<
ServiceNowListChangesParams,
ServiceNowRecordListResponse
> = {
id: 'servicenow_list_change_requests',
name: 'List ServiceNow Change Requests',
description:
'Search ServiceNow change requests by state, type, risk, assignment, or text. All filters are ANDed together.',
version: '1.0.0',
params: {
...authParams,
searchText: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Text to match against the change short description using the ServiceNow LIKE operator, which matches anywhere in the field.',
},
state: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Change state coded value. Base system: -5=New, -4=Assess, -3=Authorize, -2=Scheduled, -1=Implement, 0=Review, 3=Closed, 4=Canceled.',
},
type: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Change type: "normal", "standard", or "emergency".',
},
risk: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Risk coded value. The choice list is configured per instance.',
},
assignmentGroup: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'sys_id of the assignment group.',
},
assignedTo: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'sys_id of the assigned user.',
},
active: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Restrict to active ("true") or inactive ("false") change requests.',
},
...listParams,
},
request: {
url: (params) => {
const baseUrl = normalizeInstanceUrl(params.instanceUrl)
const searchParams = new URLSearchParams()
appendReadParams(searchParams, {
query: buildEncodedQuery(
[
['state', '=', params.state],
['type', '=', params.type],
['risk', '=', params.risk],
['assignment_group', '=', params.assignmentGroup],
['assigned_to', '=', params.assignedTo],
['active', '=', params.active],
['short_description', 'LIKE', params.searchText],
],
params.query
),
limit: params.limit,
offset: params.offset,
fields: params.fields,
displayValue: params.displayValue,
defaultDisplayValue: DEFAULT_DISPLAY_VALUE,
})
return withQueryString(
`${baseUrl}/api/now/table/${SERVICENOW_TABLES.CHANGE_REQUEST}`,
searchParams
)
},
method: 'GET',
headers: (params) => buildServiceNowHeaders(params),
},
transformResponse: transformRecordListResponse,
outputs: recordListOutputs,
}
@@ -0,0 +1,111 @@
import { authParams } from '@/tools/servicenow/params'
import type {
ServiceNowChangeTaskListResponse,
ServiceNowListChangeTasksParams,
} from '@/tools/servicenow/types'
import {
buildServiceNowHeaders,
normalizeInstanceUrl,
parseServiceNowResponse,
toRecordArray,
withQueryString,
} from '@/tools/servicenow/utils'
import type { ToolConfig } from '@/tools/types'
export const listChangeTasksTool: ToolConfig<
ServiceNowListChangeTasksParams,
ServiceNowChangeTaskListResponse
> = {
id: 'servicenow_list_change_tasks',
name: 'List ServiceNow Change Tasks',
description:
'List the change tasks belonging to a ServiceNow change request, via the Change Management API. Every field is returned as {value, display_value}, so a reference field carries both its sys_id and its label. This endpoint is not the Table API: the shape is fixed with no display-value option, and the results come back under `tasks` rather than the `records` the other list operations use.',
version: '1.0.0',
params: {
...authParams,
changeSysId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'sys_id of the change request whose tasks should be listed.',
},
query: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'ServiceNow encoded query used to filter the tasks (e.g., "active=true^ORDERBYnumber").',
},
order: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Field to sort the returned tasks by.',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of tasks to return (sysparm_limit). ServiceNow defaults to 500.',
},
offset: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of tasks to skip for pagination (sysparm_offset).',
},
},
request: {
url: (params) => {
const baseUrl = normalizeInstanceUrl(params.instanceUrl)
const changeSysId = params.changeSysId?.trim()
if (!changeSysId) {
throw new Error('A change request sys_id is required')
}
const searchParams = new URLSearchParams()
if (params.query) searchParams.append('sysparm_query', params.query)
if (params.order) searchParams.append('order', params.order)
if (params.limit !== undefined && params.limit !== null) {
searchParams.append('sysparm_limit', String(params.limit))
}
if (params.offset !== undefined && params.offset !== null) {
searchParams.append('sysparm_offset', String(params.offset))
}
return withQueryString(`${baseUrl}/api/sn_chg_rest/change/${changeSysId}/task`, searchParams)
},
method: 'GET',
headers: (params) => buildServiceNowHeaders(params),
},
transformResponse: async (response: Response) => {
const data = await parseServiceNowResponse(response)
const tasks = toRecordArray(data.result)
return {
success: true,
output: {
tasks,
metadata: { recordCount: tasks.length },
},
}
},
outputs: {
tasks: {
type: 'array',
description:
'Change tasks, under `tasks` rather than the `records` key the Table API list operations use. Each field is an object of the form {value, display_value} — the Change Management API fixes this shape, so unlike those operations there is no display-value setting. `parent` holds the owning change request.',
},
metadata: {
type: 'json',
description: 'Operation metadata',
properties: {
recordCount: { type: 'number', description: 'Number of change tasks returned' },
},
},
},
}
@@ -0,0 +1,84 @@
import { DEFAULT_DISPLAY_VALUE, SERVICENOW_TABLES } from '@/tools/servicenow/constants'
import { authParams, listParams, recordListOutputs } from '@/tools/servicenow/params'
import type {
ServiceNowListCiRelationshipsParams,
ServiceNowRecordListResponse,
} from '@/tools/servicenow/types'
import {
appendReadParams,
buildEncodedQuery,
buildServiceNowHeaders,
normalizeInstanceUrl,
transformRecordListResponse,
withQueryString,
} from '@/tools/servicenow/utils'
import type { ToolConfig } from '@/tools/types'
export const listCiRelationshipsTool: ToolConfig<
ServiceNowListCiRelationshipsParams,
ServiceNowRecordListResponse
> = {
id: 'servicenow_list_ci_relationships',
name: 'List ServiceNow CI Relationships',
description:
'List rows from the CI Relationship [cmdb_rel_ci] table for a configuration item. Each row carries the parent CI, the child CI, and the relationship type.',
version: '1.0.0',
params: {
...authParams,
ciSysId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'sys_id of the configuration item whose relationships should be listed.',
},
direction: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Which side of the relationship the CI sits on: "parent", "child", or "both" (default). "both" matches rows where the CI is either the parent or the child.',
},
...listParams,
},
request: {
url: (params) => {
const baseUrl = normalizeInstanceUrl(params.instanceUrl)
const ciSysId = params.ciSysId?.trim()
if (!ciSysId) {
throw new Error('A configuration item sys_id is required')
}
const direction = params.direction?.trim().toLowerCase() || 'both'
let directionQuery: string
if (direction === 'parent') {
directionQuery = `parent=${ciSysId}`
} else if (direction === 'child') {
directionQuery = `child=${ciSysId}`
} else {
directionQuery = `parent=${ciSysId}^ORchild=${ciSysId}`
}
const searchParams = new URLSearchParams()
appendReadParams(searchParams, {
query: buildEncodedQuery([], [directionQuery, params.query].filter(Boolean).join('^')),
limit: params.limit,
offset: params.offset,
fields: params.fields,
displayValue: params.displayValue,
defaultDisplayValue: DEFAULT_DISPLAY_VALUE,
})
return withQueryString(
`${baseUrl}/api/now/table/${SERVICENOW_TABLES.CI_RELATIONSHIP}`,
searchParams
)
},
method: 'GET',
headers: (params) => buildServiceNowHeaders(params),
},
transformResponse: transformRecordListResponse,
outputs: recordListOutputs,
}
@@ -0,0 +1,81 @@
import { DEFAULT_DISPLAY_VALUE, SERVICENOW_TABLES } from '@/tools/servicenow/constants'
import { authParams, listParams, recordListOutputs } from '@/tools/servicenow/params'
import type {
ServiceNowListGroupMembersParams,
ServiceNowRecordListResponse,
} from '@/tools/servicenow/types'
import {
appendReadParams,
buildEncodedQuery,
buildServiceNowHeaders,
normalizeInstanceUrl,
transformRecordListResponse,
withQueryString,
} from '@/tools/servicenow/utils'
import type { ToolConfig } from '@/tools/types'
export const listGroupMembersTool: ToolConfig<
ServiceNowListGroupMembersParams,
ServiceNowRecordListResponse
> = {
id: 'servicenow_list_group_members',
name: 'List ServiceNow Group Members',
description:
'List the members of a ServiceNow group from the Group Member [sys_user_grmember] table. Each row links a user to a group, so use it to find who can be assigned work for an assignment group.',
version: '1.0.0',
params: {
...authParams,
groupSysId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'sys_id of the sys_user_group whose members should be listed.',
},
groupName: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Exact group name, resolved against the referenced group record. Provide this or the group sys_id.',
},
...listParams,
},
request: {
url: (params) => {
const baseUrl = normalizeInstanceUrl(params.instanceUrl)
const query = buildEncodedQuery(
[
['group', '=', params.groupSysId],
['group.name', '=', params.groupName],
],
params.query
)
if (!query) {
throw new Error('Either a group sys_id or a group name is required')
}
const searchParams = new URLSearchParams()
appendReadParams(searchParams, {
query,
limit: params.limit,
offset: params.offset,
fields: params.fields,
displayValue: params.displayValue,
defaultDisplayValue: DEFAULT_DISPLAY_VALUE,
})
return withQueryString(
`${baseUrl}/api/now/table/${SERVICENOW_TABLES.GROUP_MEMBER}`,
searchParams
)
},
method: 'GET',
headers: (params) => buildServiceNowHeaders(params),
},
transformResponse: transformRecordListResponse,
outputs: recordListOutputs,
}
+108
View File
@@ -0,0 +1,108 @@
import { DEFAULT_DISPLAY_VALUE, SERVICENOW_TABLES } from '@/tools/servicenow/constants'
import { authParams, listParams, recordListOutputs } from '@/tools/servicenow/params'
import type {
ServiceNowListIncidentsParams,
ServiceNowRecordListResponse,
} from '@/tools/servicenow/types'
import {
appendReadParams,
buildEncodedQuery,
buildServiceNowHeaders,
normalizeInstanceUrl,
transformRecordListResponse,
withQueryString,
} from '@/tools/servicenow/utils'
import type { ToolConfig } from '@/tools/types'
export const listIncidentsTool: ToolConfig<
ServiceNowListIncidentsParams,
ServiceNowRecordListResponse
> = {
id: 'servicenow_list_incidents',
name: 'List ServiceNow Incidents',
description:
'Search ServiceNow incidents by state, priority, assignment, caller, or text. All filters are ANDed together.',
version: '1.0.0',
params: {
...authParams,
searchText: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Text to match against the incident short description using the ServiceNow LIKE operator, which matches anywhere in the field.',
},
state: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Incident state coded value. Base system: 1=New, 2=In Progress, 3=On Hold, 6=Resolved, 7=Closed, 8=Canceled.',
},
priority: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Priority coded value 1-5 (1 Critical … 5 Planning).',
},
assignmentGroup: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'sys_id of the assignment group.',
},
assignedTo: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'sys_id of the assigned user.',
},
callerId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'sys_id of the caller.',
},
active: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Restrict to active ("true") or inactive ("false") incidents.',
},
...listParams,
},
request: {
url: (params) => {
const baseUrl = normalizeInstanceUrl(params.instanceUrl)
const searchParams = new URLSearchParams()
appendReadParams(searchParams, {
query: buildEncodedQuery(
[
['state', '=', params.state],
['priority', '=', params.priority],
['assignment_group', '=', params.assignmentGroup],
['assigned_to', '=', params.assignedTo],
['caller_id', '=', params.callerId],
['active', '=', params.active],
['short_description', 'LIKE', params.searchText],
],
params.query
),
limit: params.limit,
offset: params.offset,
fields: params.fields,
displayValue: params.displayValue,
defaultDisplayValue: DEFAULT_DISPLAY_VALUE,
})
return withQueryString(`${baseUrl}/api/now/table/${SERVICENOW_TABLES.INCIDENT}`, searchParams)
},
method: 'GET',
headers: (params) => buildServiceNowHeaders(params),
},
transformResponse: transformRecordListResponse,
outputs: recordListOutputs,
}
@@ -0,0 +1,81 @@
import { DEFAULT_DISPLAY_VALUE, SERVICENOW_TABLES } from '@/tools/servicenow/constants'
import { authParams, listParams, recordListOutputs } from '@/tools/servicenow/params'
import type {
ServiceNowListRequestedItemsParams,
ServiceNowRecordListResponse,
} from '@/tools/servicenow/types'
import {
appendReadParams,
buildEncodedQuery,
buildServiceNowHeaders,
normalizeInstanceUrl,
transformRecordListResponse,
withQueryString,
} from '@/tools/servicenow/utils'
import type { ToolConfig } from '@/tools/types'
export const listRequestedItemsTool: ToolConfig<
ServiceNowListRequestedItemsParams,
ServiceNowRecordListResponse
> = {
id: 'servicenow_list_requested_items',
name: 'List ServiceNow Requested Items',
description:
'List requested items (RITMs) from the ServiceNow Requested Item [sc_req_item] table, optionally scoped to a parent request or a catalog item. To scope by requester, pass an encoded query against the parent request, for example "request.requested_for=<sys_id>".',
version: '1.0.0',
params: {
...authParams,
requestSysId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'sys_id of the parent request (sc_request) whose items should be listed.',
},
catalogItemSysId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'sys_id of the catalog item (cat_item) to filter by.',
},
active: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Restrict to active ("true") or inactive ("false") requested items.',
},
...listParams,
},
request: {
url: (params) => {
const baseUrl = normalizeInstanceUrl(params.instanceUrl)
const searchParams = new URLSearchParams()
appendReadParams(searchParams, {
query: buildEncodedQuery(
[
['request', '=', params.requestSysId],
['cat_item', '=', params.catalogItemSysId],
['active', '=', params.active],
],
params.query
),
limit: params.limit,
offset: params.offset,
fields: params.fields,
displayValue: params.displayValue,
defaultDisplayValue: DEFAULT_DISPLAY_VALUE,
})
return withQueryString(
`${baseUrl}/api/now/table/${SERVICENOW_TABLES.REQUESTED_ITEM}`,
searchParams
)
},
method: 'GET',
headers: (params) => buildServiceNowHeaders(params),
},
transformResponse: transformRecordListResponse,
outputs: recordListOutputs,
}
@@ -0,0 +1,111 @@
import { authParams } from '@/tools/servicenow/params'
import type {
ServiceNowOrderCatalogItemParams,
ServiceNowOrderCatalogItemResponse,
} from '@/tools/servicenow/types'
import {
buildServiceNowHeaders,
normalizeInstanceUrl,
parseServiceNowResponse,
readString,
toRecordObject,
} from '@/tools/servicenow/utils'
import type { ToolConfig } from '@/tools/types'
export const orderCatalogItemTool: ToolConfig<
ServiceNowOrderCatalogItemParams,
ServiceNowOrderCatalogItemResponse
> = {
id: 'servicenow_order_catalog_item',
name: 'Order ServiceNow Catalog Item',
description:
'Submit a service catalog request for a catalog item using the Service Catalog API order_now endpoint. Returns the generated request number and sys_id.',
version: '1.0.0',
params: {
...authParams,
catalogItemSysId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'sys_id of the catalog item to order. Use List ServiceNow Catalog Items to find it.',
},
quantity: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Quantity to order. Must not be negative. Defaults to 1.',
},
requestedFor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'sys_id of the sys_user the item is ordered for. Ordering on behalf of another user is governed by the glide.sc.req_for.roles instance properties.',
},
variables: {
type: 'json',
required: false,
visibility: 'user-or-llm',
description:
'Name-value pairs for the catalog item variables, as a JSON object (e.g., {"data_plan": "500MB"}). All variables the item marks mandatory must be supplied.',
},
},
request: {
url: (params) => {
const baseUrl = normalizeInstanceUrl(params.instanceUrl)
const itemSysId = params.catalogItemSysId?.trim()
if (!itemSysId) {
throw new Error('A catalog item sys_id is required')
}
return `${baseUrl}/api/sn_sc/servicecatalog/items/${itemSysId}/order_now`
},
method: 'POST',
headers: (params) => buildServiceNowHeaders(params, { json: true }),
body: (params) => {
let variables: Record<string, unknown> | undefined
if (typeof params.variables === 'string' && params.variables.trim()) {
const parsed = JSON.parse(params.variables)
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new Error('variables must be a JSON object')
}
variables = parsed as Record<string, unknown>
} else if (params.variables && typeof params.variables === 'object') {
variables = params.variables as Record<string, unknown>
}
const body: Record<string, unknown> = {
sysparm_quantity: params.quantity ?? 1,
}
if (params.requestedFor) body.sysparm_requested_for = params.requestedFor
if (variables) body.variables = variables
return body
},
},
transformResponse: async (response: Response) => {
const data = await parseServiceNowResponse(response)
const result = toRecordObject(data.result)
return {
success: true,
output: {
sysId: readString(result, 'sys_id'),
number: readString(result, 'number'),
requestNumber: readString(result, 'request_number'),
requestId: readString(result, 'request_id'),
table: readString(result, 'table'),
},
}
},
outputs: {
sysId: { type: 'string', description: 'Sys_id of the order', nullable: true },
number: { type: 'string', description: 'Number of the generated request', nullable: true },
requestNumber: { type: 'string', description: 'Request number', nullable: true },
requestId: { type: 'string', description: 'Sys_id of the order request', nullable: true },
table: { type: 'string', description: 'Table name of the request', nullable: true },
},
}
+169
View File
@@ -0,0 +1,169 @@
import type { ToolConfig } from '@/tools/types'
type ServiceNowParams = ToolConfig['params']
/** Instance URL plus Basic Auth credentials, shared by every ServiceNow tool. */
export const authParams: ServiceNowParams = {
instanceUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ServiceNow instance URL (e.g., https://instance.service-now.com)',
},
username: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ServiceNow username',
},
password: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ServiceNow password',
},
}
/**
* `sysparm_display_value`. Defaults to `all` on every semantic tool, so a
* reference field such as `assigned_to` returns both its sys_id and its label.
*/
export const displayValueParam: ServiceNowParams = {
displayValue: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'How reference and choice fields are returned: "all" (default — both the sys_id and the label, as {value, display_value}), "true" (labels only), or "false" (raw sys_ids and coded values only).',
},
}
/** `sysparm_fields`. */
export const fieldsParam: ServiceNowParams = {
fields: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Comma-separated list of fields to return (e.g., number,short_description,state). Returns all fields when omitted.',
},
}
/** Pagination plus an escape hatch to an arbitrary encoded query. */
export const listParams: ServiceNowParams = {
query: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Additional ServiceNow encoded query, ANDed with the other filters (e.g., "opened_at>=javascript:gs.beginningOfLastMonth()").',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of records to return (sysparm_limit).',
},
offset: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of records to skip for pagination (sysparm_offset).',
},
...fieldsParam,
...displayValueParam,
}
/**
* `sysparm_input_display_value`. When true, ServiceNow resolves a display name
* written to a reference field into the stored sys_id.
*/
export const inputDisplayValueParam: ServiceNowParams = {
inputDisplayValue: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: `Set to true to write display names into reference fields (e.g., assigned_to: "Beth Anglin") and let ServiceNow resolve them to sys_ids. Defaults to false, meaning reference fields must be sys_ids. Note that true also reinterprets date and time values in the requesting user's timezone rather than GMT.`,
},
}
/** Response-shaping params for create/update tools. */
export const writeParams: ServiceNowParams = {
...fieldsParam,
...displayValueParam,
...inputDisplayValueParam,
}
/** The two ways to address an existing record. */
export const recordIdentifierParams: ServiceNowParams = {
sysId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Record sys_id. Provide either this or the record number.',
},
number: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Record number (e.g., INC0010001). Provide either this or the sys_id.',
},
}
/**
* sys_id of the record being written to. The Table API addresses updates by
* sys_id only, so callers holding a record number must resolve it first.
*/
export const requiredSysIdParam: ServiceNowParams = {
sysId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'sys_id of the record to update. If you only have the record number, look it up first (for example with Get ServiceNow Incident).',
},
}
/** JSON escape hatch for fields the tool does not name explicitly. */
export const additionalFieldsParam: ServiceNowParams = {
additionalFields: {
type: 'json',
required: false,
visibility: 'user-or-llm',
description:
'Any other ServiceNow fields to set, as a JSON object of raw column names (e.g., {"correlation_id": "abc-123", "u_custom": "x"}). Merged last, so it overrides the named parameters.',
},
}
/** Standard list-shaped outputs. */
export const recordListOutputs: ToolConfig['outputs'] = {
records: {
type: 'array',
description:
'Matching ServiceNow records. With the default display value mode "all", every field is an object of the form {value, display_value}.',
},
metadata: {
type: 'json',
description: 'Operation metadata',
properties: {
recordCount: { type: 'number', description: 'Number of records returned' },
},
},
}
/** Standard single-record outputs. */
export const recordOutputs: ToolConfig['outputs'] = {
record: {
type: 'json',
description:
'The ServiceNow record. With the default display value mode "all", every field is an object of the form {value, display_value}.',
nullable: true,
},
metadata: {
type: 'json',
description: 'Operation metadata',
properties: {
recordCount: { type: 'number', description: 'Number of records returned (0 or 1)' },
},
},
}
+3 -14
View File
@@ -1,6 +1,6 @@
import { createLogger } from '@sim/logger'
import type { ServiceNowReadParams, ServiceNowReadResponse } from '@/tools/servicenow/types'
import { createBasicAuthHeader } from '@/tools/servicenow/utils'
import { buildServiceNowHeaders, normalizeInstanceUrl } from '@/tools/servicenow/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('ServiceNowReadRecordTool')
@@ -84,10 +84,7 @@ export const readRecordTool: ToolConfig<ServiceNowReadParams, ServiceNowReadResp
request: {
url: (params) => {
const baseUrl = params.instanceUrl.trim().replace(/\/$/, '')
if (!baseUrl) {
throw new Error('ServiceNow instance URL is required')
}
const baseUrl = normalizeInstanceUrl(params.instanceUrl)
let url = `${baseUrl}/api/now/table/${params.tableName.trim()}`
const queryParams = new URLSearchParams()
@@ -125,15 +122,7 @@ export const readRecordTool: ToolConfig<ServiceNowReadParams, ServiceNowReadResp
return queryString ? `${url}?${queryString}` : url
},
method: 'GET',
headers: (params) => {
if (!params.username || !params.password) {
throw new Error('ServiceNow username and password are required')
}
return {
Authorization: createBasicAuthHeader(params.username, params.password),
Accept: 'application/json',
}
},
headers: (params) => buildServiceNowHeaders(params),
},
transformResponse: async (response: Response) => {
@@ -0,0 +1,76 @@
import { INCIDENT_STATE, SERVICENOW_TABLES } from '@/tools/servicenow/constants'
import {
additionalFieldsParam,
authParams,
recordOutputs,
requiredSysIdParam,
writeParams,
} from '@/tools/servicenow/params'
import type {
ServiceNowResolveIncidentParams,
ServiceNowSingleRecordResponse,
} from '@/tools/servicenow/types'
import {
buildFieldPayload,
buildServiceNowHeaders,
buildTableRecordUrl,
transformRecordResponse,
} from '@/tools/servicenow/utils'
import type { ToolConfig } from '@/tools/types'
export const resolveIncidentTool: ToolConfig<
ServiceNowResolveIncidentParams,
ServiceNowSingleRecordResponse
> = {
id: 'servicenow_resolve_incident',
name: 'Resolve ServiceNow Incident',
description:
'Move a ServiceNow incident to Resolved (state 6) with a resolution code and resolution notes.',
version: '1.0.0',
params: {
...authParams,
...requiredSysIdParam,
closeCode: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Resolution code (close_code). This is a choice field whose values are configured per instance — read the choice list on your incident table and pass one of its values.',
},
closeNotes: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Resolution notes (close_notes) documenting how the incident was resolved.',
},
workNotes: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Additional internal work note to append.',
},
...additionalFieldsParam,
...writeParams,
},
request: {
url: (params) => buildTableRecordUrl(params, SERVICENOW_TABLES.INCIDENT),
method: 'PATCH',
headers: (params) => buildServiceNowHeaders(params, { json: true }),
body: (params) =>
buildFieldPayload(
{
state: INCIDENT_STATE.RESOLVED,
close_code: params.closeCode,
close_notes: params.closeNotes,
work_notes: params.workNotes,
},
params.additionalFields
),
},
transformResponse: transformRecordResponse,
outputs: recordOutputs,
}
+77
View File
@@ -0,0 +1,77 @@
import { DEFAULT_DISPLAY_VALUE, SERVICENOW_TABLES } from '@/tools/servicenow/constants'
import { authParams, listParams, recordListOutputs } from '@/tools/servicenow/params'
import type {
ServiceNowRecordListResponse,
ServiceNowSearchCisParams,
} from '@/tools/servicenow/types'
import {
appendReadParams,
buildEncodedQuery,
buildServiceNowHeaders,
normalizeInstanceUrl,
transformRecordListResponse,
withQueryString,
} from '@/tools/servicenow/utils'
import type { ToolConfig } from '@/tools/types'
export const searchCisTool: ToolConfig<ServiceNowSearchCisParams, ServiceNowRecordListResponse> = {
id: 'servicenow_search_cis',
name: 'Search ServiceNow Configuration Items',
description:
'Search the ServiceNow CMDB for configuration items. Defaults to the base cmdb_ci table, which returns CIs of every class; pass a CI class to scope the search to a subclass such as cmdb_ci_linux_server.',
version: '1.0.0',
params: {
...authParams,
ciClass: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: `CMDB class (table) to search, e.g., cmdb_ci_linux_server or cmdb_ci_app_server. Defaults to ${SERVICENOW_TABLES.CI}, which covers every CI class.`,
},
name: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Text to match against the CI name using the ServiceNow LIKE operator, which matches anywhere in the field.',
},
operationalStatus: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Operational status coded value (operational_status). The choice list is configured per instance.',
},
...listParams,
},
request: {
url: (params) => {
const baseUrl = normalizeInstanceUrl(params.instanceUrl)
const table = params.ciClass?.trim() || SERVICENOW_TABLES.CI
const searchParams = new URLSearchParams()
appendReadParams(searchParams, {
query: buildEncodedQuery(
[
['name', 'LIKE', params.name],
['operational_status', '=', params.operationalStatus],
],
params.query
),
limit: params.limit,
offset: params.offset,
fields: params.fields,
displayValue: params.displayValue,
defaultDisplayValue: DEFAULT_DISPLAY_VALUE,
})
return withQueryString(`${baseUrl}/api/now/table/${table}`, searchParams)
},
method: 'GET',
headers: (params) => buildServiceNowHeaders(params),
},
transformResponse: transformRecordListResponse,
outputs: recordListOutputs,
}
@@ -0,0 +1,157 @@
import { authParams } from '@/tools/servicenow/params'
import type {
ServiceNowSearchKnowledgeParams,
ServiceNowSearchKnowledgeResponse,
} from '@/tools/servicenow/types'
import {
buildServiceNowHeaders,
normalizeInstanceUrl,
parseServiceNowResponse,
readNestedNumber,
readRecordArray,
toRecordObject,
withQueryString,
} from '@/tools/servicenow/utils'
import type { ToolConfig } from '@/tools/types'
export const searchKnowledgeTool: ToolConfig<
ServiceNowSearchKnowledgeParams,
ServiceNowSearchKnowledgeResponse
> = {
id: 'servicenow_search_knowledge',
name: 'Search ServiceNow Knowledge',
description:
'Search ServiceNow knowledge base articles through the Knowledge Management API. Returns ranked results with a snippet and the article number, which Get ServiceNow Knowledge Article accepts to fetch the full body.',
version: '1.0.0',
params: {
...authParams,
query: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Text to search for across knowledge articles.',
},
filter: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Encoded query used to filter the results, against the Knowledge [kb_knowledge] table (e.g., "workflow_state=published").',
},
knowledgeBaseSysIds: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Comma-separated knowledge base sys_ids (from kb_knowledge_base) to restrict results to.',
},
language: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Comma-separated ISO 639-1 language codes to restrict results to, or "all". Defaults to the session language.',
},
fields: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Comma-separated kb_knowledge fields to include in each result (e.g., workflow_state,kb_category).',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of articles to return. ServiceNow defaults to 30.',
},
offset: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of articles to skip for pagination.',
},
},
request: {
url: (params) => {
const baseUrl = normalizeInstanceUrl(params.instanceUrl)
const searchParams = new URLSearchParams()
if (params.query) searchParams.append('query', params.query)
if (params.filter) searchParams.append('filter', params.filter)
if (params.knowledgeBaseSysIds) searchParams.append('kb', params.knowledgeBaseSysIds)
if (params.language) searchParams.append('language', params.language)
if (params.fields) searchParams.append('fields', params.fields)
if (params.limit !== undefined && params.limit !== null) {
searchParams.append('limit', String(params.limit))
}
if (params.offset !== undefined && params.offset !== null) {
searchParams.append('offset', String(params.offset))
}
return withQueryString(`${baseUrl}/api/sn_km_api/knowledge/articles`, searchParams)
},
method: 'GET',
headers: (params) => buildServiceNowHeaders(params),
},
transformResponse: async (response: Response) => {
const data = await parseServiceNowResponse(response)
const result = toRecordObject(data.result)
const articles = readRecordArray(result, 'articles')
return {
success: true,
output: {
articles,
metadata: {
recordCount: articles.length,
totalCount: readNestedNumber(result, 'meta', 'count'),
},
},
}
},
outputs: {
articles: {
type: 'array',
description: 'Matching knowledge articles, sorted in descending order by relevance score',
items: {
type: 'object',
properties: {
id: {
type: 'string',
description:
'Table-prefixed article identifier, e.g. "kb_knowledge:9e528db1...". Get ServiceNow Knowledge Article takes a bare sys_id or KB number, so pass `number` or the portion after the colon rather than this value as-is',
},
number: { type: 'string', description: 'Knowledge article number' },
title: { type: 'string', description: 'Article title (short description)' },
snippet: { type: 'string', description: 'Small excerpt of the article text' },
link: { type: 'string', description: 'Link to the article' },
score: { type: 'number', description: 'Relevancy score' },
rank: { type: 'number', description: 'Search rank of the article for this search' },
fields: {
type: 'json',
description:
'Requested kb_knowledge fields, each {name, label, type, value, display_value}',
optional: true,
},
},
},
},
metadata: {
type: 'json',
description: 'Operation metadata',
properties: {
recordCount: { type: 'number', description: 'Number of articles returned' },
totalCount: {
type: 'number',
description: 'Total number of available articles reported by ServiceNow',
nullable: true,
},
},
},
},
}
@@ -0,0 +1,620 @@
/**
* @vitest-environment node
*
* Guards the invariants of the shared `utils.ts` refactor: the eight
* pre-existing generic Table API tools must keep their original wire behavior,
* and the semantic tools must default `sysparm_display_value` to `all` without
* leaking that default onto the generic ones.
*/
import { describe, expect, it } from 'vitest'
import { ServiceNowBlock } from '@/blocks/blocks/servicenow'
import * as servicenowTools from '@/tools/servicenow'
import { aggregateTool } from '@/tools/servicenow/aggregate'
import { DEFAULT_DISPLAY_VALUE } from '@/tools/servicenow/constants'
import { createIncidentTool } from '@/tools/servicenow/create_incident'
import { createRecordTool } from '@/tools/servicenow/create_record'
import { deleteRecordTool } from '@/tools/servicenow/delete_record'
import { downloadAttachmentTool } from '@/tools/servicenow/download_attachment'
import { getChangeNextStatesTool } from '@/tools/servicenow/get_change_next_states'
import { getIncidentTool } from '@/tools/servicenow/get_incident'
import { listAttachmentsTool } from '@/tools/servicenow/list_attachments'
import { listIncidentsTool } from '@/tools/servicenow/list_incidents'
import { readRecordTool } from '@/tools/servicenow/read_record'
import { searchKnowledgeTool } from '@/tools/servicenow/search_knowledge'
import { updateChangeStateTool } from '@/tools/servicenow/update_change_state'
import { updateIncidentTool } from '@/tools/servicenow/update_incident'
import { updateRecordTool } from '@/tools/servicenow/update_record'
/** Obvious non-secret so credential scanners do not flag these fixtures. */
const PLACEHOLDER_PASSWORD = 'not-a-real-password'
const auth = {
instanceUrl: 'https://example.service-now.com',
username: 'svc.user',
password: PLACEHOLDER_PASSWORD,
}
const EXPECTED_BASIC = `Basic ${Buffer.from(`svc.user:${PLACEHOLDER_PASSWORD}`).toString('base64')}`
function urlOf(tool: { request: { url: (p: never) => string } }, params: unknown): URL {
return new URL(tool.request.url(params as never))
}
function headersOf(
tool: { request: { headers?: (p: never) => Record<string, string> } },
params: unknown
) {
return tool.request.headers?.(params as never) ?? {}
}
describe('ServiceNow shared request helpers', () => {
it('normalizes the instance URL by trimming whitespace and a trailing slash', () => {
const url = urlOf(readRecordTool, {
...auth,
instanceUrl: ' https://example.service-now.com/ ',
tableName: 'incident',
})
expect(url.origin).toBe('https://example.service-now.com')
expect(url.pathname).toBe('/api/now/table/incident')
})
it('throws the original message when the instance URL is blank', () => {
expect(() =>
urlOf(readRecordTool, { ...auth, instanceUrl: ' ', tableName: 'incident' })
).toThrow('ServiceNow instance URL is required')
})
it('throws the original message when credentials are missing', () => {
expect(() =>
headersOf(readRecordTool, { ...auth, password: '', tableName: 'incident' })
).toThrow('ServiceNow username and password are required')
})
})
describe('pre-existing generic Table API tools keep their original wire behavior', () => {
it('create_record posts to the table collection with JSON headers', () => {
const params = { ...auth, tableName: 'incident', fields: { short_description: 'x' } }
expect(urlOf(createRecordTool, params).pathname).toBe('/api/now/table/incident')
expect(createRecordTool.request.method).toBe('POST')
expect(headersOf(createRecordTool, params)).toEqual({
Authorization: EXPECTED_BASIC,
'Content-Type': 'application/json',
Accept: 'application/json',
})
})
it('update_record patches the record URL with JSON headers', () => {
const params = { ...auth, tableName: 'incident', sysId: ' abc123 ', fields: { state: '2' } }
expect(urlOf(updateRecordTool, params).pathname).toBe('/api/now/table/incident/abc123')
expect(updateRecordTool.request.method).toBe('PATCH')
expect(headersOf(updateRecordTool, params)['Content-Type']).toBe('application/json')
})
it('delete_record targets the record URL without a Content-Type', () => {
const params = { ...auth, tableName: 'incident', sysId: 'abc123' }
expect(urlOf(deleteRecordTool, params).pathname).toBe('/api/now/table/incident/abc123')
expect(deleteRecordTool.request.method).toBe('DELETE')
expect(headersOf(deleteRecordTool, params)).toEqual({
Authorization: EXPECTED_BASIC,
Accept: 'application/json',
})
})
it('aggregate targets the stats endpoint', () => {
const params = { ...auth, tableName: 'incident', count: true }
expect(urlOf(aggregateTool, params).pathname).toBe('/api/now/stats/incident')
expect(headersOf(aggregateTool, params)).toEqual({
Authorization: EXPECTED_BASIC,
Accept: 'application/json',
})
})
it('list_attachments filters by table and record sys_id', () => {
const url = urlOf(listAttachmentsTool, { ...auth, tableName: 'incident', recordSysId: 'rec1' })
expect(url.pathname).toBe('/api/now/attachment')
expect(url.searchParams.get('sysparm_query')).toBe('table_name=incident^table_sys_id=rec1')
})
it('download_attachment keeps its wildcard Accept header', () => {
const params = { ...auth, attachmentSysId: 'att1' }
expect(urlOf(downloadAttachmentTool, params).pathname).toBe('/api/now/attachment/att1/file')
expect(headersOf(downloadAttachmentTool, params)).toEqual({
Authorization: EXPECTED_BASIC,
Accept: '*/*',
})
})
})
describe('sysparm_display_value separation', () => {
it('read_record omits sysparm_display_value unless the caller sets one', () => {
const url = urlOf(readRecordTool, { ...auth, tableName: 'incident' })
expect(url.searchParams.has('sysparm_display_value')).toBe(false)
})
it('read_record still forwards an explicit display value', () => {
const url = urlOf(readRecordTool, { ...auth, tableName: 'incident', displayValue: 'true' })
expect(url.searchParams.get('sysparm_display_value')).toBe('true')
})
it('aggregate omits sysparm_display_value unless the caller sets one', () => {
const url = urlOf(aggregateTool, { ...auth, tableName: 'incident', count: true })
expect(url.searchParams.has('sysparm_display_value')).toBe(false)
})
it('semantic reads default to all', () => {
expect(urlOf(listIncidentsTool, auth).searchParams.get('sysparm_display_value')).toBe(
DEFAULT_DISPLAY_VALUE
)
expect(
urlOf(getIncidentTool, { ...auth, number: 'INC0010001' }).searchParams.get(
'sysparm_display_value'
)
).toBe(DEFAULT_DISPLAY_VALUE)
})
it('semantic writes default to all and omit sysparm_input_display_value', () => {
const url = urlOf(createIncidentTool, { ...auth, shortDescription: 'x' })
expect(url.searchParams.get('sysparm_display_value')).toBe(DEFAULT_DISPLAY_VALUE)
expect(url.searchParams.has('sysparm_input_display_value')).toBe(false)
})
it('semantic writes surface sysparm_input_display_value when enabled', () => {
const url = urlOf(createIncidentTool, {
...auth,
shortDescription: 'x',
inputDisplayValue: true,
})
expect(url.searchParams.get('sysparm_input_display_value')).toBe('true')
})
})
describe('block params mapping keeps per-operation defaults from colliding', () => {
const mapParams = ServiceNowBlock.tools.config?.params
/**
* Every subBlock default is seeded by id, so two subBlocks sharing an id
* would leave a single stored value that the last definition wins.
*/
function seededDefaults(): Record<string, unknown> {
const seeded: Record<string, unknown> = {}
for (const subBlock of ServiceNowBlock.subBlocks) {
if (typeof subBlock.value === 'function') {
seeded[subBlock.id] = (subBlock.value as (p: Record<string, never>) => unknown)({})
}
}
return seeded
}
/**
* The block's mapping is merged over the raw inputs as
* `{ ...inputs, ...mapped }`, so a key the mapper leaves off is not dropped
* it keeps whatever the subBlock store held. Assertions therefore have to be
* made against the merged result, not the mapper's return value.
*/
function mergedParams(stored: Record<string, unknown>): Record<string, unknown> {
const inputs = { ...seededDefaults(), ...auth, ...stored }
const mapped = (mapParams?.(inputs as never) ?? {}) as Record<string, unknown>
return { ...inputs, ...mapped }
}
/**
* Standing guard for the whole bug class: a subBlock id may legitimately be
* reused across operations that feed the same tool param, but the definitions
* must agree on the seeded value, since only the last one survives. An absent
* default and an empty default both mean "unset" and are treated as equal.
*/
it('never lets one subBlock id carry two different seeded defaults', () => {
const seededById = new Map<string, Set<string>>()
for (const subBlock of ServiceNowBlock.subBlocks) {
const seeded =
typeof subBlock.value === 'function'
? (subBlock.value as (p: Record<string, never>) => unknown)({})
: undefined
const normalized = seeded === undefined || seeded === '' ? '' : JSON.stringify(seeded)
const values = seededById.get(subBlock.id) ?? new Set<string>()
values.add(normalized)
seededById.set(subBlock.id, values)
}
const conflicts = [...seededById.entries()]
.filter(([, values]) => values.size > 1)
.map(([id, values]) => `${id}: ${[...values].join(' vs ')}`)
expect(conflicts).toEqual([])
})
it('does not leak the semantic "all" default onto the generic Table API tools', () => {
const mapped = mapParams?.({
...seededDefaults(),
...auth,
operation: 'servicenow_read_record',
tableName: 'incident',
} as never) as Record<string, unknown>
expect(mapped.displayValue).toBeFalsy()
})
it('does not leak the semantic "all" default onto aggregate', () => {
const mapped = mapParams?.({
...seededDefaults(),
...auth,
operation: 'servicenow_aggregate',
tableName: 'incident',
} as never) as Record<string, unknown>
expect(mapped.displayValue).toBeFalsy()
})
it('applies the semantic "all" default on a semantic operation', () => {
const mapped = mapParams?.({
...seededDefaults(),
...auth,
operation: 'servicenow_list_incidents',
} as never) as Record<string, unknown>
expect(mapped.displayValue).toBe(DEFAULT_DISPLAY_VALUE)
})
it('does not leak the approval state default onto incident creation', () => {
const mapped = mapParams?.({
...seededDefaults(),
...auth,
operation: 'servicenow_create_incident',
shortDescription: 'x',
} as never) as Record<string, unknown>
expect(mapped.state).toBeFalsy()
})
it('routes the approval state control to state for list approvals', () => {
const mapped = mapParams?.({
...seededDefaults(),
...auth,
operation: 'servicenow_list_approvals',
} as never) as Record<string, unknown>
expect(mapped.state).toBe('requested')
})
it('routes the target state control to state for a change transition', () => {
const mapped = mapParams?.({
...seededDefaults(),
...auth,
operation: 'servicenow_update_change_state',
sysId: 'chg1',
} as never) as Record<string, unknown>
expect(mapped.state).toBe('-5')
})
})
describe('one subBlock id never carries two different value spaces', () => {
const mapParams = ServiceNowBlock.tools.config?.params
function seededDefaults(): Record<string, unknown> {
const seeded: Record<string, unknown> = {}
for (const subBlock of ServiceNowBlock.subBlocks) {
if (typeof subBlock.value === 'function') {
seeded[subBlock.id] = (subBlock.value as (p: Record<string, never>) => unknown)({})
}
}
return seeded
}
function mergedParams(stored: Record<string, unknown>): Record<string, unknown> {
const inputs = { ...seededDefaults(), ...auth, ...stored }
const mapped = (mapParams?.(inputs as never) ?? {}) as Record<string, unknown>
return { ...inputs, ...mapped }
}
/**
* Subblock values are stored per block keyed by id, so switching operations
* leaves the previous operation's value in place. Where two operations mean
* different things by the same tool param an incident state versus a change
* state, a close code from two different choice lists, a search phrase versus
* an encoded query they must not share a subBlock id, or the stale value
* rides along and is written to the wrong record.
*/
it.each([
['incidentState', 'changeState', 'state', 'servicenow_update_change_request', '6', '-2'],
['changeState', 'incidentState', 'state', 'servicenow_update_incident', '-2', '6'],
[
'resolutionCode',
'changeCloseCode',
'closeCode',
'servicenow_update_change_request',
'Solved (Permanently)',
'successful',
],
[
'incidentComments',
'approvalComments',
'comments',
'servicenow_update_approval',
'visible to the caller',
'approved by change board',
],
[
'query',
'knowledgeQuery',
'query',
'servicenow_search_knowledge',
'active=true^priority=1',
'vpn setup',
],
])(
'a stale %s never reaches %s of the wrong operation',
(staleId, ownId, param, operation, staleValue, ownValue) => {
const leaked = mergedParams({ operation, [staleId]: staleValue })
expect(leaked[param]).not.toBe(staleValue)
const kept = mergedParams({ operation, [staleId]: staleValue, [ownId]: ownValue })
expect(kept[param]).toBe(ownValue)
}
)
/**
* `fields` carries a JSON body on Create/Update Record and a comma-separated
* projection everywhere else. The shipped ids keep the original `fields`
* subblock, so the split is one-directional: no operation added since can put
* a projection where a JSON body is parsed, or a body where a projection goes.
*/
it('never sends a JSON body as a field projection', () => {
const merged = mergedParams({
operation: 'servicenow_list_incidents',
fields: '{"short_description":"x"}',
returnFields: 'number,short_description',
})
expect(merged.fields).toBe('number,short_description')
})
it('never parses a field projection as a create body', () => {
const merged = mergedParams({
operation: 'servicenow_create_record',
tableName: 'incident',
fields: '{"short_description":"x"}',
returnFields: 'number,short_description',
})
expect(merged.fields).toEqual({ short_description: 'x' })
})
it('leaves the generic Table API operations without a semantic state', () => {
const merged = mergedParams({
operation: 'servicenow_read_record',
tableName: 'incident',
incidentState: '6',
})
expect(merged.state).toBeUndefined()
})
})
describe('every subBlock a tool reads is one the tool actually declares', () => {
const toolsById = new Map(Object.values(servicenowTools).map((tool) => [tool.id, tool] as const))
/**
* The block seeds and serializes a subBlock purely from its `condition`, with
* no check that the target tool declares a matching param. A control offered
* on an operation whose tool ignores it is a silent data-loss bug: the user
* fills it in, the block maps it, and the request builder drops it.
*/
it.each([
['additionalFields', 'additionalFields'],
['targetState', 'state'],
['approvalState', 'state'],
])('routes the %s control only to operations declaring %s', (subBlockId, paramName) => {
const subBlock = ServiceNowBlock.subBlocks.find((candidate) => candidate.id === subBlockId)
const conditionValue =
subBlock?.condition && 'value' in subBlock.condition ? subBlock.condition.value : undefined
const ops = Array.isArray(conditionValue) ? conditionValue : [conditionValue]
const ignoring = ops
.filter((op): op is string => typeof op === 'string')
.filter((op) => !toolsById.get(op)?.params?.[paramName])
expect(ignoring).toEqual([])
})
it('lets a change transition carry raw fields the named controls do not cover', () => {
const body = updateChangeStateTool.request.body?.({
...auth,
sysId: 'chg1',
state: '-2',
additionalFields: { on_hold_reason: 'Awaiting vendor' },
} as never)
expect(body).toEqual({ state: '-2', on_hold_reason: 'Awaiting vendor' })
})
})
describe('coded-value controls stay reachable on a customized instance', () => {
/**
* ServiceNow does not publish incident state codes at all, and any instance
* may extend a choice list. A select-only `dropdown` would make those codes
* unreachable most sharply on Move Change State, whose target state is
* required and whose real codes come from get_change_next_states. A free-text
* control is equally fine; the invariant is only that the control is not
* select-only.
*/
it.each([
'incidentState',
'changeState',
'targetState',
'approvalState',
'impact',
'urgency',
'priority',
'type',
'resolutionCode',
'changeCloseCode',
])('accepts a raw value for %s', (subBlockId) => {
const matches = ServiceNowBlock.subBlocks.filter((subBlock) => subBlock.id === subBlockId)
expect(matches.length).toBeGreaterThan(0)
const selectOnly = matches.filter((subBlock) => subBlock.type === 'dropdown')
expect(selectOnly).toEqual([])
})
})
describe('a successful response never yields a non-record where a record is declared', () => {
/**
* A collection member that is not a plain object would otherwise be cast and
* handed to the next block as a record, so the tool reports success while
* emitting a value its declared output says cannot occur.
*/
it('drops non-object members of a record collection', async () => {
const output = (await listIncidentsTool.transformResponse?.(
new Response(JSON.stringify({ result: [{ number: 'INC1' }, null, 'oops', [1], 7] }), {
status: 200,
}) as never,
undefined as never
)) as { output: { records: unknown[]; metadata: { recordCount: number } } }
expect(output.output.records).toEqual([{ number: 'INC1' }])
expect(output.output.metadata.recordCount).toBe(1)
})
it('reports no record when a single-record endpoint returns a scalar', async () => {
const output = (await getIncidentTool.transformResponse?.(
new Response(JSON.stringify({ result: 'not a record' }), { status: 200 }) as never,
undefined as never
)) as { output: { record: unknown; metadata: { recordCount: number } } }
expect(output.output.record).toBeNull()
expect(output.output.metadata.recordCount).toBe(0)
})
it('drops non-object knowledge articles', async () => {
const output = (await searchKnowledgeTool.transformResponse?.(
new Response(
JSON.stringify({ result: { articles: [{ id: 'kb_knowledge:1' }, null, 'x'], meta: {} } }),
{ status: 200 }
) as never,
undefined as never
)) as { output: { articles: unknown[] } }
expect(output.output.articles).toEqual([{ id: 'kb_knowledge:1' }])
})
it('drops non-object change state transitions', async () => {
const output = (await getChangeNextStatesTool.transformResponse?.(
new Response(
JSON.stringify({
result: {
available_states: ['0'],
state_transitions: [[{ to_state: '0', transition_available: true }, null], ['nope']],
state_label: { '0': 'Review' },
},
}),
{ status: 200 }
) as never,
undefined as never
)) as { output: { stateTransitions: unknown[]; allowedStates: string[] } }
expect(output.output.stateTransitions).toEqual([{ to_state: '0', transition_available: true }])
expect(output.output.allowedStates).toEqual(['0'])
})
})
describe('write tools require a sys_id rather than a record number', () => {
it('tells the caller to resolve the number first', () => {
expect(updateIncidentTool.params.sysId?.required).toBe(true)
expect(updateIncidentTool.params.sysId?.description).toMatch(/record number/i)
})
})
describe('get_change_next_states', () => {
/** The sample response published with the nextstates endpoint. */
const documentedResult = {
result: {
available_states: ['0', '4', '-1'],
state_transitions: [
[
{
sys_id: '7a0d2ccdc343101035ae3f52c1d3ae2e',
display_value: 'Implement to Review',
from_state: '-1',
to_state: '0',
transition_available: false,
automatic_transition: true,
conditions: [
{
passed: false,
condition: {
name: 'No active Change Tasks',
description: null,
sys_id: '3c1d2ccdc343101035ae3f52c1d3aea4',
},
},
],
},
{
sys_id: 'db401481c343101035ae3f52c1d3aedd',
display_value: 'Implement to Review',
from_state: '-1',
to_state: '0',
transition_available: true,
automatic_transition: false,
conditions: [
{
passed: true,
condition: {
name: 'Not On hold',
description: null,
sys_id: '2132deb6c303101035ae3f52c1d3ae8c',
},
},
],
},
],
[
{
sys_id: '5327c551c343101035ae3f52c1d3aeec',
display_value: 'Implement to Canceled',
from_state: '-1',
to_state: '4',
transition_available: true,
automatic_transition: false,
conditions: [],
},
],
],
state_label: { '0': 'Review', '4': 'Canceled', '-1': 'Implement' },
},
}
it('targets the documented nextstates endpoint', () => {
const url = urlOf(getChangeNextStatesTool, { ...auth, changeSysId: ' chg1 ' })
expect(url.pathname).toBe('/api/sn_chg_rest/change/chg1/nextstates')
})
it('flattens the per-target-state grouping and derives the reachable states', async () => {
const output = (await getChangeNextStatesTool.transformResponse?.(
new Response(JSON.stringify(documentedResult), { status: 200 }) as never,
undefined as never
)) as { output: Record<string, unknown> }
expect(output.output.availableStates).toEqual(['0', '4', '-1'])
expect(output.output.stateTransitions).toHaveLength(3)
expect(output.output.stateLabels).toEqual({ '0': 'Review', '4': 'Canceled', '-1': 'Implement' })
expect(output.output.metadata).toEqual({ transitionCount: 3 })
})
it('reports only states whose transition is currently available, without duplicates', async () => {
const output = (await getChangeNextStatesTool.transformResponse?.(
new Response(JSON.stringify(documentedResult), { status: 200 }) as never,
undefined as never
)) as { output: { allowedStates: string[] } }
expect(output.output.allowedStates).toEqual(['0', '4'])
})
it('surfaces the instance error message', async () => {
await expect(
getChangeNextStatesTool.transformResponse?.(
new Response(JSON.stringify({ error: { message: 'No Record found' } }), {
status: 404,
}) as never,
undefined as never
)
).rejects.toThrow('No Record found')
})
})
+429 -9
View File
@@ -1,20 +1,431 @@
import type { ToolResponse } from '@/tools/types'
interface ServiceNowRecord {
sys_id: string
number?: string
[key: string]: any
/**
* A ServiceNow record as returned by the Table API. With
* `sysparm_display_value=all` every field is `{ value, display_value }`; with
* `false` (ServiceNow's default) reference fields are `{ value, link }` and
* everything else is a raw string.
*/
export interface ServiceNowRecord {
sys_id?: unknown
number?: unknown
[key: string]: unknown
}
interface ServiceNowBaseParams {
/**
* The envelope every ServiceNow REST endpoint replies with. Success bodies
* carry `result` an object for single-record endpoints, an array for
* collections; failures carry `error`. `result` stays `unknown` so each tool
* narrows it deliberately rather than inheriting an unchecked shape.
*/
export interface ServiceNowEnvelope {
result?: unknown
error?: { message?: string; detail?: string } | string
}
/** Credentials shared by every ServiceNow tool. */
export interface ServiceNowAuthParams {
instanceUrl: string
username: string
password: string
}
interface ServiceNowBaseParams extends ServiceNowAuthParams {
tableName: string
}
/** Table API read parameters shared by every semantic list/get tool. */
export interface ServiceNowReadOptions {
query?: string
limit?: number
offset?: number
fields?: string
displayValue?: string
}
/** Table API write parameters shared by every semantic create/update tool. */
export interface ServiceNowWriteOptions {
fields?: string
displayValue?: string
inputDisplayValue?: boolean | string
}
/** Standard list-shaped output for the semantic tools. */
export interface ServiceNowRecordListResponse extends ToolResponse {
output: {
records: ServiceNowRecord[]
metadata: {
recordCount: number
}
}
}
/** Standard single-record output for the semantic tools. */
export interface ServiceNowSingleRecordResponse extends ToolResponse {
output: {
record: ServiceNowRecord | null
metadata: {
recordCount: number
}
}
}
export interface ServiceNowCreateIncidentParams
extends ServiceNowAuthParams,
ServiceNowWriteOptions {
shortDescription: string
description?: string
callerId?: string
category?: string
subcategory?: string
impact?: string
urgency?: string
priority?: string
state?: string
assignmentGroup?: string
assignedTo?: string
cmdbCi?: string
businessService?: string
contactType?: string
workNotes?: string
comments?: string
additionalFields?: Record<string, unknown> | string
}
export interface ServiceNowGetIncidentParams extends ServiceNowAuthParams {
sysId?: string
number?: string
fields?: string
displayValue?: string
}
export interface ServiceNowListIncidentsParams extends ServiceNowAuthParams, ServiceNowReadOptions {
state?: string
priority?: string
assignmentGroup?: string
assignedTo?: string
callerId?: string
active?: string
searchText?: string
}
export interface ServiceNowUpdateIncidentParams
extends ServiceNowAuthParams,
ServiceNowWriteOptions {
sysId?: string
shortDescription?: string
description?: string
state?: string
impact?: string
urgency?: string
priority?: string
category?: string
subcategory?: string
assignmentGroup?: string
assignedTo?: string
cmdbCi?: string
workNotes?: string
comments?: string
additionalFields?: Record<string, unknown> | string
}
export interface ServiceNowResolveIncidentParams
extends ServiceNowAuthParams,
ServiceNowWriteOptions {
sysId?: string
closeCode: string
closeNotes: string
workNotes?: string
additionalFields?: Record<string, unknown> | string
}
export interface ServiceNowAddCommentParams extends ServiceNowAuthParams, ServiceNowWriteOptions {
sysId?: string
commentField?: string
comment: string
}
export interface ServiceNowCreateChangeParams extends ServiceNowAuthParams, ServiceNowWriteOptions {
shortDescription: string
description?: string
type?: string
category?: string
risk?: string
impact?: string
priority?: string
assignmentGroup?: string
assignedTo?: string
requestedBy?: string
cmdbCi?: string
startDate?: string
endDate?: string
justification?: string
implementationPlan?: string
backoutPlan?: string
testPlan?: string
additionalFields?: Record<string, unknown> | string
}
export interface ServiceNowGetChangeParams extends ServiceNowAuthParams {
sysId?: string
number?: string
fields?: string
displayValue?: string
}
export interface ServiceNowListChangesParams extends ServiceNowAuthParams, ServiceNowReadOptions {
state?: string
type?: string
risk?: string
assignmentGroup?: string
assignedTo?: string
active?: string
searchText?: string
}
export interface ServiceNowUpdateChangeParams extends ServiceNowAuthParams, ServiceNowWriteOptions {
sysId?: string
shortDescription?: string
description?: string
state?: string
risk?: string
impact?: string
priority?: string
assignmentGroup?: string
assignedTo?: string
startDate?: string
endDate?: string
closeCode?: string
closeNotes?: string
workNotes?: string
additionalFields?: Record<string, unknown> | string
}
export interface ServiceNowChangeStateParams extends ServiceNowAuthParams, ServiceNowWriteOptions {
sysId?: string
state: string
closeCode?: string
closeNotes?: string
workNotes?: string
additionalFields?: Record<string, unknown> | string
}
export interface ServiceNowListChangeTasksParams extends ServiceNowAuthParams {
changeSysId: string
query?: string
limit?: number
offset?: number
order?: string
}
export interface ServiceNowChangeTaskListResponse extends ToolResponse {
output: {
tasks: ServiceNowRecord[]
metadata: {
recordCount: number
}
}
}
export interface ServiceNowGetChangeNextStatesParams extends ServiceNowAuthParams {
changeSysId: string
}
export interface ServiceNowGetChangeNextStatesResponse extends ToolResponse {
output: {
availableStates: string[]
allowedStates: string[]
stateLabels: Record<string, string>
/**
* Transitions are passed through verbatim once flattened. The Change
* Management API documents each as `{ sys_id, display_value, from_state,
* to_state, transition_available, automatic_transition, conditions[] }`, but
* only the outer object shape is verified at runtime, so the type stays as
* the raw record it actually is.
*/
stateTransitions: ServiceNowRecord[]
metadata: {
transitionCount: number
}
}
}
export interface ServiceNowListCatalogItemsParams extends ServiceNowAuthParams {
searchText?: string
catalogSysId?: string
categorySysId?: string
limit?: number
offset?: number
}
export interface ServiceNowCatalogItem {
sys_id?: string
name?: string
short_description?: string | null
description?: string
type?: string
sys_class_name?: string
category?: { sys_id?: string; title?: string } | null
catalogs?: Array<{ sys_id?: string; title?: string }>
price?: string
show_price?: boolean
picture?: string
icon?: string
order?: number
[key: string]: unknown
}
export interface ServiceNowListCatalogItemsResponse extends ToolResponse {
output: {
items: ServiceNowCatalogItem[]
metadata: {
recordCount: number
}
}
}
export interface ServiceNowOrderCatalogItemParams extends ServiceNowAuthParams {
catalogItemSysId: string
quantity?: number
requestedFor?: string
variables?: Record<string, unknown> | string
}
export interface ServiceNowOrderCatalogItemResponse extends ToolResponse {
output: {
sysId: string | null
number: string | null
requestNumber: string | null
requestId: string | null
table: string | null
}
}
export interface ServiceNowListRequestedItemsParams
extends ServiceNowAuthParams,
ServiceNowReadOptions {
requestSysId?: string
catalogItemSysId?: string
active?: string
}
export interface ServiceNowGetRequestedItemParams extends ServiceNowAuthParams {
sysId?: string
number?: string
fields?: string
displayValue?: string
}
export interface ServiceNowListApprovalsParams extends ServiceNowAuthParams, ServiceNowReadOptions {
approverSysId?: string
state?: string
approvalFor?: string
}
export interface ServiceNowUpdateApprovalParams
extends ServiceNowAuthParams,
ServiceNowWriteOptions {
approvalSysId: string
decision: string
comments?: string
}
export interface ServiceNowSearchCisParams extends ServiceNowAuthParams, ServiceNowReadOptions {
ciClass?: string
name?: string
operationalStatus?: string
}
export interface ServiceNowGetCiParams extends ServiceNowAuthParams {
ciClass: string
sysId: string
}
export interface ServiceNowGetCiResponse extends ToolResponse {
output: {
attributes: Record<string, unknown> | null
/**
* Relation entries are passed through verbatim. The CMDB Instance API
* documents each as `{ sys_id, type: { value, display_value, link },
* target: { value, display_value, link } }`, but the shape is not verified
* at runtime, so the type stays as the raw record it actually is.
*/
inboundRelations: ServiceNowRecord[]
outboundRelations: ServiceNowRecord[]
metadata: {
inboundCount: number
outboundCount: number
}
}
}
export interface ServiceNowListCiRelationshipsParams
extends ServiceNowAuthParams,
ServiceNowReadOptions {
ciSysId: string
direction?: string
}
export interface ServiceNowSearchKnowledgeParams extends ServiceNowAuthParams {
query?: string
filter?: string
knowledgeBaseSysIds?: string
language?: string
fields?: string
limit?: number
offset?: number
}
export interface ServiceNowSearchKnowledgeResponse extends ToolResponse {
output: {
/**
* Articles are passed through verbatim. The Knowledge Management API
* documents each as `{ id, number, title, snippet, link, score, rank,
* fields }`, but only the outer object shape is verified at runtime, so the
* type stays as the raw record it actually is.
*/
articles: ServiceNowRecord[]
metadata: {
recordCount: number
totalCount: number | null
}
}
}
export interface ServiceNowGetKnowledgeArticleParams extends ServiceNowAuthParams {
articleId: string
fields?: string
language?: string
updateView?: boolean | string
}
export interface ServiceNowGetKnowledgeArticleResponse extends ToolResponse {
output: {
sysId: string | null
number: string | null
title: string | null
content: string | null
fields: Record<string, unknown> | null
attachments: Array<Record<string, unknown>>
}
}
export interface ServiceNowFindUserParams extends ServiceNowAuthParams, ServiceNowReadOptions {
email?: string
userName?: string
name?: string
active?: string
}
export interface ServiceNowListGroupMembersParams
extends ServiceNowAuthParams,
ServiceNowReadOptions {
groupSysId?: string
groupName?: string
}
export interface ServiceNowCreateParams extends ServiceNowBaseParams {
fields: Record<string, any>
fields: Record<string, unknown>
}
export interface ServiceNowCreateResponse extends ToolResponse {
@@ -47,7 +458,7 @@ export interface ServiceNowReadResponse extends ToolResponse {
export interface ServiceNowUpdateParams extends ServiceNowBaseParams {
sysId: string
fields: Record<string, any>
fields: Record<string, unknown>
}
export interface ServiceNowUpdateResponse extends ToolResponse {
@@ -87,7 +498,7 @@ export interface ServiceNowAggregateParams extends ServiceNowBaseParams {
export interface ServiceNowAggregateResponse extends ToolResponse {
output: {
result: Record<string, any> | Record<string, any>[] | null
result: Record<string, unknown> | Record<string, unknown>[] | null
count: number | null
metadata: {
grouped: boolean
@@ -104,7 +515,7 @@ export interface ServiceNowAttachment {
table_name?: string
table_sys_id?: string
download_link?: string
[key: string]: any
[key: string]: unknown
}
export interface ServiceNowListAttachmentsParams {
@@ -172,3 +583,12 @@ export type ServiceNowResponse =
| ServiceNowListAttachmentsResponse
| ServiceNowDownloadAttachmentResponse
| ServiceNowUploadAttachmentResponse
| ServiceNowRecordListResponse
| ServiceNowSingleRecordResponse
| ServiceNowChangeTaskListResponse
| ServiceNowGetChangeNextStatesResponse
| ServiceNowListCatalogItemsResponse
| ServiceNowOrderCatalogItemResponse
| ServiceNowGetCiResponse
| ServiceNowSearchKnowledgeResponse
| ServiceNowGetKnowledgeArticleResponse
@@ -0,0 +1,72 @@
import { APPROVAL_STATE, SERVICENOW_TABLES } from '@/tools/servicenow/constants'
import { authParams, recordOutputs, writeParams } from '@/tools/servicenow/params'
import type {
ServiceNowSingleRecordResponse,
ServiceNowUpdateApprovalParams,
} from '@/tools/servicenow/types'
import {
buildFieldPayload,
buildServiceNowHeaders,
buildTableRecordUrl,
transformRecordResponse,
} from '@/tools/servicenow/utils'
import type { ToolConfig } from '@/tools/types'
const ALLOWED_DECISIONS = new Set<string>([APPROVAL_STATE.APPROVED, APPROVAL_STATE.REJECTED])
export const updateApprovalTool: ToolConfig<
ServiceNowUpdateApprovalParams,
ServiceNowSingleRecordResponse
> = {
id: 'servicenow_update_approval',
name: 'Approve or Reject ServiceNow Approval',
description:
'Approve or reject a ServiceNow approval record by setting its state on the Approval [sysapproval_approver] table.',
version: '1.0.0',
params: {
...authParams,
approvalSysId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'sys_id of the approval record on the sysapproval_approver table. Use List ServiceNow Approvals to find it.',
},
decision: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Decision to record: "approved" or "rejected".',
},
comments: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comment to record alongside the decision. This is a journal field.',
},
...writeParams,
},
request: {
url: (params) =>
buildTableRecordUrl({ ...params, sysId: params.approvalSysId }, SERVICENOW_TABLES.APPROVAL),
method: 'PATCH',
headers: (params) => buildServiceNowHeaders(params, { json: true }),
body: (params) => {
const decision = String(params.decision ?? '')
.trim()
.toLowerCase()
if (!ALLOWED_DECISIONS.has(decision)) {
throw new Error(
`decision must be "${APPROVAL_STATE.APPROVED}" or "${APPROVAL_STATE.REJECTED}"`
)
}
return buildFieldPayload({ state: decision, comments: params.comments })
},
},
transformResponse: transformRecordResponse,
outputs: recordOutputs,
}
@@ -0,0 +1,146 @@
import { SERVICENOW_TABLES } from '@/tools/servicenow/constants'
import {
additionalFieldsParam,
authParams,
recordOutputs,
requiredSysIdParam,
writeParams,
} from '@/tools/servicenow/params'
import type {
ServiceNowSingleRecordResponse,
ServiceNowUpdateChangeParams,
} from '@/tools/servicenow/types'
import {
buildFieldPayload,
buildServiceNowHeaders,
buildTableRecordUrl,
transformRecordResponse,
} from '@/tools/servicenow/utils'
import type { ToolConfig } from '@/tools/types'
export const updateChangeRequestTool: ToolConfig<
ServiceNowUpdateChangeParams,
ServiceNowSingleRecordResponse
> = {
id: 'servicenow_update_change_request',
name: 'Update ServiceNow Change Request',
description:
'Update fields on an existing ServiceNow change request. Only the fields you supply are changed. Use Move ServiceNow Change State for state transitions.',
version: '1.0.0',
params: {
...authParams,
...requiredSysIdParam,
shortDescription: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New short description.',
},
description: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New detailed description.',
},
state: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Change state coded value. Base system: -5=New, -4=Assess, -3=Authorize, -2=Scheduled, -1=Implement, 0=Review, 3=Closed, 4=Canceled.',
},
risk: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Risk coded value. The choice list is configured per instance.',
},
impact: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Impact: 1 (High), 2 (Medium), or 3 (Low).',
},
priority: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Priority coded value 1-5 (1 Critical … 5 Planning).',
},
assignmentGroup: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Assignment group (assignment_group) — sys_id of the sys_user_group.',
},
assignedTo: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Assigned to (assigned_to) — sys_id of the sys_user.',
},
startDate: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Planned start date (start_date) as "YYYY-MM-DD HH:mm:ss" in UTC.',
},
endDate: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Planned end date (end_date) as "YYYY-MM-DD HH:mm:ss" in UTC.',
},
closeCode: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Close code: "successful", "successful_issues", or "unsuccessful". Required when closing a change.',
},
closeNotes: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Close notes describing the outcome of the change.',
},
workNotes: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Internal work note to append.',
},
...additionalFieldsParam,
...writeParams,
},
request: {
url: (params) => buildTableRecordUrl(params, SERVICENOW_TABLES.CHANGE_REQUEST),
method: 'PATCH',
headers: (params) => buildServiceNowHeaders(params, { json: true }),
body: (params) =>
buildFieldPayload(
{
short_description: params.shortDescription,
description: params.description,
state: params.state,
risk: params.risk,
impact: params.impact,
priority: params.priority,
assignment_group: params.assignmentGroup,
assigned_to: params.assignedTo,
start_date: params.startDate,
end_date: params.endDate,
close_code: params.closeCode,
close_notes: params.closeNotes,
work_notes: params.workNotes,
},
params.additionalFields
),
},
transformResponse: transformRecordResponse,
outputs: recordOutputs,
}
@@ -0,0 +1,88 @@
import { SERVICENOW_TABLES } from '@/tools/servicenow/constants'
import {
additionalFieldsParam,
authParams,
recordOutputs,
requiredSysIdParam,
writeParams,
} from '@/tools/servicenow/params'
import type {
ServiceNowChangeStateParams,
ServiceNowSingleRecordResponse,
} from '@/tools/servicenow/types'
import {
buildFieldPayload,
buildServiceNowHeaders,
buildTableRecordUrl,
transformRecordResponse,
} from '@/tools/servicenow/utils'
import type { ToolConfig } from '@/tools/types'
export const updateChangeStateTool: ToolConfig<
ServiceNowChangeStateParams,
ServiceNowSingleRecordResponse
> = {
id: 'servicenow_update_change_state',
name: 'Move ServiceNow Change State',
description:
'Move a ServiceNow change request to another state. Base-system change model states are -5=New, -4=Assess, -3=Authorize, -2=Scheduled, -1=Implement, 0=Review, 3=Closed, 4=Canceled. The state machine rejects transitions whose conditions are not met.',
version: '1.0.0',
params: {
...authParams,
...requiredSysIdParam,
state: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Target state coded value: -5 (New), -4 (Assess), -3 (Authorize), -2 (Scheduled), -1 (Implement), 0 (Review), 3 (Closed), or 4 (Canceled).',
},
closeCode: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Close code, required when moving to Closed (3): "successful", "successful_issues", or "unsuccessful".',
},
closeNotes: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Close notes describing the outcome of the change.',
},
workNotes: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Internal work note explaining the transition.',
},
...additionalFieldsParam,
...writeParams,
},
request: {
url: (params) => buildTableRecordUrl(params, SERVICENOW_TABLES.CHANGE_REQUEST),
method: 'PATCH',
headers: (params) => buildServiceNowHeaders(params, { json: true }),
body: (params) => {
const state = String(params.state ?? '').trim()
if (!state) {
throw new Error('A target state is required')
}
return buildFieldPayload(
{
state,
close_code: params.closeCode,
close_notes: params.closeNotes,
work_notes: params.workNotes,
},
params.additionalFields
)
},
},
transformResponse: transformRecordResponse,
outputs: recordOutputs,
}
@@ -0,0 +1,145 @@
import { SERVICENOW_TABLES } from '@/tools/servicenow/constants'
import {
additionalFieldsParam,
authParams,
recordOutputs,
requiredSysIdParam,
writeParams,
} from '@/tools/servicenow/params'
import type {
ServiceNowSingleRecordResponse,
ServiceNowUpdateIncidentParams,
} from '@/tools/servicenow/types'
import {
buildFieldPayload,
buildServiceNowHeaders,
buildTableRecordUrl,
transformRecordResponse,
} from '@/tools/servicenow/utils'
import type { ToolConfig } from '@/tools/types'
export const updateIncidentTool: ToolConfig<
ServiceNowUpdateIncidentParams,
ServiceNowSingleRecordResponse
> = {
id: 'servicenow_update_incident',
name: 'Update ServiceNow Incident',
description:
'Update fields on an existing ServiceNow incident. Only the fields you supply are changed. Reference fields take sys_ids unless input display value is enabled.',
version: '1.0.0',
params: {
...authParams,
...requiredSysIdParam,
shortDescription: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New short description.',
},
description: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New detailed description.',
},
state: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Incident state coded value. Base system: 1=New, 2=In Progress, 3=On Hold, 6=Resolved, 7=Closed, 8=Canceled. Use Resolve or Close ServiceNow Incident for those transitions so the resolution fields are populated.',
},
impact: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Impact: 1 (High), 2 (Medium), or 3 (Low).',
},
urgency: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Urgency: 1 (High), 2 (Medium), or 3 (Low).',
},
priority: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Priority coded value 1-5 (1 Critical … 5 Planning).',
},
category: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Category.',
},
subcategory: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Subcategory.',
},
assignmentGroup: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Assignment group (assignment_group) — sys_id of the sys_user_group.',
},
assignedTo: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Assigned to (assigned_to) — sys_id of the sys_user.',
},
cmdbCi: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Configuration item (cmdb_ci) — sys_id of the affected CI.',
},
workNotes: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Internal work note to append.',
},
comments: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Customer-visible additional comment to append.',
},
...additionalFieldsParam,
...writeParams,
},
request: {
url: (params) => buildTableRecordUrl(params, SERVICENOW_TABLES.INCIDENT),
method: 'PATCH',
headers: (params) => buildServiceNowHeaders(params, { json: true }),
body: (params) =>
buildFieldPayload(
{
short_description: params.shortDescription,
description: params.description,
state: params.state,
impact: params.impact,
urgency: params.urgency,
priority: params.priority,
category: params.category,
subcategory: params.subcategory,
assignment_group: params.assignmentGroup,
assigned_to: params.assignedTo,
cmdb_ci: params.cmdbCi,
work_notes: params.workNotes,
comments: params.comments,
},
params.additionalFields
),
},
transformResponse: transformRecordResponse,
outputs: recordOutputs,
}
+3 -15
View File
@@ -1,6 +1,6 @@
import { createLogger } from '@sim/logger'
import type { ServiceNowUpdateParams, ServiceNowUpdateResponse } from '@/tools/servicenow/types'
import { createBasicAuthHeader } from '@/tools/servicenow/utils'
import { buildServiceNowHeaders, normalizeInstanceUrl } from '@/tools/servicenow/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('ServiceNowUpdateRecordTool')
@@ -52,23 +52,11 @@ export const updateRecordTool: ToolConfig<ServiceNowUpdateParams, ServiceNowUpda
request: {
url: (params) => {
const baseUrl = params.instanceUrl.trim().replace(/\/$/, '')
if (!baseUrl) {
throw new Error('ServiceNow instance URL is required')
}
const baseUrl = normalizeInstanceUrl(params.instanceUrl)
return `${baseUrl}/api/now/table/${params.tableName.trim()}/${params.sysId.trim()}`
},
method: 'PATCH',
headers: (params) => {
if (!params.username || !params.password) {
throw new Error('ServiceNow username and password are required')
}
return {
Authorization: createBasicAuthHeader(params.username, params.password),
'Content-Type': 'application/json',
Accept: 'application/json',
}
},
headers: (params) => buildServiceNowHeaders(params, { json: true }),
body: (params) => {
if (!params.fields || typeof params.fields !== 'object') {
throw new Error('Fields must be a JSON object')
+314
View File
@@ -1,3 +1,13 @@
import { filterUndefined } from '@sim/utils/object'
import { DEFAULT_DISPLAY_VALUE } from '@/tools/servicenow/constants'
import type {
ServiceNowAuthParams,
ServiceNowEnvelope,
ServiceNowReadOptions,
ServiceNowRecord,
ServiceNowWriteOptions,
} from '@/tools/servicenow/types'
/**
* Creates a Basic Authentication header from username and password
* @param username ServiceNow username
@@ -8,3 +18,307 @@ export function createBasicAuthHeader(username: string, password: string): strin
const credentials = Buffer.from(`${username}:${password}`).toString('base64')
return `Basic ${credentials}`
}
/**
* Normalizes a ServiceNow instance URL into an origin without a trailing slash.
* @throws when the instance URL is missing or blank
*/
export function normalizeInstanceUrl(instanceUrl: string | undefined): string {
const baseUrl = (instanceUrl ?? '').trim().replace(/\/$/, '')
if (!baseUrl) {
throw new Error('ServiceNow instance URL is required')
}
return baseUrl
}
/**
* Builds the Basic Auth + Accept headers every ServiceNow REST call needs.
* Pass `json` to also declare a JSON request body.
*/
export function buildServiceNowHeaders(
params: ServiceNowAuthParams,
options: { json?: boolean } = {}
): Record<string, string> {
if (!params.username || !params.password) {
throw new Error('ServiceNow username and password are required')
}
const headers: Record<string, string> = {
Authorization: createBasicAuthHeader(params.username, params.password),
Accept: 'application/json',
}
if (options.json) {
headers['Content-Type'] = 'application/json'
}
return headers
}
/**
* Appends the Table API read parameters (`sysparm_query`, `sysparm_limit`,
* `sysparm_offset`, `sysparm_fields`, `sysparm_display_value`) to a query string.
*
* `displayValue` defaults to `all` on the semantic tools so reference fields
* (`assigned_to`, `assignment_group`, `caller_id`, `cmdb_ci`) come back as
* `{ value: <sys_id>, display_value: <label> }` instead of a bare sys_id.
*/
export function appendReadParams(
searchParams: URLSearchParams,
options: ServiceNowReadOptions & { defaultDisplayValue?: string }
): void {
const { query, limit, offset, fields, displayValue, defaultDisplayValue } = options
if (query) searchParams.append('sysparm_query', query)
if (limit !== undefined && limit !== null) searchParams.append('sysparm_limit', String(limit))
if (offset !== undefined && offset !== null) searchParams.append('sysparm_offset', String(offset))
if (fields) searchParams.append('sysparm_fields', fields)
const resolvedDisplayValue = displayValue || defaultDisplayValue
if (resolvedDisplayValue) {
searchParams.append('sysparm_display_value', resolvedDisplayValue)
}
}
/**
* Appends the Table API write parameters (`sysparm_display_value`,
* `sysparm_input_display_value`, `sysparm_fields`) to a query string.
*
* `sysparm_input_display_value=true` lets callers pass a display name (for
* example `assigned_to: "Beth Anglin"`) and have ServiceNow resolve it to the
* stored sys_id. It defaults to `false`, meaning reference fields must be sys_ids.
*/
export function appendWriteParams(
searchParams: URLSearchParams,
options: ServiceNowWriteOptions & { defaultDisplayValue?: string }
): void {
const { fields, displayValue, inputDisplayValue, defaultDisplayValue } = options
if (fields) searchParams.append('sysparm_fields', fields)
const resolvedDisplayValue = displayValue || defaultDisplayValue
if (resolvedDisplayValue) {
searchParams.append('sysparm_display_value', resolvedDisplayValue)
}
if (inputDisplayValue === true || inputDisplayValue === 'true') {
searchParams.append('sysparm_input_display_value', 'true')
}
}
/**
* Builds the Table API URL for a single record, applying the shared write
* parameters. Used by every semantic update tool.
*/
export function buildTableRecordUrl(
params: ServiceNowAuthParams & ServiceNowWriteOptions & { sysId?: string },
tableName: string
): string {
const baseUrl = normalizeInstanceUrl(params.instanceUrl)
const sysId = params.sysId?.trim()
if (!sysId) {
throw new Error('A record sys_id is required')
}
const searchParams = new URLSearchParams()
appendWriteParams(searchParams, { ...params, defaultDisplayValue: DEFAULT_DISPLAY_VALUE })
return withQueryString(`${baseUrl}/api/now/table/${tableName}/${sysId}`, searchParams)
}
/**
* Joins a base URL with an already-built query string.
*/
export function withQueryString(url: string, searchParams: URLSearchParams): string {
const queryString = searchParams.toString()
return queryString ? `${url}?${queryString}` : url
}
/**
* Parses a ServiceNow JSON response, raising the instance's own error message
* when the request failed. ServiceNow error bodies are `{ error: { message, detail } }`.
*/
export async function parseServiceNowResponse(response: Response): Promise<ServiceNowEnvelope> {
const data = (await response.json()) as ServiceNowEnvelope
if (!response.ok) {
const error = data?.error ?? data
const message =
typeof error === 'string'
? error
: ((error as { message?: string })?.message ?? JSON.stringify(error))
throw new Error(message)
}
return data
}
/**
* Narrows an envelope `result` to a keyed record. Single-record endpoints reply
* with an object; anything else (a collection, a scalar, `null`) becomes `{}` so
* callers can read fields without an unchecked cast.
*/
export function toRecordObject(result: unknown): ServiceNowRecord {
return isRecord(result) ? result : {}
}
/**
* Reads a string field off a ServiceNow record. Returns `null` when the field is
* absent or is not a string, so a tool declaring a `string | null` output cannot
* silently emit an object because the instance returned a different shape.
*/
export function readString(record: ServiceNowRecord, key: string): string | null {
const value = record[key]
return typeof value === 'string' ? value : null
}
/**
* Reads a nested object off a ServiceNow record, returning `null` when the field
* is absent or is not a plain object. Keeps a tool declaring an object-shaped
* output from emitting a scalar because the instance returned a different shape.
*/
export function readRecord(record: ServiceNowRecord, key: string): ServiceNowRecord | null {
const value = record[key]
return isRecord(value) ? value : null
}
/**
* Reads an array of nested objects off a ServiceNow record, dropping any entry
* that is not a plain object. Absent or non-array fields become an empty array.
*/
export function readRecordArray(record: ServiceNowRecord, key: string): ServiceNowRecord[] {
const value = record[key]
return Array.isArray(value) ? value.filter(isRecord) : []
}
/**
* Reads a number field off a nested object on a ServiceNow record, for example
* the `meta.count` the Knowledge search API returns alongside its results.
*/
export function readNestedNumber(
record: ServiceNowRecord,
key: string,
nestedKey: string
): number | null {
const nested = toRecordObject(record[key])[nestedKey]
return typeof nested === 'number' ? nested : null
}
/**
* Normalizes the `{ result: ... }` envelope into an array of records. Collection
* endpoints return an array, single-record endpoints return an object.
*
* Members that are not plain objects are dropped rather than cast. Every tool
* built on this declares an object-shaped `records`/`record` output, so passing
* a `null` or a scalar straight through would report success while handing the
* next block a value its declared contract says cannot occur.
*/
export function toRecordArray(result: unknown): ServiceNowRecord[] {
if (result === null || result === undefined) return []
return (Array.isArray(result) ? result : [result]).filter(isRecord)
}
/** Narrows an unknown value to a plain (non-array, non-null) object. */
export function isRecord(value: unknown): value is ServiceNowRecord {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
}
/**
* Shared `transformResponse` for tools that return a list of records.
*/
export async function transformRecordListResponse(response: Response) {
const data = await parseServiceNowResponse(response)
const records = toRecordArray(data.result)
return {
success: true as const,
output: {
records,
metadata: { recordCount: records.length },
},
}
}
/**
* Shared `transformResponse` for tools that return exactly one record.
*/
export async function transformRecordResponse(response: Response) {
const data = await parseServiceNowResponse(response)
const record = toRecordArray(data.result)[0] ?? null
return {
success: true as const,
output: {
record,
metadata: { recordCount: record ? 1 : 0 },
},
}
}
/**
* Merges the explicit named field params of a semantic tool with the
* `additionalFields` escape hatch, dropping undefined and empty values so a
* blank optional input never overwrites an existing ServiceNow field.
*/
export function buildFieldPayload(
named: Record<string, unknown>,
additionalFields?: Record<string, unknown> | string
): Record<string, unknown> {
const cleaned = filterUndefined(named)
for (const key of Object.keys(cleaned)) {
if (cleaned[key] === '' || cleaned[key] === null) delete cleaned[key]
}
let extra: Record<string, unknown> = {}
if (typeof additionalFields === 'string' && additionalFields.trim()) {
const parsed = JSON.parse(additionalFields)
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new Error('additionalFields must be a JSON object')
}
extra = parsed as Record<string, unknown>
} else if (additionalFields && typeof additionalFields === 'object') {
extra = additionalFields as Record<string, unknown>
}
const payload = { ...cleaned, ...extra }
if (Object.keys(payload).length === 0) {
throw new Error('At least one field must be provided')
}
return payload
}
/**
* ANDs a set of optional `field=value` clauses into a single ServiceNow encoded
* query, dropping blanks. Caller-supplied encoded queries are appended last.
*/
export function buildEncodedQuery(
clauses: Array<[field: string, operator: string, value: unknown]>,
extraQuery?: string
): string | undefined {
const parts: string[] = []
for (const [field, operator, value] of clauses) {
if (value === undefined || value === null || value === '') continue
parts.push(`${field}${operator}${String(value).trim()}`)
}
if (extraQuery?.trim()) parts.push(extraQuery.trim())
return parts.length > 0 ? parts.join('^') : undefined
}
/**
* Builds the `sysparm_query` that identifies a record by sys_id or number, ANDed
* with an optional caller-supplied encoded query.
* @throws when neither identifier is supplied
*/
export function buildIdentifierQuery(
identifiers: { sysId?: string; number?: string },
extraQuery?: string
): string {
const sysId = identifiers.sysId?.trim()
const number = identifiers.number?.trim()
if (!sysId && !number) {
throw new Error('Either a sys_id or a record number is required')
}
const clauses = [sysId ? `sys_id=${sysId}` : `number=${number}`]
if (extraQuery) clauses.push(extraQuery)
return clauses.join('^')
}
+79 -3
View File
@@ -2032,10 +2032,79 @@ function resolveFactorySource(fileContent: string, toolFilePath: string, rootDir
return ''
}
/**
* Reads the module a symbol is imported from, so a spread of a shared const
* declared in a sibling module can be followed. Returns an empty string when
* the symbol is not imported or the module cannot be located on disk.
*/
function readImportedModuleSource(
fileContent: string,
symbol: string,
toolFilePath: string,
rootDir: string
): string {
const importMatch = fileContent.match(
new RegExp(`import\\s*(?:type\\s*)?\\{[^}]*\\b${symbol}\\b[^}]*\\}\\s*from\\s*['"]([^'"]+)['"]`)
)
if (!importMatch) return ''
const specifier = importMatch[1]
const resolved = specifier.startsWith('@/')
? path.join(rootDir, 'apps/sim', specifier.slice(2))
: specifier.startsWith('.')
? path.resolve(path.dirname(toolFilePath), specifier)
: ''
if (!resolved) return ''
for (const candidate of [`${resolved}.ts`, path.join(resolved, 'index.ts')]) {
if (fs.existsSync(candidate)) return fs.readFileSync(candidate, 'utf-8')
}
return ''
}
/**
* Inlines `...sharedConst` spreads inside a `params:` or `outputs:` object body.
*
* Tools increasingly hoist their repeated auth/paging/output declarations into a
* sibling `params.ts`. This generator reads tool *source* rather than importing
* it, so an unresolved spread silently drops every one of those rows from the
* published table. Follows same-file declarations first, then the module the
* symbol is imported from, and recurses so a shared const may itself spread.
*/
function expandSpreadConsts(
objectBody: string,
fileContent: string,
toolFilePath: string,
rootDir: string,
seen: Set<string> = new Set()
): string {
return objectBody.replace(/\.\.\.(\w+)\s*,?/g, (whole, symbol: string) => {
if (seen.has(symbol)) return ''
const declRegex = new RegExp(`(?:export\\s+)?const\\s+${symbol}(?=[^a-zA-Z0-9_])[^=]*=\\s*\\{`)
for (const source of [
fileContent,
readImportedModuleSource(fileContent, symbol, toolFilePath, rootDir),
]) {
if (!source) continue
const declMatch = source.match(declRegex)
if (!declMatch || declMatch.index === undefined) continue
const open = declMatch.index + declMatch[0].length - 1
const close = findMatchingClose(source, open)
if (close === -1) continue
const body = source.substring(open + 1, close - 1)
return `${expandSpreadConsts(body, source, toolFilePath, rootDir, new Set([...seen, symbol]))},`
}
return whole
})
}
function extractToolInfo(
toolName: string,
fileContent: string,
factorySource = ''
factorySource = '',
toolFilePath = '',
rootDir = ''
): {
description: string
params: Array<{ name: string; type: string; required: boolean; description: string }>
@@ -2129,7 +2198,12 @@ function extractToolInfo(
const params: Array<{ name: string; type: string; required: boolean; description: string }> = []
if (toolConfigMatch) {
const paramsContent = toolConfigMatch[1]
const paramsContent = expandSpreadConsts(
toolConfigMatch[1],
fileContent,
toolFilePath,
rootDir
)
const paramBlocksRegex = /(\w+)\s*:\s*{/g
let paramMatch
@@ -2923,7 +2997,9 @@ async function getToolInfo(toolName: string): Promise<{
return extractToolInfo(
toolName,
toolFileContent,
resolveFactorySource(toolFileContent, foundFile, rootDir)
resolveFactorySource(toolFileContent, foundFile, rootDir),
foundFile,
rootDir
)
} catch (error) {
console.error(`Error getting info for tool ${toolName}:`, error)