diff --git a/.agiloft-spec/SPEC.md b/.agiloft-spec/SPEC.md new file mode 100644 index 00000000000..21f60148482 --- /dev/null +++ b/.agiloft-spec/SPEC.md @@ -0,0 +1,233 @@ +# Agiloft REST API — authoritative spec (transcribed from live help.agiloft.com) + +## GLOBAL / API Security +- Every REST call should contain credentials as `login={login}&password={password}`. +- POST-body credentials supported ONLY for: /ewws/EWRead, /ewws/EWSelect, /ewws/EWCreate, + /ewws/EWUpdate, /ewws/EWDelete. ("avoid passing the login or password ... by using POST + instead of GET to pass the parameters in the request body") +- JWT: EWLogin returns a token; "The token can then be used in an Authorization request header, + prefixed by the authentication scheme, instead of including the login and password parameters + in following requests." Default scheme Bearer, expiry 15 min (token_expires_in, max 60). +- Statefulness: pattern is "login, do multiple calls, logout". EWLogout terminates the session + associated with the token passed in the Authorization header. +- DELAYS: every REST call has a delay after completion, default 1 second, global var WSDelay. +- Group must be REST-enabled (Setup > System > Manage Web Services > Groups allowed for REST), + else 403. + +### General error codes (selected) +- 400 "There is no permissions to access this resource" +- 400 "One has to specify $login and $password parameters or authentication token." <-- BOTH auth methods provided +- 400 "Token is expired" +- 400 "One has to specify $login, $password parameters or use $genhotlink/$genproject pair ..." <-- no auth +- 401 "Wrong Authorization data" <-- invalid authorization scheme +- 401 "Token is blocked" / "User is blocked" +- 403 invalid login attempt / "Authentication failed." / "Invalid login/password combination ..." +- 500 "No active session found for current token" +- 400 "Unable to identify KB with name" / "Cannot find specified knowledgebase: " +- 400 "One has to specify $table, $KB, $lang parameters or use $genhotlink/$genproject pair ..." +- 400 "Wrong combination of access token and KB name. No access to data in KB" +- 400 "One has to specify id value." +- 400 "Project has not been found" / "Table has not been found" +- 400 "No value for 'field' parameter specified." +- 403 "not allowed, please check logs" (IP blacklist) +- 405 "HTTP method is not supported by this URL" + +## URL CONVENTIONS +- KB names and table names are CASE SENSITIVE (use Logical Table Name). +- REST style: `/ewws/REST/{kbName}/{table}[/{id}]?$login={login}&password={password}&lang={lang}&...` + (omit /{id} for Create) +- GET/POST style: `/ewws/{operation}?$KB={kbName}&$table={table}&$login={login}&password={password}&lang={lang}&...` + "The parameters of the POST request can be inserted into the body of the request to conceal the user credentials." +- Return values: JavaScript eval() form, all names prefixed `EWREST_`. Empty fields returned as nulls. +- JSON decorator: append `/.json` -> `{"success":true,"message":"","result":{...}}` + optional `err_code_resp=1` for real status codes instead of always 200. +- Async decorator: `/ewws/async/EWCreate?...` or `/ewws/EWCreate/.async?...` (EWCreate, EWUpdate, EWDelete) +- Redirect decorator: `/ewws/redirect/...` with $exiturl and $errorurl +- Decorators chain left to right. + +## OPERATIONS TABLE (endpoint / methods / returns) +| Create | GET/POST | /ewws/EWCreate | ID of new record | +| Read | GET/POST | /ewws/EWRead | encoded record info | +| Update | GET/POST | /ewws/EWUpdate | encoded record info after update | +| Delete | GET/POST/DELETE | /ewws/EWDelete | nothing | +| Select | GET/POST | /ewws/EWSelect | list of record ids + length | +| Login | GET/POST | /ewws/EWLogin | session token, expiration, auth scheme | +| Logout | GET/POST | /ewws/EWLogout | nothing | +| Search | GET/POST | /ewws/EWSearch | saved search + ad hoc | +| Attach | PUT | /ewws/EWAttach | total files attached | +| RemoveAttached | GET/POST | /ewws/EWRemoveAttachment | nothing | +| RetrieveAttached | GET/POST | /ewws/EWRetrieve | attachment | +| Lock | GET/PUT/DELETE | /ewws/EWLock | lock status | +| AttachInfo | GET/POST | /ewws/EWAttachInfo | attachment info | +| Hotlink | POST | /ewws/EWHotlinks | hotlink | +| Table | GET/POST | /ewws/EWTable | all tables and fields | +| Async Status | GET/POST | /ewws/EWAsyncStatus | execution status | +| GetChoiceLineId | GET | /ewws/GetChoiceLineID | internal id for a choice value | +| Action Button | POST | /ewws/EWActionButton | runs an action button | +| Saved Search | GET/POST | /ewws/EWSavedSearch | saved search details | + +NOTE: the operations table lists `/ewws/GetChoiceLineID` and `/ewws/EWActionButton`, but the +detail pages use `/ewws/EWGetChoiceLineId` and `/ewws/async/EWActionButton`. Detail pages carry +working curl examples; the table does not. + +## EWLogin +POST /ewws/EWLogin, Content-Type: plain/text. +Params (CAN BE FILLED TO REQUEST BODY): $KB, $login, $password, $lang (default en). +Response JSON: access_token, refresh_token, expiration_time_unit, expires_in, authentication_scheme + (default "Bearer " — NOTE TRAILING SPACE in examples). +Example: POST https://server/ewws/EWLogin?$login=user&$password=passwd&$KB=Demo&$lang=en +Refresh: POST /ewws/EWLogin with Authorization header + body refresh_token=... +Logout: POST or GET /ewws/EWLogout with Authorization header; params $KB, $lang. +Errors: 400 no refresh_token / wrong refresh_token; 401 Refresh Token is expired; + 403 "User lacks permission log in" + +## EWCreate +GET/POST /ewws/EWCreate. Content-Type application/x-www-form-urlencoded. +Params in URL/body: $KB, $table, $login, $password, $lang + field values. +Returns: EWREST_id='353'; +Async-compatible. +Errors: 400 "Wrong format/value pointed to "; linked-field errors. + +## EWRead +GET/POST /ewws/EWRead. Params: $KB,$table,$login,$password,$lang,id +Alternative to id: $searchSQL=ext_id='a0B2c345' (must match exactly one record). +Returns EWREST_=''; lines including EWREST_id. +Errors: 400 "no data found for id range(s)" + +## EWUpdate +GET/POST /ewws/EWUpdate. Params: ... id=358 + field values. +Alternative to id: $searchSQL. +$operationHints=NOLOCK forces update on a locked record (URL or POST body). +Returns full updated record as EWREST_ lines. +Errors: 400 "One has to specify id or searchSQL value."; constraint violations. + +## EWDelete +GET/POST/DELETE /ewws/EWDelete. Params: ... id=358 & deleteRule=... +deleteRule values: ERROR_IF_DEPENDANTS, APPLY_DELETE_WHERE_POSSIBLE, + DELETE_WHERE_POSSIBLE_OTHERWISE_UNLINK, APPLY_UNLINK, + UNLINK_WHERE_POSSIBLE_OTHERWISE_DELETE, REPLACE_WITH_ANOTHER (needs `subs`). +Returns nothing on success; error message on failure. +Errors: 400 "One has to specify deleteRule, id and substitute values."; + 409 "Operation cannot be done. Record has dependants" etc. + +## EWSelect +GET/POST /ewws/EWSelect. Params: ... where= +Queries in the URL must use %N equivalent operators. +SQL uses dbname column names. Choice values via GetChoiceLineId. +Limit via DB syntax e.g. "limit 0,200". No sort control (use EWSearch). +Returns: EWREST_id_length = '3'; EWREST_id_0 = '150'; ... +Empty: EWREST_id_length = '0'; +Errors: 400 "Error parsing the query "; 500 "Error executing query, please consult logs" + +## EWSearch +GET/POST /ewws/EWSearch. Content-Type x-www-form-urlencoded. +Params: $KB,$table,$login,$password,$lang, search=, query=, + field=, page, limit +Operators: = %3D | != %21%3D | ~= %7E%3D (contains) | && %26%26 | || %7C%7C | < <= > >= +Surround each search value in single quotes; if a field label contains spaces, quote the label too. +Empty fields designated with null. +Returns: EWREST_length = '4'; then EWREST__='value'; +Empty: EWREST_id_length = '0'; +Pagination: page starts 0. limit 0 = ALL records on page 0. + "The REST interface creates a new session and performs an explicit logout for each call. + As such, though pagination is available, the query will always be rebuilt and rerun." +ALREST EQUIVALENT (documented in this page): + curl --location 'http://localhost:8080/ewws/alrest/CLM Template/case/search?lang=en' \ + --data '{ "field": ["id","summary"], "query": "summary=test" }' +Errors: 400 "No search for table "; 400 "No column in table " + +## EWAttach +PUT /ewws/EWAttach. Content-Type multipart/form-data (file in body). +Params: $KB,$table,$login,$password,id,field,fileName +Returns: EWREST_someField.length='1'; (key is `.length`) +Errors: 400 "No value for 'fileName' parameter specified."; forbidden extension etc. + +## EWAttachInfo +GET/POST /ewws/EWAttachInfo. Example uses /.json +URL: /ewws/EWAttachInfo/.json?$KB=..&$table=..&$lang=en&field=attached_file&$login=..&$password=..&id=1 +Returns: {"success":true,"message":"","result":[{"fileName":"..","size":22126,"filePosition":0}]} + +## EWRemoveAttachment +GET/POST /ewws/EWRemoveAttachment. Params: $KB,$table,$login,$password,id,field,filePosition +Returns: the number of attached files remaining in the field. + +## EWRetrieve +GET/POST /ewws/EWRetrieve. Params: $KB,$table,$login,$password,id,field,filePosition +Returns: file content in body. Content-Type = the type used when attaching. + +## EWLock +GET (status) / PUT (lock) / DELETE (unlock) /ewws/EWLock +Params: $KB,$table,$lang,id (+ $login/$password OR OAuth/JWT). `force` (any value) on DELETE only. +Success JSON: {"id":18,"table_id":2788,"locked_by":"admin","lock_status":"LOCKED","lock_expires_in_minutes":25} +Unlock: {"id":18,"table_id":2788,"lock_status":"NO_LOCK"} +lock_status values: NO_LOCK | LOCKED +Failure JSON: {error, error_description}; codes BAD_REQUEST/UNAUTHORIZED/FORBIDDEN/CONFLICT/SERVER_ERROR + +## EWGetChoiceLineId +GET. Detail page URL: /ewws/EWGetChoiceLineId?$KB=..&$login=..&$password=..&$table=case&$lang=en&field=priority&value=High +Returns: EWREST_choiceLineId = '1'; +No match -> HTTP 400. +Errors: 400 "No choice line found for value "; 500 unexpected + +## EWActionButton +POST ONLY. URL: /ewws/async/EWActionButton?$KB=..&$login=..&$password=..&$lang=en&$table=case&name=ab_field&id=82 +Returns: EWREST_id='82'; EWREST_EWCALLBACK_ID='10100_1'; +Compatible with EWAsyncStatus. +Errors: 400 "Wrong value for 'sequence' parameter"; 400 "No information for requested column " + +## EWAsyncStatus +GET or POST /ewws/EWAsyncStatus. Params: $KB,$login,$password,$lang,$table,callback_id +Returns response CODE only (empty body): 200 completed, 201 queued, 202 in progress, + 501 failed, 523 no info for callback id. + +## EWSavedSearch +GET/POST /ewws/EWSavedSearch/.json (JSON is the ONLY output; /.json is MANDATORY) +URL must include the logical table name. For POST the table param may be in the body. +MUST be used with EWLogin or OAuth 2.0 authorization. Never asynchronous. +Example: https://localhost:8080/ewws/EWSavedSearch/.json?$table=contract +Returns: {"success":true,"message":"","result":[{"label":"...","name":"...","id":265185,"description":""}]} + +## EWTable +GET/POST /ewws/EWTable/.json. Requests x-www-form-urlencoded; returns application/json. +MUST be used with EWLogin or OAuth 2.0 authorization. Never asynchronous. +Params: $KB; optional `table` (plain, NOT $table) = logical name e.g. table=contacts; + includelinkedinfo=true; skipColumnsInfo=true +Example: https://localhost:8080/ewws/EWTable/.json?$KB=Demo&includelinkedinfo=true +Returns: {"success":true,"message":"","result":{"tables":[{"label":"WMI Sample","logicalName":"wmi_sample", + "fields":[{"columnLabel":"ID","columnName":"id","columnType":"BIGINT","columnTypeDomain":"swautoincrementfield"}, + {"columnLabel":"Updated By","columnName":"_1794_full_name","columnType":"VARCHAR", + "columnTypeDomain":"swshorttextfield","isLinked":true, + "linkedInfo":[{"linkedTable":"contacts","linkedColumn":"full_name","linkedDao":"_dao3_link0"}], + "textFieldType":"text/plain"}]}]}} +Returns for fields: name, label, type, and required flag. Linked fields return only source table info. +Action buttons, related tables, embedded search results, embedded communications NOT supported. + +## EWUpsert +POST /ewws/EWUpsert. Content-Type application/x-www-form-urlencoded. +Authentication: Required ($login and $password). +Async support: Yes (EWAsyncStatus) via $async. +System params: $KB*, $table*, $login*, $password*, $match*, $lang, $async +Remaining params are record fields. +Matching: no match -> create; one match -> update; multiple -> error. +Example body: $KB=Demo / $table=contacts.employees / $login=admin / $password=qwerty / + $lang=en / $match=_login / _login=jdoe / first_name=John / ... +Returns: EWREST_id='353'; +Status: 200 updated, 201 created, 202 accepted(async), 400, 401, 403, 404, + 409 Conflict (multiple matching records), 500 + +## EWNLPSearch +Content-Types: application/json, application/x-www-form-urlencoded +Params: $KB*, $login*, $password*, $lang, field[]* , nlp_query*, page, limit +Returns same format as REST-Search. +Example return: {"success":true,"message":"","result":[{"company_name":"...","id":31,...}]} + +## DATA ENCODING +- Choice fields: text value as in GUI (`&country=USA`). For EWSelect ad hoc queries use + GetChoiceLineId IDs instead. +- Multi-choice: repeated key/value pairs. +- Elapsed time: "days:hours:minutes:seconds" e.g. "0:1:35:15" +- Linked fields: Query By Example with ':' qualifier (`&company_name=:Agiloft` or + `&company_name=Company:Agiloft`). ':' and '?' in values escaped with backslash. + SQL sub-select form uses '?' qualifier. +- File/image fields: POST with enctype multipart/form-data; form field name = file field name; + `fieldName$overwrite` to replace rather than add. diff --git a/apps/docs/content/docs/en/integrations/agiloft.mdx b/apps/docs/content/docs/en/integrations/agiloft.mdx index 545b4f41efd..3c70095a9a3 100644 --- a/apps/docs/content/docs/en/integrations/agiloft.mdx +++ b/apps/docs/content/docs/en/integrations/agiloft.mdx @@ -40,6 +40,30 @@ Integrate with Agiloft contract lifecycle management to create, read, update, de ## Actions +### Agiloft Async Status + +Check whether an asynchronous Agiloft call, such as a run action button, has completed. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `instanceUrl` | string | Yes | Agiloft instance URL \(e.g., https://mycompany.agiloft.com\) | +| `knowledgeBase` | string | Yes | Knowledge base name | +| `login` | string | Yes | Agiloft username | +| `password` | string | Yes | Agiloft password | +| `table` | string | Yes | Table the asynchronous call was made against | +| `callbackId` | string | Yes | Callback ID returned by the asynchronous call, e.g. from Run Action Button | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `callbackId` | string | Callback ID that was checked | +| `statusCode` | number | Raw status code Agiloft returned | +| `status` | string | completed, queued, in_progress, failed, or unknown_callback | +| `complete` | boolean | True when the operation has finished, whether it succeeded or failed | + ### Agiloft Attach File Attach a file to a field in an Agiloft record. @@ -55,8 +79,9 @@ Attach a file to a field in an Agiloft record. | `table` | string | Yes | Table name \(e.g., "contracts"\) | | `recordId` | string | Yes | ID of the record to attach the file to | | `fieldName` | string | Yes | Name of the attachment field | -| `file` | file | No | File to attach | +| `file` | file | Yes | File to attach | | `fileName` | string | No | Name to assign to the file \(defaults to original file name\) | +| `overwrite` | boolean | No | Replace the contents of the field instead of adding another file to it | #### Output @@ -129,7 +154,8 @@ Delete a record from an Agiloft table. | `password` | string | Yes | Agiloft password | | `table` | string | Yes | Table name \(e.g., "contracts", "contacts.employees"\) | | `recordId` | string | Yes | ID of the record to delete | -| `deleteRule` | string | No | How to treat records that depend on this one: ERROR_IF_DEPENDANTS \(default — fails rather than cascading\), APPLY_DELETE_WHERE_POSSIBLE, DELETE_WHERE_POSSIBLE_OTHERWISE_UNLINK, APPLY_UNLINK, or UNLINK_WHERE_POSSIBLE_OTHERWISE_DELETE | +| `substituteIds` | string | No | Comma-separated IDs of records that adopt the dependants of the deleted record. Read only when the delete rule is REPLACE_WITH_ANOTHER. | +| `deleteRule` | string | No | How to treat records that depend on this one: ERROR_IF_DEPENDANTS \(default — fails rather than cascading\), APPLY_DELETE_WHERE_POSSIBLE, DELETE_WHERE_POSSIBLE_OTHERWISE_UNLINK, APPLY_UNLINK, UNLINK_WHERE_POSSIBLE_OTHERWISE_DELETE, or REPLACE_WITH_ANOTHER | #### Output @@ -160,6 +186,42 @@ Resolve the internal numeric ID of a choice-list value, for use in EWSelect WHER | --------- | ---- | ----------- | | `choiceLineId` | number | Internal numeric line ID of the choice value | +### Agiloft List Tables + +List the tables and fields in an Agiloft knowledge base, to discover the logical names other operations need. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `instanceUrl` | string | Yes | Agiloft instance URL \(e.g., https://mycompany.agiloft.com\) | +| `knowledgeBase` | string | Yes | Knowledge base name | +| `login` | string | Yes | Agiloft username | +| `password` | string | Yes | Agiloft password | +| `table` | string | No | Logical name of a single table to describe \(e.g., "contacts"\). Leave empty to list every table in the knowledge base. | +| `includeLinkedInfo` | boolean | No | Include the source table and column behind each linked field | +| `skipColumnsInfo` | boolean | No | Return table names only, omitting field details, for a much smaller response | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `tables` | array | Tables in the knowledge base with their fields | +| ↳ `label` | string | Display name of the table | +| ↳ `logicalName` | string | Logical table name, as other Agiloft operations expect it | +| ↳ `fields` | array | Fields on the table | +| ↳ `columnName` | string | Logical field name | +| ↳ `columnLabel` | string | Display label | +| ↳ `columnType` | string | SQL column type | +| ↳ `columnTypeDomain` | string | Agiloft field type | +| ↳ `required` | boolean | Whether the field is mandatory | +| ↳ `isLinked` | boolean | Whether the field is a linked field | +| ↳ `linkedInfo` | array | Source table and column, when linked-field details were requested | +| ↳ `linkedTable` | string | Source table | +| ↳ `linkedColumn` | string | Source column | +| ↳ `textFieldType` | string | Content type for text fields, e.g. text/plain | +| `totalCount` | number | Number of tables returned | + ### Agiloft Lock Record Lock, unlock, or check the lock status of an Agiloft record. @@ -175,17 +237,43 @@ Lock, unlock, or check the lock status of an Agiloft record. | `table` | string | Yes | Table name \(e.g., "contracts"\) | | `recordId` | string | Yes | ID of the record to lock, unlock, or check | | `lockAction` | string | Yes | Action to perform: "lock", "unlock", or "check" | -| `force` | boolean | No | Unlock only: release a lock held by another user. Requires membership in the admin group. | +| `force` | boolean | No | Unlock only: release a lock held by another user. | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | | `id` | string | Record ID | +| `tableId` | number | Numeric system identifier of the table holding the record | | `lockStatus` | string | Lock status: "LOCKED" when the record is held, "NO_LOCK" when it is free | | `lockedBy` | string | Username of the user who locked the record | | `lockExpiresInMinutes` | number | Minutes until the lock expires | +### Agiloft Natural Language Search + +Search Agiloft records by describing what you want in plain language, such as "active NDAs submitted last month". + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `instanceUrl` | string | Yes | Agiloft instance URL \(e.g., https://mycompany.agiloft.com\) | +| `knowledgeBase` | string | Yes | Knowledge base name | +| `login` | string | Yes | Agiloft username | +| `password` | string | Yes | Agiloft password | +| `nlpQuery` | string | Yes | The request in plain language, e.g. "Show me open, high-priority contracts". Structured field filters are not accepted — use Search Records for those. | +| `fields` | string | Yes | Comma-separated field names to return, e.g. "id, contract_title1, company_name" | +| `page` | string | No | Page number, starting from 0 | +| `limit` | string | No | Records per page | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `records` | json | Matching records with the requested field values | +| `totalCount` | number | Number of records in this response | +| `truncated` | boolean | True when more records were returned upstream than this call reports | + ### Agiloft Read Record Read a record by ID from an Agiloft table. @@ -280,29 +368,30 @@ Run an action button on an Agiloft record, such as an approval or send-for-signa | `recordId` | string | ID of the record the action button was run on | | `callbackId` | string | Callback identifier for the asynchronous run, which Agiloft returns as EWCALLBACK_ID | -### saved_search - - -### Agiloft Saved Search (retired) +### Agiloft Saved Search -Retired. Agiloft does not document an endpoint for listing saved searches — use the Search Records operation and set its Saved Search field instead. +List the saved searches defined for an Agiloft table. #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `instanceUrl` | string | No | Agiloft instance URL | -| `knowledgeBase` | string | No | Knowledge base name | -| `login` | string | No | Agiloft username | -| `password` | string | No | Agiloft password | -| `table` | string | No | Table name | -| `output` | string | No | No description | +| `instanceUrl` | string | Yes | Agiloft instance URL \(e.g., https://mycompany.agiloft.com\) | +| `knowledgeBase` | string | Yes | Knowledge base name | +| `login` | string | Yes | Agiloft username | +| `password` | string | Yes | Agiloft password | +| `table` | string | Yes | Logical table name to list saved searches for \(e.g., "contract"\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `searches` | array | Always empty; this operation is retired | +| `searches` | array | Saved searches defined on the table | +| ↳ `name` | string | Internal saved search name | +| ↳ `label` | string | Display label, as used by Search Records | +| ↳ `id` | number | Saved search identifier in the Agiloft database | +| ↳ `description` | string | Saved search description | +| `totalCount` | number | Number of saved searches returned | ### Agiloft Search Records @@ -327,8 +416,9 @@ Search for records in an Agiloft table using a query. | Parameter | Type | Description | | --------- | ---- | ----------- | +| `truncated` | boolean | True when more records were returned upstream than this call reports | | `records` | json | Array of matching records with their field values | -| `totalCount` | number | Number of records reported by EWSearch. When paginating this is the count for the current page, not the whole result set. | +| `totalCount` | number | Number of records in this response. Not a total match count — compare with `truncated`. | | `page` | number | Page number that was requested \(0-based\) | | `limit` | number | Page size that was requested; 0 when no limit was sent and Agiloft chose one | @@ -351,8 +441,9 @@ Select record IDs matching a SQL WHERE clause from an Agiloft table. | Parameter | Type | Description | | --------- | ---- | ----------- | +| `truncated` | boolean | True when more IDs matched than this call reports | | `recordIds` | array | Array of record IDs matching the query | -| `totalCount` | number | Total number of matching records | +| `totalCount` | number | Number of IDs in this response — compare with `truncated` | ### Agiloft Update Record @@ -377,4 +468,29 @@ Update an existing record in an Agiloft table. | `id` | string | ID of the updated record | | `fields` | json | Updated field values of the record | +### Agiloft Upsert Record + +Create an Agiloft record, or update it when a record already matches the given fields. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `instanceUrl` | string | Yes | Agiloft instance URL \(e.g., https://mycompany.agiloft.com\) | +| `knowledgeBase` | string | Yes | Knowledge base name | +| `login` | string | Yes | Agiloft username | +| `password` | string | Yes | Agiloft password | +| `table` | string | Yes | Table name \(e.g., "contracts", "contacts.employees"\) | +| `match` | string | Yes | Field used to find an existing record \(e.g., "ext_id"\). Pick something that identifies a record uniquely — if more than one record matches, Agiloft writes nothing and returns a conflict. | +| `async` | boolean | No | Queue the write instead of waiting for it. Returns a callback ID instead of a record ID; pass that to Async Status to poll the result. | +| `data` | string | Yes | Field values as a JSON object. On create these populate the new record; on update only the supplied fields change. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | ID of the created or updated record | +| `created` | boolean | True when a new record was created, false when an existing one was updated | +| `callbackId` | string | Returned for a queued upsert; pass it to Async Status to poll the result | + diff --git a/apps/sim/app/api/tools/agiloft/async_status/route.ts b/apps/sim/app/api/tools/agiloft/async_status/route.ts new file mode 100644 index 00000000000..f034d4a0bf6 --- /dev/null +++ b/apps/sim/app/api/tools/agiloft/async_status/route.ts @@ -0,0 +1,96 @@ +import { createLogger } from '@sim/logger' +import { toError } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { agiloftAsyncStatusContract } from '@/lib/api/contracts/tools/agiloft' +import { getValidationErrorMessage, parseRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import type { AgiloftAsyncStatusResponse } from '@/tools/agiloft/types' +import { AGILOFT_ASYNC_STATUS, buildAsyncStatusUrl } from '@/tools/agiloft/utils' +import { executeEwRequest } from '@/tools/agiloft/utils.server' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('AgiloftAsyncStatusAPI') + +export const POST = withRouteHandler(async (request: NextRequest) => { + const requestId = generateRequestId() + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + + if (!authResult.success || !authResult.userId) { + logger.warn(`[${requestId}] Unauthorized Agiloft async_status attempt: ${authResult.error}`) + return NextResponse.json( + { success: false, error: authResult.error || 'Authentication required' }, + { status: 401 } + ) + } + + const parsed = await parseRequest( + agiloftAsyncStatusContract, + request, + {}, + { + validationErrorResponse: (error) => { + logger.warn(`[${requestId}] Invalid request data`, { errors: error.issues }) + return NextResponse.json( + { + success: false, + error: getValidationErrorMessage(error, 'Invalid request data'), + details: error.issues, + }, + { status: 400 } + ) + }, + } + ) + if (!parsed.success) return parsed.response + const params = parsed.data.body + + const result = await executeEwRequest( + params, + (base) => ({ url: buildAsyncStatusUrl(base, params), method: 'GET' }), + async (response) => { + /** + * EWAsyncStatus communicates entirely through the status code and + * returns an empty body, so the code is the result rather than an + * error signal — 501 means the async operation failed, not that the + * status check did. + */ + const known = AGILOFT_ASYNC_STATUS[response.status] + + if (!known) { + const body = await response.text() + return { + success: false, + output: { + callbackId: params.callbackId.trim(), + statusCode: response.status, + status: 'unrecognized', + complete: false, + }, + error: `Agiloft returned an unrecognized async status ${response.status}: ${body.trim() || '(empty response)'}`, + } + } + + return { + success: true, + output: { + callbackId: params.callbackId.trim(), + statusCode: response.status, + status: known.status, + complete: known.complete, + }, + } + } + ) + + return NextResponse.json(result) + } catch (error) { + logger.error(`[${requestId}] Error checking Agiloft async status:`, error) + + return NextResponse.json({ success: false, error: toError(error).message }, { status: 500 }) + } +}) diff --git a/apps/sim/app/api/tools/agiloft/attach/route.test.ts b/apps/sim/app/api/tools/agiloft/attach/route.test.ts index 3f1ac2d1fae..a5db37f2079 100644 --- a/apps/sim/app/api/tools/agiloft/attach/route.test.ts +++ b/apps/sim/app/api/tools/agiloft/attach/route.test.ts @@ -9,6 +9,9 @@ import { } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' +/** Obvious non-secret so credential scanners do not flag these fixtures. */ +const PLACEHOLDER_PASSWORD = 'not-a-real-password' + const { mockProcessFilesToUserFiles, mockDownloadFileFromStorage, mockAssertToolFileAccess } = vi.hoisted(() => ({ mockProcessFilesToUserFiles: vi.fn(), @@ -35,7 +38,7 @@ const baseBody = { instanceUrl: 'https://example.agiloft.com', knowledgeBase: 'demo', login: 'admin', - password: 'secret', + password: PLACEHOLDER_PASSWORD, table: 'contracts', recordId: '42', fieldName: 'attachments', @@ -55,7 +58,7 @@ function mockSecureFetchResponse(body: { statusText: '', headers: new Headers(), body: null, - text: async () => body.text ?? '', + text: async () => body.text ?? JSON.stringify(body.json ?? {}), json: async () => body.json ?? {}, arrayBuffer: async () => new ArrayBuffer(0), } @@ -109,11 +112,11 @@ describe('POST /api/tools/agiloft/attach', () => { expect(inputValidationMockFns.mockSecureFetchWithPinnedIP).not.toHaveBeenCalled() }) - it('pins the resolved IP for login, attach, and logout (TOCTOU fix)', async () => { - inputValidationMockFns.mockSecureFetchWithPinnedIP - .mockResolvedValueOnce(mockSecureFetchResponse({ json: { access_token: 'tok-att' } })) - .mockResolvedValueOnce(mockSecureFetchResponse({ text: '1' })) - .mockResolvedValueOnce(mockSecureFetchResponse({})) + it('attaches with inline credentials on the pinned IP, with no login round trip', async () => { + /** Documented response: EWREST_.length='1'; */ + inputValidationMockFns.mockSecureFetchWithPinnedIP.mockResolvedValueOnce( + mockSecureFetchResponse({ text: "EWREST_attachments.length='1';" }) + ) const response = await POST(createMockRequest('POST', baseBody)) expect(response.status).toBe(200) @@ -125,21 +128,17 @@ describe('POST /api/tools/agiloft/attach', () => { expect(data.output.fileName).toBe('file.txt') const calls = inputValidationMockFns.mockSecureFetchWithPinnedIP.mock.calls - expect(calls).toHaveLength(3) - for (const call of calls) { - expect(call[1]).toBe(PINNED_IP) - } + expect(calls).toHaveLength(1) + expect(calls[0][1]).toBe(PINNED_IP) - expect(calls[0][0]).toContain('https://example.agiloft.com/ewws/EWLogin') - expect(calls[1][0]).toContain('https://example.agiloft.com/ewws/EWAttach') - expect(calls[1][2]).toMatchObject({ + expect(calls[0][0]).toContain('https://example.agiloft.com/ewws/EWAttach') + expect(calls[0][0]).toContain('&$login=admin') + expect(calls[0][2]).toMatchObject({ method: 'PUT', - headers: { - Authorization: 'Bearer tok-att', - 'Content-Type': 'application/octet-stream', - }, + headers: { 'Content-Type': 'application/octet-stream' }, }) - expect(calls[2][0]).toContain('https://example.agiloft.com/ewws/EWLogout') + // A bearer token on this surface is rejected; it must not be sent. + expect(calls[0][2].headers.Authorization).toBeUndefined() // DNS only resolved once. expect(inputValidationMockFns.mockValidateUrlWithDNS).toHaveBeenCalledTimes(1) diff --git a/apps/sim/app/api/tools/agiloft/attach/route.ts b/apps/sim/app/api/tools/agiloft/attach/route.ts index 64da0e6acbb..014e34eb271 100644 --- a/apps/sim/app/api/tools/agiloft/attach/route.ts +++ b/apps/sim/app/api/tools/agiloft/attach/route.ts @@ -12,12 +12,9 @@ import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' import { downloadServableFileFromStorage } from '@/lib/uploads/utils/file-utils.server' import { docNotReadyResponse } from '@/lib/uploads/utils/servable-file-response' import { assertToolFileAccess } from '@/app/api/files/authorization' -import { buildAttachFileUrl } from '@/tools/agiloft/utils' -import { - agiloftLoginPinned, - agiloftLogoutPinned, - resolveAgiloftInstance, -} from '@/tools/agiloft/utils.server' +import { parseEwRest } from '@/tools/agiloft/ewrest' +import { buildAttachFileUrl, describeAgiloftError } from '@/tools/agiloft/utils' +import { resolveAgiloftInstance } from '@/tools/agiloft/utils.server' export const dynamic = 'force-dynamic' @@ -99,10 +96,14 @@ export const POST = withRouteHandler(async (request: NextRequest) => { return NextResponse.json({ success: false, error: toError(error).message }, { status: 400 }) } - const token = await agiloftLoginPinned(data, resolvedIP) const base = data.instanceUrl.replace(/\/$/, '') - try { + { + /** + * EWAttach lives on the legacy surface, which authenticates from the + * inline credentials in the URL and rejects a bearer token — so there is + * no login/logout pair here. + */ const url = buildAttachFileUrl(base, data, resolvedFileName) logger.info(`[${requestId}] Uploading file to Agiloft: ${resolvedFileName}`) @@ -111,7 +112,6 @@ export const POST = withRouteHandler(async (request: NextRequest) => { method: 'PUT', headers: { 'Content-Type': 'application/octet-stream', - Authorization: `Bearer ${token}`, }, body: new Uint8Array(fileBuffer), }) @@ -122,19 +122,40 @@ export const POST = withRouteHandler(async (request: NextRequest) => { `[${requestId}] Agiloft attach error: ${agiloftResponse.status} - ${errorText}` ) return NextResponse.json( - { success: false, error: `Agiloft error: ${agiloftResponse.status} - ${errorText}` }, + { + success: false, + error: `Agiloft error ${agiloftResponse.status}: ${describeAgiloftError(errorText)}`, + }, { status: agiloftResponse.status } ) } - let totalAttachments = 0 + /** + * EWAttach reports the new file count against the field it wrote to, as + * EWREST_.length='1'; — the key is field-specific, so the + * single returned assignment is read rather than a fixed key. + */ const responseText = await agiloftResponse.text() - try { - const responseData = JSON.parse(responseText) - const result = responseData.result ?? responseData - totalAttachments = typeof result === 'number' ? result : (result.count ?? result.total ?? 1) - } catch { - totalAttachments = Number(responseText) || 1 + const assignments = parseEwRest(responseText) + const countRaw = + assignments.get(`${data.fieldName.trim()}.length`) ?? [...assignments.values()][0] + const totalAttachments = Number(countRaw) + + /** + * A 200 with no parsable count is not a confirmed write, so it fails + * closed rather than reporting success with zero attachments. + */ + if (!Number.isFinite(totalAttachments)) { + logger.error(`[${requestId}] Agiloft attach returned an unrecognised body`, { + body: responseText.slice(0, 200), + }) + return NextResponse.json( + { + success: false, + error: `Agiloft did not confirm the attachment: ${describeAgiloftError(responseText) || '(empty response)'}`, + }, + { status: 502 } + ) } logger.info( @@ -150,8 +171,6 @@ export const POST = withRouteHandler(async (request: NextRequest) => { totalAttachments, }, }) - } finally { - await agiloftLogoutPinned(data.instanceUrl, data.knowledgeBase, token, resolvedIP) } } catch (error) { logger.error(`[${requestId}] Error attaching file to Agiloft:`, error) diff --git a/apps/sim/app/api/tools/agiloft/attachment_info/route.ts b/apps/sim/app/api/tools/agiloft/attachment_info/route.ts index d1eb3a8cbbc..3404758ad1b 100644 --- a/apps/sim/app/api/tools/agiloft/attachment_info/route.ts +++ b/apps/sim/app/api/tools/agiloft/attachment_info/route.ts @@ -7,8 +7,8 @@ import { checkInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import type { AgiloftAttachmentInfoResponse } from '@/tools/agiloft/types' -import { buildAttachmentInfoUrl } from '@/tools/agiloft/utils' -import { executeAgiloftRequest } from '@/tools/agiloft/utils.server' +import { buildAttachmentInfoUrl, describeAgiloftError } from '@/tools/agiloft/utils' +import { executeEwRequest } from '@/tools/agiloft/utils.server' export const dynamic = 'force-dynamic' @@ -51,7 +51,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { if (!parsed.success) return parsed.response const params = parsed.data.body - const result = await executeAgiloftRequest( + const result = await executeEwRequest( params, (base) => ({ url: buildAttachmentInfoUrl(base, params), @@ -63,7 +63,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { return { success: false, output: { attachments: [], totalCount: 0 }, - error: `Agiloft error: ${response.status} - ${errorText}`, + error: `Agiloft error ${response.status}: ${describeAgiloftError(errorText)}`, } } diff --git a/apps/sim/app/api/tools/agiloft/create_record/route.test.ts b/apps/sim/app/api/tools/agiloft/create_record/route.test.ts index 34fbace33cf..4e8f8e98718 100644 --- a/apps/sim/app/api/tools/agiloft/create_record/route.test.ts +++ b/apps/sim/app/api/tools/agiloft/create_record/route.test.ts @@ -12,33 +12,41 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' vi.mock('@/lib/core/security/input-validation.server', () => inputValidationMock) import { POST } from '@/app/api/tools/agiloft/create_record/route' +import { POST as READ } from '@/app/api/tools/agiloft/read_record/route' import { POST as SEARCH } from '@/app/api/tools/agiloft/search_records/route' -import { POST as SELECT } from '@/app/api/tools/agiloft/select_records/route' + +/** Obvious non-secret so credential scanners do not flag these fixtures. */ +const PLACEHOLDER_PASSWORD = 'not-a-real-password' const PINNED_IP = '93.184.216.34' const baseBody = { instanceUrl: 'https://example.agiloft.com', - knowledgeBase: 'Demo', - login: 'admin', - password: 'secret', - table: 'contacts.employees', - data: JSON.stringify({ first_name: 'John', last_name: 'Doe' }), + knowledgeBase: 'Russell Investments', + login: 'svc.user', + password: PLACEHOLDER_PASSWORD, + table: 'contract', } -function mockSecureFetchResponse(body: { ok?: boolean; json?: unknown; text?: string }) { +function res(body: { ok?: boolean; status?: number; json?: unknown; text?: string }) { + const text = body.text ?? JSON.stringify(body.json ?? {}) return { ok: body.ok ?? true, - status: body.ok === false ? 400 : 200, + status: body.status ?? 200, statusText: '', headers: new Headers(), body: null, - text: async () => body.text ?? '', - json: async () => body.json ?? {}, + text: async () => text, + json: async () => JSON.parse(text), arrayBuffer: async () => new ArrayBuffer(0), } } +/** Login envelope Agiloft returns — note the trailing space on the scheme. */ +const LOGIN_OK = res({ + json: { access_token: 'tok-123', authentication_scheme: 'Bearer ' }, +}) + beforeEach(() => { vi.clearAllMocks() hybridAuthMockFns.mockCheckInternalAuth.mockResolvedValue({ @@ -53,124 +61,571 @@ beforeEach(() => { }) }) -describe('POST /api/tools/agiloft/create_record', () => { - it("reads the record ID out of EWCreate's EWREST_id assignment", async () => { - inputValidationMockFns.mockSecureFetchWithPinnedIP - .mockResolvedValueOnce(mockSecureFetchResponse({ json: { access_token: 'tok-c' } })) - .mockResolvedValueOnce(mockSecureFetchResponse({ text: "EWREST_id='353';" })) - .mockResolvedValueOnce(mockSecureFetchResponse({})) +function arrange(operationResponse: ReturnType) { + inputValidationMockFns.mockSecureFetchWithPinnedIP + .mockResolvedValueOnce(LOGIN_OK) + .mockResolvedValueOnce(operationResponse) + .mockResolvedValueOnce(res({})) +} - const response = await POST(createMockRequest('POST', baseBody)) - const data = (await response.json()) as { - success: boolean - output: { id: string | null } - } +describe('EWLogin', () => { + it('sends $table and $lang alongside $KB in a form body, which the live server requires', async () => { + arrange(res({ json: { success: true, result: { id: 6342 } } })) - expect(data.success).toBe(true) - expect(data.output.id).toBe('353') + await POST(createMockRequest('POST', { ...baseBody, data: '{"a":"b"}' })) + + const [url, ip, init] = inputValidationMockFns.mockSecureFetchWithPinnedIP.mock.calls[0] + expect(url).toBe('https://example.agiloft.com/ewws/EWLogin') + expect(ip).toBe(PINNED_IP) + expect(init.method).toBe('POST') + expect(init.headers['Content-Type']).toBe('application/x-www-form-urlencoded') - const operationCall = inputValidationMockFns.mockSecureFetchWithPinnedIP.mock.calls[1] - expect(operationCall[0]).toContain('/ewws/EWCreate?') - expect(operationCall[0]).toContain('&first_name=John') - expect(operationCall[2]).toMatchObject({ method: 'POST' }) + const sent = new URLSearchParams(init.body as string) + expect(sent.get('$KB')).toBe('Russell Investments') + expect(sent.get('$table')).toBe('contract') + expect(sent.get('$lang')).toBe('en') + expect(sent.get('$login')).toBe('svc.user') + expect(sent.get('$password')).toBe(PLACEHOLDER_PASSWORD) + // Credentials must not leak into the URL. + expect(url).not.toContain(PLACEHOLDER_PASSWORD) }) - it('fails loudly when Agiloft answers 200 with something that is not an EWREST body', async () => { - inputValidationMockFns.mockSecureFetchWithPinnedIP - .mockResolvedValueOnce(mockSecureFetchResponse({ json: { access_token: 'tok-c' } })) - .mockResolvedValueOnce( - mockSecureFetchResponse({ text: 'Error executing query, please consult logs' }) - ) - .mockResolvedValueOnce(mockSecureFetchResponse({})) + it('trims the trailing space Agiloft puts on authentication_scheme', async () => { + arrange(res({ json: { success: true, result: { id: 6342 } } })) + + await POST(createMockRequest('POST', { ...baseBody, data: '{"a":"b"}' })) - const response = await POST(createMockRequest('POST', baseBody)) + const [, , init] = inputValidationMockFns.mockSecureFetchWithPinnedIP.mock.calls[1] + expect(init.headers.Authorization).toBe('Bearer tok-123') + }) + + it('surfaces the live "One has to specify" refusal instead of a generic failure', async () => { + inputValidationMockFns.mockSecureFetchWithPinnedIP.mockResolvedValueOnce( + res({ + ok: false, + status: 400, + text: 'EWWrongDataException has occurred: One has to specify $table, $KB, $lang parameters', + }) + ) + + const response = await POST(createMockRequest('POST', { ...baseBody, data: '{"a":"b"}' })) const data = (await response.json()) as { success: boolean; error?: string } expect(data.success).toBe(false) - expect(data.error).toContain('did not return a record ID') + expect(data.error).toContain('One has to specify $table, $KB, $lang') }) +}) + +describe('alrest envelope handling', () => { + it('targets /ewws/alrest/{KB} for record creation', async () => { + arrange(res({ json: { success: true, result: { id: 6342, contract_title1: 'X' } } })) - it('rejects a data payload that is not a JSON object', async () => { const response = await POST( - createMockRequest('POST', { ...baseBody, data: '["not", "an", "object"]' }) + createMockRequest('POST', { ...baseBody, data: '{"contract_title1":"X"}' }) + ) + const data = (await response.json()) as { success: boolean; output: { id: string | null } } + + const [url] = inputValidationMockFns.mockSecureFetchWithPinnedIP.mock.calls[1] + expect(url).toBe( + 'https://example.agiloft.com/ewws/alrest/Russell%20Investments/contract?lang=en' + ) + expect(data.output.id).toBe('6342') + }) + + it('treats HTTP 200 with success:false as a failure, not a successful create', async () => { + arrange( + res({ + json: { success: false, errors: [{ message: 'Field contract_title1 is required' }] }, + }) ) + + const response = await POST(createMockRequest('POST', { ...baseBody, data: '{"a":"b"}' })) const data = (await response.json()) as { success: boolean; error?: string } expect(data.success).toBe(false) - expect(data.error).toContain('must be a JSON object') - expect(inputValidationMockFns.mockSecureFetchWithPinnedIP).not.toHaveBeenCalled() + expect(data.error).toContain('Field contract_title1 is required') }) }) -describe('empty EWREST bodies on search and select', () => { - const listBase = { - instanceUrl: 'https://example.agiloft.com', - knowledgeBase: 'Demo', - login: 'admin', - password: 'secret', - table: 'helpdesk_case', - } +describe('field projection', () => { + it('reads through search when fields are named, so a 184KB record is not pulled whole', async () => { + arrange(res({ json: { success: true, result: [{ id: 6342, contract_title1: 'X' }] } })) - function arrange(text: string) { - inputValidationMockFns.mockSecureFetchWithPinnedIP - .mockResolvedValueOnce(mockSecureFetchResponse({ json: { access_token: 'tok' } })) - .mockResolvedValueOnce(mockSecureFetchResponse({ text })) - .mockResolvedValueOnce(mockSecureFetchResponse({})) - } + await READ( + createMockRequest('POST', { + ...baseBody, + recordId: '6342', + fields: 'contract_title1, company_name', + }) + ) - it('treats a plain-text refusal from EWSearch as a failure, not an empty result', async () => { - arrange('Error executing query, please consult logs') + const [url, , init] = inputValidationMockFns.mockSecureFetchWithPinnedIP.mock.calls[1] + expect(url).toContain('/contract/search?lang=en') + expect(JSON.parse(init.body as string)).toEqual({ + field: ['id', 'contract_title1', 'company_name'], + query: 'id=6342', + }) + }) + + it('fetches the record directly when no projection was asked for', async () => { + arrange(res({ json: { success: true, result: { id: 6342 } } })) + + await READ(createMockRequest('POST', { ...baseBody, recordId: '6342' })) + + const [url, , init] = inputValidationMockFns.mockSecureFetchWithPinnedIP.mock.calls[1] + expect(url).toContain('/contract/6342?lang=en') + expect(init.method).toBe('GET') + }) +}) + +describe('optional inputs arriving as null', () => { + it('accepts a null page instead of rejecting the call before it is made', async () => { + arrange(res({ json: { success: true, result: [] } })) const response = await SEARCH( - createMockRequest('POST', { ...listBase, query: "priority='High'" }) + createMockRequest('POST', { + ...baseBody, + query: "status='Active'", + page: null, + limit: null, + fields: null, + search: null, + }) ) const data = (await response.json()) as { success: boolean; error?: string } - expect(data.success).toBe(false) - expect(data.error).toContain('did not return search results') + expect(data.success).toBe(true) + expect(data.error).toBeUndefined() + }) + + it('omits unset optional fields from the search body rather than sending null', async () => { + arrange(res({ json: { success: true, result: [] } })) + + await SEARCH(createMockRequest('POST', { ...baseBody, query: "status='Active'", page: null })) + + const [, , init] = inputValidationMockFns.mockSecureFetchWithPinnedIP.mock.calls[1] + expect(JSON.parse(init.body as string)).toEqual({ query: "status='Active'" }) }) +}) - it('still reports a genuinely empty EWSearch result as a success', async () => { - arrange("EWREST_id_length = '0';") +describe('search result ceiling', () => { + it('caps the returned records, since alrest honouring limit is unverified', async () => { + arrange( + res({ + json: { + success: true, + result: Array.from({ length: 250 }, (_, i) => ({ id: i })), + }, + }) + ) const response = await SEARCH( - createMockRequest('POST', { ...listBase, query: "priority='High'" }) + createMockRequest('POST', { ...baseBody, query: "status='Active'" }) ) const data = (await response.json()) as { - success: boolean output: { records: unknown[]; totalCount: number } } + expect(data.output.records).toHaveLength(200) + expect(data.output.totalCount).toBe(200) + }) +}) + +describe('review round 1 fixes', () => { + it('fails a create that comes back without a record ID', async () => { + arrange(res({ json: { success: true, result: {} } })) + + const response = await POST(createMockRequest('POST', { ...baseBody, data: '{"a":"b"}' })) + const data = (await response.json()) as { success: boolean; error?: string } + + expect(data.success).toBe(false) + expect(data.error).toContain('did not return an ID') + }) + + it('refuses a non-numeric record ID on a projected read rather than interpolating it', async () => { + const response = await READ( + createMockRequest('POST', { + ...baseBody, + recordId: "1' || priority='High", + fields: 'contract_title1', + }) + ) + const data = (await response.json()) as { success: boolean; error?: string } + + expect(data.success).toBe(false) + expect(data.error).toContain('must be numeric') + expect(inputValidationMockFns.mockSecureFetchWithPinnedIP).not.toHaveBeenCalled() + }) + + it('does not return an unrelated record when the search matches something else', async () => { + arrange(res({ json: { success: true, result: [{ id: 99, contract_title1: 'Other' }] } })) + + const response = await READ( + createMockRequest('POST', { ...baseBody, recordId: '6342', fields: 'contract_title1' }) + ) + const data = (await response.json()) as { success: boolean; error?: string } + + expect(data.success).toBe(false) + expect(data.error).toContain('no record for ID 6342') + }) +}) + +describe('documented EWREST response keys', () => { + it('reads the choice line ID from EWREST_choiceLineId', async () => { + const { POST: CHOICE } = await import('@/app/api/tools/agiloft/get_choice_line_id/route') + inputValidationMockFns.mockSecureFetchWithPinnedIP.mockResolvedValueOnce( + res({ text: "EWREST_choiceLineId = '1';" }) + ) + + const response = await CHOICE( + createMockRequest('POST', { ...baseBody, fieldName: 'priority', value: 'High' }) + ) + const data = (await response.json()) as { success: boolean; output: { choiceLineId: number } } + expect(data.success).toBe(true) - expect(data.output.records).toEqual([]) - expect(data.output.totalCount).toBe(0) + expect(data.output.choiceLineId).toBe(1) }) +}) - it('treats a plain-text refusal from EWSelect as a failure, not an empty result', async () => { - arrange('Error executing query, please consult logs') +describe('EWTable', () => { + it('is KB-scoped: no $table, mandatory .json, and no inline credentials', async () => { + const { POST: LIST } = await import('@/app/api/tools/agiloft/list_tables/route') + arrange(res({ json: { success: true, result: { tables: [] } } })) - const response = await SELECT( - createMockRequest('POST', { ...listBase, where: "summary like '%new%'" }) + await LIST( + createMockRequest('POST', { + instanceUrl: baseBody.instanceUrl, + knowledgeBase: baseBody.knowledgeBase, + login: baseBody.login, + password: baseBody.password, + includeLinkedInfo: true, + }) ) + + const [url, , init] = inputValidationMockFns.mockSecureFetchWithPinnedIP.mock.calls[1] + expect(url).toContain('/ewws/EWTable/.json?') + expect(url).toContain('&includelinkedinfo=true') + expect(url).not.toContain('$table=') + expect(url).not.toContain('$password') + expect(init.headers.Authorization).toBe('Bearer tok-123') + }) + + it('narrows to one table with the plain table parameter, not $table', async () => { + const { POST: LIST } = await import('@/app/api/tools/agiloft/list_tables/route') + arrange(res({ json: { success: true, result: { tables: [] } } })) + + await LIST(createMockRequest('POST', { ...baseBody, table: 'contacts' })) + + const [url] = inputValidationMockFns.mockSecureFetchWithPinnedIP.mock.calls[1] + expect(url).toContain('&table=contacts') + expect(url).not.toContain('$table=') + }) + + it('flattens tables and fields into the documented shape', async () => { + const { POST: LIST } = await import('@/app/api/tools/agiloft/list_tables/route') + arrange( + res({ + json: { + success: true, + result: { + tables: [ + { + label: 'WMI Sample', + logicalName: 'wmi_sample', + fields: [ + { + columnLabel: 'ID', + columnName: 'id', + columnType: 'BIGINT', + columnTypeDomain: 'swautoincrementfield', + }, + { + columnLabel: 'Updated By', + columnName: '_1794_full_name', + columnType: 'VARCHAR', + columnTypeDomain: 'swshorttextfield', + isLinked: true, + linkedInfo: [ + { + linkedTable: 'contacts', + linkedColumn: 'full_name', + linkedDao: '_dao3_link0', + }, + ], + textFieldType: 'text/plain', + }, + ], + }, + ], + }, + }, + }) + ) + + const response = await LIST(createMockRequest('POST', baseBody)) + const data = (await response.json()) as { + output: { tables: Array<{ logicalName: string; fields: unknown[] }>; totalCount: number } + } + + expect(data.output.totalCount).toBe(1) + expect(data.output.tables[0].logicalName).toBe('wmi_sample') + expect(data.output.tables[0].fields).toEqual([ + { + columnName: 'id', + columnLabel: 'ID', + columnType: 'BIGINT', + columnTypeDomain: 'swautoincrementfield', + required: false, + isLinked: false, + linkedInfo: [], + textFieldType: null, + }, + { + columnName: '_1794_full_name', + columnLabel: 'Updated By', + columnType: 'VARCHAR', + columnTypeDomain: 'swshorttextfield', + required: false, + isLinked: true, + // includeLinkedInfo must actually surface the source table/column. + linkedInfo: [{ linkedTable: 'contacts', linkedColumn: 'full_name' }], + textFieldType: 'text/plain', + }, + ]) + }) +}) + +describe('EWUpsert', () => { + const upsertBody = { ...baseBody, match: 'ext_id', data: '{"first_name":"John"}' } + + it('sends every parameter in the form body, keeping credentials out of the URL', async () => { + const { POST: UPSERT } = await import('@/app/api/tools/agiloft/upsert_record/route') + inputValidationMockFns.mockSecureFetchWithPinnedIP.mockResolvedValueOnce( + res({ status: 201, text: "EWREST_id='353';" }) + ) + + await UPSERT(createMockRequest('POST', upsertBody)) + + const calls = inputValidationMockFns.mockSecureFetchWithPinnedIP.mock.calls + // Inline-credential auth: a single call, no login/logout pair. + expect(calls).toHaveLength(1) + expect(calls[0][0]).toBe('https://example.agiloft.com/ewws/EWUpsert') + expect(calls[0][0]).not.toContain('?') + + const sent = new URLSearchParams(calls[0][2].body as string) + expect(sent.get('$match')).toBe('ext_id') + expect(sent.get('$table')).toBe('contract') + expect(sent.get('$password')).toBe(PLACEHOLDER_PASSWORD) + expect(sent.get('first_name')).toBe('John') + }) + + it('reports 201 as a create and 200 as an update', async () => { + const { POST: UPSERT } = await import('@/app/api/tools/agiloft/upsert_record/route') + + inputValidationMockFns.mockSecureFetchWithPinnedIP.mockResolvedValueOnce( + res({ status: 201, text: "EWREST_id='353';" }) + ) + let data = (await (await UPSERT(createMockRequest('POST', upsertBody))).json()) as { + output: { id: string; created: boolean } + } + expect(data.output).toEqual({ id: '353', created: true, callbackId: null }) + + inputValidationMockFns.mockSecureFetchWithPinnedIP.mockResolvedValueOnce( + res({ status: 200, text: "EWREST_id='353';" }) + ) + data = (await (await UPSERT(createMockRequest('POST', upsertBody))).json()) as { + output: { id: string; created: boolean } + } + expect(data.output).toEqual({ id: '353', created: false, callbackId: null }) + }) + + it('surfaces a 409 as an ambiguous match rather than a generic failure', async () => { + const { POST: UPSERT } = await import('@/app/api/tools/agiloft/upsert_record/route') + inputValidationMockFns.mockSecureFetchWithPinnedIP.mockResolvedValueOnce( + res({ ok: false, status: 409, text: 'Multiple matching records found' }) + ) + + const response = await UPSERT(createMockRequest('POST', upsertBody)) const data = (await response.json()) as { success: boolean; error?: string } expect(data.success).toBe(false) - expect(data.error).toContain('did not return a result set') + expect(data.error).toContain('more than one record matching "ext_id"') + }) +}) + +describe('EWAsyncStatus', () => { + const statusBody = { ...baseBody, callbackId: '10100_1' } + + it('maps each documented status code to its meaning', async () => { + const { POST: STATUS } = await import('@/app/api/tools/agiloft/async_status/route') + + const cases: Array<[number, string, boolean]> = [ + [200, 'completed', true], + [201, 'queued', false], + [202, 'in_progress', false], + [501, 'failed', true], + [523, 'unknown_callback', true], + ] + + for (const [status, expected, complete] of cases) { + inputValidationMockFns.mockSecureFetchWithPinnedIP.mockResolvedValueOnce( + res({ ok: status < 400, status, text: '' }) + ) + + const response = await STATUS(createMockRequest('POST', statusBody)) + const data = (await response.json()) as { + success: boolean + output: { status: string; complete: boolean; statusCode: number } + } + + // 501 is a failed *operation*, not a failed status check. + expect(data.success).toBe(true) + expect(data.output).toMatchObject({ status: expected, complete, statusCode: status }) + } }) - it('still reports a genuinely empty EWSelect result as a success', async () => { - arrange("EWREST_id_length = '0';") + it('authenticates inline and sends the documented callback_id param', async () => { + const { POST: STATUS } = await import('@/app/api/tools/agiloft/async_status/route') + inputValidationMockFns.mockSecureFetchWithPinnedIP.mockResolvedValueOnce( + res({ status: 200, text: '' }) + ) + + await STATUS(createMockRequest('POST', statusBody)) - const response = await SELECT( - createMockRequest('POST', { ...listBase, where: "summary like '%new%'" }) + const calls = inputValidationMockFns.mockSecureFetchWithPinnedIP.mock.calls + expect(calls).toHaveLength(1) + expect(calls[0][0]).toContain('/ewws/EWAsyncStatus?') + expect(calls[0][0]).toContain('&callback_id=10100_1') + }) +}) + +describe('EWActionButton single-line response', () => { + it('parses both assignments when Agiloft returns them on one line', async () => { + const { POST: ACTION } = await import('@/app/api/tools/agiloft/run_action_button/route') + inputValidationMockFns.mockSecureFetchWithPinnedIP.mockResolvedValueOnce( + res({ text: "EWREST_id='82'; EWREST_EWCALLBACK_ID='10100_1';" }) + ) + + const response = await ACTION( + createMockRequest('POST', { ...baseBody, recordId: '82', actionButtonField: 'ab_field' }) ) const data = (await response.json()) as { success: boolean - output: { recordIds: string[]; totalCount: number } + output: { recordId: string; callbackId: string | null } } + expect(data.output.recordId).toBe('82') + expect(data.output.callbackId).toBe('10100_1') + }) +}) + +describe('review round 2 fixes', () => { + const upsertBody = { ...baseBody, match: 'ext_id', data: '{"a":"b"}' } + + it('returns a callback ID for a queued upsert so Async Status can poll it', async () => { + const { POST: UPSERT } = await import('@/app/api/tools/agiloft/upsert_record/route') + inputValidationMockFns.mockSecureFetchWithPinnedIP.mockResolvedValueOnce( + res({ status: 202, text: "EWREST_EWCALLBACK_ID='10100_7';" }) + ) + + const response = await UPSERT(createMockRequest('POST', upsertBody)) + const data = (await response.json()) as { + success: boolean + output: { id: string | null; callbackId: string | null } + } + + expect(data.success).toBe(true) + expect(data.output.callbackId).toBe('10100_7') + }) + + it('matches a projected read on the canonical numeric ID, not the string', async () => { + arrange(res({ json: { success: true, result: [{ id: 123, contract_title1: 'X' }] } })) + + // Agiloft echoes 123 for a request made as 00123. + const response = await READ( + createMockRequest('POST', { ...baseBody, recordId: '00123', fields: 'contract_title1' }) + ) + const data = (await response.json()) as { success: boolean; output: { id: string | null } } + expect(data.success).toBe(true) - expect(data.output.recordIds).toEqual([]) - expect(data.output.totalCount).toBe(0) + expect(data.output.id).toBe('123') + }) +}) + +describe('review round 3 fixes', () => { + it('reports an Agiloft refusal as a non-retryable failure, not a 500', async () => { + arrange( + res({ json: { success: false, errors: [{ message: 'Field contract_title1 is required' }] } }) + ) + + const response = await POST(createMockRequest('POST', { ...baseBody, data: '{"a":"b"}' })) + + /** + * The tool runner retries 500s, and retrying a refused create can duplicate + * a record — a refusal must come back as a settled failure. + */ + expect(response.status).toBe(200) + const data = (await response.json()) as { success: boolean; error?: string } + expect(data.success).toBe(false) + expect(data.error).toContain('Field contract_title1 is required') + }) + + it('tells the user what to do when EWTable login needs a table', async () => { + const { POST: LIST } = await import('@/app/api/tools/agiloft/list_tables/route') + inputValidationMockFns.mockSecureFetchWithPinnedIP.mockResolvedValueOnce( + res({ + ok: false, + status: 400, + text: 'EWWrongDataException has occurred: One has to specify $table, $KB, $lang parameters', + }) + ) + + const response = await LIST( + createMockRequest('POST', { + instanceUrl: baseBody.instanceUrl, + knowledgeBase: baseBody.knowledgeBase, + login: baseBody.login, + password: baseBody.password, + }) + ) + const data = (await response.json()) as { success: boolean; error?: string } + + expect(data.success).toBe(false) + expect(data.error).toContain('requires a table name to authenticate') + }) + + it('encodes a multi-value upsert field as repeated pairs, not a joined string', async () => { + const { POST: UPSERT } = await import('@/app/api/tools/agiloft/upsert_record/route') + inputValidationMockFns.mockSecureFetchWithPinnedIP.mockResolvedValueOnce( + res({ status: 200, text: "EWREST_id='353';" }) + ) + + await UPSERT( + createMockRequest('POST', { + ...baseBody, + match: 'ext_id', + data: '{"contactMethod":["phone","email"]}', + }) + ) + + const body = inputValidationMockFns.mockSecureFetchWithPinnedIP.mock.calls[0][2].body as string + expect(new URLSearchParams(body).getAll('contactMethod')).toEqual(['phone', 'email']) + }) + + it('refuses an object field value rather than writing [object Object]', async () => { + const { POST: UPSERT } = await import('@/app/api/tools/agiloft/upsert_record/route') + + const response = await UPSERT( + createMockRequest('POST', { + ...baseBody, + match: 'ext_id', + data: '{"nested":{"a":1}}', + }) + ) + const data = (await response.json()) as { success: boolean; error?: string } + + expect(data.success).toBe(false) + expect(data.error).toContain('has no encoding for') + expect(inputValidationMockFns.mockSecureFetchWithPinnedIP).not.toHaveBeenCalled() }) }) diff --git a/apps/sim/app/api/tools/agiloft/create_record/route.ts b/apps/sim/app/api/tools/agiloft/create_record/route.ts index f28ca57f316..e6f183c0699 100644 --- a/apps/sim/app/api/tools/agiloft/create_record/route.ts +++ b/apps/sim/app/api/tools/agiloft/create_record/route.ts @@ -6,10 +6,13 @@ import { getValidationErrorMessage, parseRequest } from '@/lib/api/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { parseEwRest, toRecord } from '@/tools/agiloft/ewrest' import type { AgiloftRecordResponse } from '@/tools/agiloft/types' -import { buildCreateRecordUrl, recordUrlLengthError } from '@/tools/agiloft/utils' -import { executeAgiloftRequest } from '@/tools/agiloft/utils.server' +import { alrestRecordCollectionUrl } from '@/tools/agiloft/utils' +import { + executeAlrestRequest, + isAgiloftRefusal, + readAlrestJson, +} from '@/tools/agiloft/utils.server' export const dynamic = 'force-dynamic' @@ -65,52 +68,53 @@ export const POST = withRouteHandler(async (request: NextRequest) => { }) } - const oversized = recordUrlLengthError(params.instanceUrl, (base) => - buildCreateRecordUrl(base, params, fieldValues) - ) - if (oversized) { - return NextResponse.json({ - success: false, - output: { id: null, fields: {} }, - error: oversized, - }) - } - - const result = await executeAgiloftRequest( + const result = await executeAlrestRequest( params, (base) => ({ - url: buildCreateRecordUrl(base, params, fieldValues), + url: alrestRecordCollectionUrl(base, params.table), method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, + body: JSON.stringify(fieldValues), }), async (response) => { - const body = await response.text() + const record = await readAlrestJson>(response) + const id = record?.id - if (!response.ok) { + /** + * A create that reports no ID did not create anything usable — callers + * chain on this ID, so surface it as a failure rather than handing back + * a successful-looking null. + */ + if (id == null) { return { success: false, - output: { id: null, fields: {} }, - error: `Agiloft error: ${response.status} - ${body}`, + output: { id: null, fields: record ?? {} }, + error: 'Agiloft did not return an ID for the created record', } } - /** EWCreate answers with a single assignment: EWREST_id='353'; */ - const { id, fields } = toRecord(parseEwRest(body)) - - if (id === null) { - return { - success: false, - output: { id: null, fields }, - error: `Agiloft did not return a record ID: ${body.trim() || '(empty response)'}`, - } + return { + success: true, + output: { id: String(id), fields: record ?? {} }, } - - return { success: true, output: { id, fields } } } ) return NextResponse.json(result) } catch (error) { + /** + * A refusal Agiloft already decided on is a final answer, not a transient + * fault — returning 500 would make the tool runner retry it. + */ + if (isAgiloftRefusal(error)) { + logger.warn(`[${requestId}] Agiloft refused the request`, { error: error.message }) + return NextResponse.json({ + success: false, + output: { id: null, fields: {} }, + error: error.message, + }) + } + logger.error(`[${requestId}] Error creating Agiloft record:`, error) return NextResponse.json({ success: false, error: toError(error).message }, { status: 500 }) diff --git a/apps/sim/app/api/tools/agiloft/delete_record/route.ts b/apps/sim/app/api/tools/agiloft/delete_record/route.ts index 873a51c88f1..e6381ce0137 100644 --- a/apps/sim/app/api/tools/agiloft/delete_record/route.ts +++ b/apps/sim/app/api/tools/agiloft/delete_record/route.ts @@ -6,10 +6,13 @@ import { getValidationErrorMessage, parseRequest } from '@/lib/api/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { isEwRestBody } from '@/tools/agiloft/ewrest' import type { AgiloftDeleteResponse } from '@/tools/agiloft/types' -import { buildDeleteRecordUrl } from '@/tools/agiloft/utils' -import { executeAgiloftRequest } from '@/tools/agiloft/utils.server' +import { alrestDeleteRecordUrl } from '@/tools/agiloft/utils' +import { + executeAlrestRequest, + isAgiloftRefusal, + readAlrestJson, +} from '@/tools/agiloft/utils.server' export const dynamic = 'force-dynamic' @@ -50,45 +53,43 @@ export const POST = withRouteHandler(async (request: NextRequest) => { if (!parsed.success) return parsed.response const params = parsed.data.body - const result = await executeAgiloftRequest( + const result = await executeAlrestRequest( params, (base) => ({ - url: buildDeleteRecordUrl(base, params), - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + url: alrestDeleteRecordUrl( + base, + params.table, + params.recordId, + params.deleteRule, + params.substituteIds + ), + method: 'DELETE', + headers: { Accept: 'application/json' }, }), async (response) => { - const body = (await response.text()).trim() - const recordId = params.recordId.trim() - - if (!response.ok) { - return { - success: false, - output: { id: recordId, deleted: false }, - error: `Agiloft error: ${response.status} - ${body}`, - } - } - - /** - * EWDelete returns nothing on success and an error message on failure, - * so a non-empty body that is not an EWREST assignment is a refusal the - * HTTP status did not surface — most often the delete rule rejecting - * dependent records. - */ - if (body && !isEwRestBody(body)) { - return { - success: false, - output: { id: recordId, deleted: false }, - error: `Agiloft refused the delete: ${body}`, - } + await readAlrestJson(response) + return { + success: true, + output: { id: params.recordId.trim(), deleted: true }, } - - return { success: true, output: { id: recordId, deleted: true } } } ) return NextResponse.json(result) } catch (error) { + /** + * A refusal Agiloft already decided on is a final answer, not a transient + * fault — returning 500 would make the tool runner retry it. + */ + if (isAgiloftRefusal(error)) { + logger.warn(`[${requestId}] Agiloft refused the request`, { error: error.message }) + return NextResponse.json({ + success: false, + output: { id: '', deleted: false }, + error: error.message, + }) + } + logger.error(`[${requestId}] Error deleting Agiloft record:`, error) return NextResponse.json({ success: false, error: toError(error).message }, { status: 500 }) diff --git a/apps/sim/app/api/tools/agiloft/get_choice_line_id/route.ts b/apps/sim/app/api/tools/agiloft/get_choice_line_id/route.ts index 8d6cd9b95d8..3324c6970ef 100644 --- a/apps/sim/app/api/tools/agiloft/get_choice_line_id/route.ts +++ b/apps/sim/app/api/tools/agiloft/get_choice_line_id/route.ts @@ -8,8 +8,8 @@ import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { parseEwRest } from '@/tools/agiloft/ewrest' import type { AgiloftGetChoiceLineIdResponse } from '@/tools/agiloft/types' -import { buildGetChoiceLineIdUrl } from '@/tools/agiloft/utils' -import { executeAgiloftRequest } from '@/tools/agiloft/utils.server' +import { buildGetChoiceLineIdUrl, describeAgiloftError } from '@/tools/agiloft/utils' +import { executeEwRequest } from '@/tools/agiloft/utils.server' export const dynamic = 'force-dynamic' @@ -52,7 +52,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { if (!parsed.success) return parsed.response const params = parsed.data.body - const result = await executeAgiloftRequest( + const result = await executeEwRequest( params, (base) => ({ url: buildGetChoiceLineIdUrl(base, params), @@ -65,21 +65,21 @@ export const POST = withRouteHandler(async (request: NextRequest) => { return { success: false, output: { choiceLineId: null }, - error: `Agiloft error: ${response.status} - ${body}`, + error: `Agiloft error ${response.status}: ${describeAgiloftError(body)}`, } } - /** - * The docs state only that EWGetChoiceLineId "returns the ID of the - * choice list element" without naming the assignment key, so the first - * numeric EWREST_ value is taken rather than guessing a key name. - */ - let choiceLineId: number | null = null - for (const value of parseEwRest(body).values()) { - const parsedValue = Number(value) - if (value.trim() !== '' && Number.isFinite(parsedValue)) { - choiceLineId = parsedValue - break + /** Documented response: EWREST_choiceLineId = '1'; */ + const raw = parseEwRest(body).get('choiceLineId') + const parsedId = Number(raw) + const choiceLineId = + raw !== undefined && raw.trim() !== '' && Number.isFinite(parsedId) ? parsedId : null + + if (raw === undefined) { + return { + success: false, + output: { choiceLineId: null }, + error: `Agiloft did not return a choice line ID for "${params.value}" in field "${params.fieldName}": ${body.trim() || '(empty response)'}`, } } @@ -87,7 +87,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { return { success: false, output: { choiceLineId: null }, - error: `No choice line ID found for value "${params.value}" in field "${params.fieldName}": ${body.trim() || '(empty response)'}`, + error: `Agiloft returned a non-numeric choice line ID for "${params.value}" in field "${params.fieldName}": "${raw}"`, } } diff --git a/apps/sim/app/api/tools/agiloft/list_tables/route.ts b/apps/sim/app/api/tools/agiloft/list_tables/route.ts new file mode 100644 index 00000000000..42187655c0c --- /dev/null +++ b/apps/sim/app/api/tools/agiloft/list_tables/route.ts @@ -0,0 +1,152 @@ +import { createLogger } from '@sim/logger' +import { toError } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { agiloftListTablesContract } from '@/lib/api/contracts/tools/agiloft' +import { getValidationErrorMessage, parseRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import type { + AgiloftListTablesParams, + AgiloftListTablesResponse, + AgiloftTableField, +} from '@/tools/agiloft/types' +import { buildListTablesUrl } from '@/tools/agiloft/utils' +import { + executeAgiloftRequest, + isAgiloftRefusal, + readAlrestJson, +} from '@/tools/agiloft/utils.server' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('AgiloftListTablesAPI') + +/** Shape of the `result` object EWTable returns. */ +interface EwTableResult { + tables?: Array<{ + label?: string + logicalName?: string + fields?: Array<{ + columnName?: string + columnLabel?: string + columnType?: string + columnTypeDomain?: string + required?: boolean + isLinked?: boolean + linkedInfo?: Array<{ linkedTable?: string; linkedColumn?: string }> + textFieldType?: string + }> + }> +} + +export const POST = withRouteHandler(async (request: NextRequest) => { + const requestId = generateRequestId() + let params: AgiloftListTablesParams | undefined + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + + if (!authResult.success || !authResult.userId) { + logger.warn(`[${requestId}] Unauthorized Agiloft list_tables attempt: ${authResult.error}`) + return NextResponse.json( + { success: false, error: authResult.error || 'Authentication required' }, + { status: 401 } + ) + } + + const parsed = await parseRequest( + agiloftListTablesContract, + request, + {}, + { + validationErrorResponse: (error) => { + logger.warn(`[${requestId}] Invalid request data`, { errors: error.issues }) + return NextResponse.json( + { + success: false, + error: getValidationErrorMessage(error, 'Invalid request data'), + details: error.issues, + }, + { status: 400 } + ) + }, + } + ) + if (!parsed.success) return parsed.response + const listParams = parsed.data.body + params = listParams + + /** EWTable must run under EWLogin or OAuth authorization. */ + const result = await executeAgiloftRequest( + listParams, + (base) => ({ + url: buildListTablesUrl(base, listParams), + method: 'GET', + headers: { Accept: 'application/json' }, + }), + async (response) => { + const payload = await readAlrestJson(response) + + const tables = (payload?.tables ?? []).map((table) => ({ + label: table.label ?? '', + logicalName: table.logicalName ?? '', + fields: (table.fields ?? []).map( + (field): AgiloftTableField => ({ + columnName: field.columnName ?? '', + columnLabel: field.columnLabel ?? '', + columnType: field.columnType ?? '', + columnTypeDomain: field.columnTypeDomain ?? '', + required: field.required === true, + isLinked: field.isLinked === true, + /** + * Only present when includeLinkedInfo was requested; dropping it + * would make that option have no observable effect. + */ + linkedInfo: (field.linkedInfo ?? []).map((link) => ({ + linkedTable: link.linkedTable ?? '', + linkedColumn: link.linkedColumn ?? '', + })), + textFieldType: field.textFieldType ?? null, + }) + ), + })) + + return { success: true, output: { tables, totalCount: tables.length } } + } + ) + + return NextResponse.json(result) + } catch (error) { + /** + * A refusal Agiloft already decided on is a final answer, not a transient + * fault — returning 500 would make the tool runner retry it. + */ + /** + * EWTable is knowledge-base scoped, but some instances reject EWLogin + * without a $table. When that happens there is nothing to fall back to, so + * say what the caller can actually do about it. + */ + if (!params?.table && /\$table/.test(toError(error).message)) { + return NextResponse.json({ + success: false, + output: { tables: [], totalCount: 0 }, + error: + 'This Agiloft instance requires a table name to authenticate. Put any known table in the Table field — it also narrows the result to that table.', + }) + } + + if (isAgiloftRefusal(error)) { + logger.warn(`[${requestId}] Agiloft refused the request`, { error: error.message }) + return NextResponse.json({ + success: false, + output: { tables: [], totalCount: 0 }, + error: error.message, + }) + } + + logger.error(`[${requestId}] Error listing Agiloft tables:`, error) + + return NextResponse.json({ success: false, error: toError(error).message }, { status: 500 }) + } +}) diff --git a/apps/sim/app/api/tools/agiloft/lock_record/route.ts b/apps/sim/app/api/tools/agiloft/lock_record/route.ts index 3b353211a1d..96ebe9f587b 100644 --- a/apps/sim/app/api/tools/agiloft/lock_record/route.ts +++ b/apps/sim/app/api/tools/agiloft/lock_record/route.ts @@ -7,13 +7,24 @@ import { checkInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import type { AgiloftLockResponse } from '@/tools/agiloft/types' -import { buildLockRecordUrl, getLockHttpMethod } from '@/tools/agiloft/utils' -import { executeAgiloftRequest } from '@/tools/agiloft/utils.server' +import { buildLockRecordUrl, describeAgiloftError, getLockHttpMethod } from '@/tools/agiloft/utils' +import { executeEwRequest } from '@/tools/agiloft/utils.server' export const dynamic = 'force-dynamic' const logger = createLogger('AgiloftLockRecordAPI') +/** Lock output for a call that never returned a lock state. */ +function emptyLock(recordId: string) { + return { + id: recordId.trim(), + tableId: null, + lockStatus: '', + lockedBy: null, + lockExpiresInMinutes: null, + } +} + export const POST = withRouteHandler(async (request: NextRequest) => { const requestId = generateRequestId() @@ -49,7 +60,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { if (!parsed.success) return parsed.response const params = parsed.data.body - const result = await executeAgiloftRequest( + const result = await executeEwRequest( params, (base) => ({ url: buildLockRecordUrl(base, params), @@ -60,31 +71,42 @@ export const POST = withRouteHandler(async (request: NextRequest) => { const errorText = await response.text() return { success: false, - output: { - id: params.recordId?.trim() ?? '', - lockStatus: 'UNKNOWN', - lockedBy: null, - lockExpiresInMinutes: null, - }, - error: `Agiloft error: ${response.status} - ${errorText}`, + output: emptyLock(params.recordId), + error: `Agiloft error ${response.status}: ${describeAgiloftError(errorText)}`, } } const data = (await response.json()) as Record - const result = (data.result ?? data) as Record + + /** + * EWLock answers failures with `{error, error_description}` rather than + * a lock body, and can do so on a 200 — so the absence of a + * `lock_status` is the reliable signal, not the status code. + */ + if (typeof data.lock_status !== 'string') { + const code = typeof data.error === 'string' ? data.error : 'UNKNOWN' + const detail = + typeof data.error_description === 'string' + ? data.error_description + : JSON.stringify(data) + return { + success: false, + output: emptyLock(params.recordId), + error: `Agiloft lock error (${code}): ${detail}`, + } + } return { - success: data.success !== false, + success: true, output: { - id: String(result.id ?? params.recordId?.trim() ?? ''), - lockStatus: - (result.lock_status as string) ?? (result.lockStatus as string) ?? 'UNKNOWN', - lockedBy: - (result.locked_by as string | null) ?? (result.lockedBy as string | null) ?? null, + id: String(data.id ?? params.recordId.trim()), + tableId: typeof data.table_id === 'number' ? data.table_id : null, + lockStatus: data.lock_status, + lockedBy: typeof data.locked_by === 'string' ? data.locked_by : null, lockExpiresInMinutes: - (result.lock_expires_in_minutes as number | null) ?? - (result.lockExpiresInMinutes as number | null) ?? - null, + typeof data.lock_expires_in_minutes === 'number' + ? data.lock_expires_in_minutes + : null, }, } } diff --git a/apps/sim/app/api/tools/agiloft/nlp_search/route.ts b/apps/sim/app/api/tools/agiloft/nlp_search/route.ts new file mode 100644 index 00000000000..4b6c2e50a01 --- /dev/null +++ b/apps/sim/app/api/tools/agiloft/nlp_search/route.ts @@ -0,0 +1,108 @@ +import { createLogger } from '@sim/logger' +import { toError } from '@sim/utils/errors' +import { filterUndefined } from '@sim/utils/object' +import { type NextRequest, NextResponse } from 'next/server' +import { agiloftNlpSearchContract } from '@/lib/api/contracts/tools/agiloft' +import { getValidationErrorMessage, parseRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import type { AgiloftNlpSearchResponse } from '@/tools/agiloft/types' +import { + AGILOFT_LANG, + AGILOFT_MAX_SEARCH_RECORDS, + buildNlpSearchUrl, + parseFieldList, +} from '@/tools/agiloft/utils' +import { executeEwRequest, readAlrestJson } from '@/tools/agiloft/utils.server' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('AgiloftNlpSearchAPI') + +export const POST = withRouteHandler(async (request: NextRequest) => { + const requestId = generateRequestId() + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + + if (!authResult.success || !authResult.userId) { + logger.warn(`[${requestId}] Unauthorized Agiloft nlp_search attempt: ${authResult.error}`) + return NextResponse.json( + { success: false, error: authResult.error || 'Authentication required' }, + { status: 401 } + ) + } + + const parsed = await parseRequest( + agiloftNlpSearchContract, + request, + {}, + { + validationErrorResponse: (error) => { + logger.warn(`[${requestId}] Invalid request data`, { errors: error.issues }) + return NextResponse.json( + { + success: false, + error: getValidationErrorMessage(error, 'Invalid request data'), + details: error.issues, + }, + { status: 400 } + ) + }, + } + ) + if (!parsed.success) return parsed.response + const params = parsed.data.body + + const result = await executeEwRequest( + params, + (base) => ({ + url: buildNlpSearchUrl(base), + method: 'POST', + headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, + /** + * EWNLPSearch accepts application/json, so credentials travel in the + * body rather than the query string. + */ + body: JSON.stringify( + filterUndefined({ + $KB: params.knowledgeBase, + $login: params.login, + $password: params.password, + $lang: AGILOFT_LANG, + field: parseFieldList(params.fields), + nlp_query: params.nlpQuery.trim(), + page: params.page ? Number(params.page) : undefined, + limit: params.limit ? Number(params.limit) : undefined, + }) + ), + }), + async (response) => { + const returned = (await readAlrestJson[]>(response)) ?? [] + const records = returned.slice(0, AGILOFT_MAX_SEARCH_RECORDS) + + if (returned.length > records.length) { + logger.warn( + `[${requestId}] Agiloft NLP search returned ${returned.length} records; truncated to ${AGILOFT_MAX_SEARCH_RECORDS}` + ) + } + + return { + success: true, + output: { + records, + totalCount: records.length, + truncated: returned.length > records.length, + }, + } + } + ) + + return NextResponse.json(result) + } catch (error) { + logger.error(`[${requestId}] Error running Agiloft NLP search:`, error) + + return NextResponse.json({ success: false, error: toError(error).message }, { status: 500 }) + } +}) diff --git a/apps/sim/app/api/tools/agiloft/read_record/route.ts b/apps/sim/app/api/tools/agiloft/read_record/route.ts index 51781ef791d..53c1479024d 100644 --- a/apps/sim/app/api/tools/agiloft/read_record/route.ts +++ b/apps/sim/app/api/tools/agiloft/read_record/route.ts @@ -6,10 +6,14 @@ import { getValidationErrorMessage, parseRequest } from '@/lib/api/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { parseEwRest, toRecord } from '@/tools/agiloft/ewrest' import type { AgiloftRecordResponse } from '@/tools/agiloft/types' -import { buildReadRecordUrl } from '@/tools/agiloft/utils' -import { executeAgiloftRequest } from '@/tools/agiloft/utils.server' +import { alrestRecordUrl, alrestSearchUrl, parseFieldList } from '@/tools/agiloft/utils' +import { + type AgiloftRequestConfig, + executeAlrestRequest, + isAgiloftRefusal, + readAlrestJson, +} from '@/tools/agiloft/utils.server' export const dynamic = 'force-dynamic' @@ -51,57 +55,103 @@ export const POST = withRouteHandler(async (request: NextRequest) => { const params = parsed.data.body /** - * EWRead has no documented field-selection parameter — it returns the whole - * record the caller is permitted to see — so the requested subset is - * applied here rather than sent upstream. + * A full record is large — a contract runs to roughly 184 KB — so when the + * caller names the fields they want, the read goes through the search + * endpoint, which is the only route that accepts a `field` projection. + * Without a field list there is nothing to project and the plain record + * fetch is cheaper. */ - const requestedFields = params.fields - ?.split(',') - .map((field) => field.trim()) - .filter(Boolean) + const requestedFields = parseFieldList(params.fields) + const recordId = params.recordId.trim() - const result = await executeAgiloftRequest( - params, - (base) => ({ - url: buildReadRecordUrl(base, params), - method: 'GET', - }), - async (response) => { - const body = await response.text() + /** + * The projected read puts the ID inside a search predicate, so anything + * other than a plain number could change which records the query selects. + * Agiloft record IDs are integers, so reject everything else rather than + * trying to escape it. + */ + if (requestedFields && !/^\d+$/.test(recordId)) { + return NextResponse.json({ + success: false, + output: { id: null, fields: {} }, + error: `Record ID must be numeric to read specific fields, got "${recordId}"`, + }) + } - if (!response.ok) { + const result = await executeAlrestRequest( + params, + (base): AgiloftRequestConfig => { + if (!requestedFields) { return { - success: false, - output: { id: null, fields: {} }, - error: `Agiloft error: ${response.status} - ${body}`, + url: alrestRecordUrl(base, params.table, params.recordId), + method: 'GET', + headers: { Accept: 'application/json' }, } } - const values = parseEwRest(body) - if (values.size === 0) { + return { + url: alrestSearchUrl(base, params.table), + method: 'POST', + headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, + body: JSON.stringify({ + field: requestedFields.includes('id') ? requestedFields : ['id', ...requestedFields], + query: `id=${recordId}`, + }), + } + }, + async (response) => { + const payload = await readAlrestJson | Record[]>( + response + ) + + /** + * Match on ID rather than taking the first row: a search answers with a + * result set, and returning an unrelated record as a successful read + * would be worse than failing. + */ + /** + * Compare numerically: Agiloft echoes the canonical ID, so a request + * for `00123` comes back as `123` and a string compare would discard + * the very record that was asked for. + */ + const record = Array.isArray(payload) + ? payload.find((row) => { + const rowId = String(row?.id ?? '') + return /^\d+$/.test(rowId) && BigInt(rowId) === BigInt(recordId) + }) + : payload + + if (!record) { return { success: false, output: { id: null, fields: {} }, - error: `Agiloft returned no record data: ${body.trim() || '(empty response)'}`, + error: `Agiloft returned no record for ID ${recordId}`, } } - const { id, fields } = toRecord(values) - - if (!requestedFields?.length) { - return { success: true, output: { id, fields } } - } - - const selected: Record = {} - for (const field of requestedFields) { - if (field in fields) selected[field] = fields[field] + const id = record.id + return { + success: true, + output: { id: id == null ? null : String(id), fields: record }, } - return { success: true, output: { id, fields: selected } } } ) return NextResponse.json(result) } catch (error) { + /** + * A refusal Agiloft already decided on is a final answer, not a transient + * fault — returning 500 would make the tool runner retry it. + */ + if (isAgiloftRefusal(error)) { + logger.warn(`[${requestId}] Agiloft refused the request`, { error: error.message }) + return NextResponse.json({ + success: false, + output: { id: null, fields: {} }, + error: error.message, + }) + } + logger.error(`[${requestId}] Error reading Agiloft record:`, error) return NextResponse.json({ success: false, error: toError(error).message }, { status: 500 }) diff --git a/apps/sim/app/api/tools/agiloft/remove_attachment/route.test.ts b/apps/sim/app/api/tools/agiloft/remove_attachment/route.test.ts index ba40c039af7..027f4dfadd2 100644 --- a/apps/sim/app/api/tools/agiloft/remove_attachment/route.test.ts +++ b/apps/sim/app/api/tools/agiloft/remove_attachment/route.test.ts @@ -13,13 +13,16 @@ vi.mock('@/lib/core/security/input-validation.server', () => inputValidationMock import { POST } from '@/app/api/tools/agiloft/remove_attachment/route' +/** Obvious non-secret so credential scanners do not flag these fixtures. */ +const PLACEHOLDER_PASSWORD = 'not-a-real-password' + const PINNED_IP = '93.184.216.34' const baseBody = { instanceUrl: 'https://example.agiloft.com', knowledgeBase: 'demo', login: 'admin', - password: 'secret', + password: PLACEHOLDER_PASSWORD, table: 'contracts', recordId: '42', fieldName: 'attachments', @@ -33,7 +36,7 @@ function mockSecureFetchResponse(body: { ok?: boolean; json?: unknown; text?: st statusText: '', headers: new Headers(), body: null, - text: async () => body.text ?? '', + text: async () => body.text ?? JSON.stringify(body.json ?? {}), json: async () => body.json ?? {}, arrayBuffer: async () => new ArrayBuffer(0), } @@ -55,10 +58,9 @@ beforeEach(() => { describe('POST /api/tools/agiloft/remove_attachment', () => { it('calls EWRemoveAttachment with GET, the only verb it accepts besides POST', async () => { - inputValidationMockFns.mockSecureFetchWithPinnedIP - .mockResolvedValueOnce(mockSecureFetchResponse({ json: { access_token: 'tok-rm' } })) - .mockResolvedValueOnce(mockSecureFetchResponse({ text: '2' })) - .mockResolvedValueOnce(mockSecureFetchResponse({})) + inputValidationMockFns.mockSecureFetchWithPinnedIP.mockResolvedValueOnce( + mockSecureFetchResponse({ text: "EWREST_attachments.length='2';" }) + ) const response = await POST(createMockRequest('POST', baseBody)) expect(response.status).toBe(200) @@ -69,8 +71,14 @@ describe('POST /api/tools/agiloft/remove_attachment', () => { } expect(data.output.remainingAttachments).toBe(2) - const operationCall = inputValidationMockFns.mockSecureFetchWithPinnedIP.mock.calls[1] - expect(operationCall[0]).toContain('/ewws/EWRemoveAttachment') - expect(operationCall[2]).toMatchObject({ method: 'GET' }) + /** + * One call, not three: the EW* surface rejects the bearer token, so it + * authenticates from inline credentials and needs no login/logout pair. + */ + const calls = inputValidationMockFns.mockSecureFetchWithPinnedIP.mock.calls + expect(calls).toHaveLength(1) + expect(calls[0][0]).toContain('/ewws/EWRemoveAttachment') + expect(calls[0][0]).toContain('&$login=admin') + expect(calls[0][2]).toMatchObject({ method: 'GET' }) }) }) diff --git a/apps/sim/app/api/tools/agiloft/remove_attachment/route.ts b/apps/sim/app/api/tools/agiloft/remove_attachment/route.ts index 0d0a4a4840e..d9abd48a518 100644 --- a/apps/sim/app/api/tools/agiloft/remove_attachment/route.ts +++ b/apps/sim/app/api/tools/agiloft/remove_attachment/route.ts @@ -6,9 +6,10 @@ import { getValidationErrorMessage, parseRequest } from '@/lib/api/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { parseEwRest } from '@/tools/agiloft/ewrest' import type { AgiloftRemoveAttachmentResponse } from '@/tools/agiloft/types' -import { buildRemoveAttachmentUrl } from '@/tools/agiloft/utils' -import { executeAgiloftRequest } from '@/tools/agiloft/utils.server' +import { buildRemoveAttachmentUrl, describeAgiloftError } from '@/tools/agiloft/utils' +import { executeEwRequest } from '@/tools/agiloft/utils.server' export const dynamic = 'force-dynamic' @@ -51,7 +52,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { if (!parsed.success) return parsed.response const params = parsed.data.body - const result = await executeAgiloftRequest( + const result = await executeEwRequest( params, (base) => ({ url: buildRemoveAttachmentUrl(base, params), @@ -69,25 +70,34 @@ export const POST = withRouteHandler(async (request: NextRequest) => { fieldName: params.fieldName?.trim() ?? '', remainingAttachments: 0, }, - error: `Agiloft error: ${response.status} - ${text}`, + error: `Agiloft error ${response.status}: ${describeAgiloftError(text)}`, } } - let remainingAttachments = 0 - try { - const data = JSON.parse(text) - const result = data.result ?? data - remainingAttachments = - typeof result === 'number' ? result : (result.count ?? result.remaining ?? 0) - } catch { - remainingAttachments = Number(text) || 0 + /** + * The URL carries no `.json` decorator, so the body is the EWREST + * assignment form — `EWREST_.length='0';` — exactly as + * EWAttach returns. Parsing it as JSON silently produced 0 on every + * call regardless of what Agiloft reported. + */ + const fieldName = params.fieldName.trim() + const assignments = parseEwRest(text) + const countRaw = assignments.get(`${fieldName}.length`) ?? [...assignments.values()][0] + const remainingAttachments = Number(countRaw) + + if (!Number.isFinite(remainingAttachments)) { + return { + success: false, + output: { recordId: params.recordId.trim(), fieldName, remainingAttachments: 0 }, + error: `Agiloft did not report the remaining attachment count: ${text.trim() || '(empty response)'}`, + } } return { success: true, output: { - recordId: params.recordId?.trim() ?? '', - fieldName: params.fieldName?.trim() ?? '', + recordId: params.recordId.trim(), + fieldName, remainingAttachments, }, } diff --git a/apps/sim/app/api/tools/agiloft/retrieve/route.test.ts b/apps/sim/app/api/tools/agiloft/retrieve/route.test.ts index efd435b5b04..1756eeb0897 100644 --- a/apps/sim/app/api/tools/agiloft/retrieve/route.test.ts +++ b/apps/sim/app/api/tools/agiloft/retrieve/route.test.ts @@ -13,13 +13,16 @@ vi.mock('@/lib/core/security/input-validation.server', () => inputValidationMock import { POST } from '@/app/api/tools/agiloft/retrieve/route' +/** Obvious non-secret so credential scanners do not flag these fixtures. */ +const PLACEHOLDER_PASSWORD = 'not-a-real-password' + const PINNED_IP = '93.184.216.34' const baseBody = { instanceUrl: 'https://example.agiloft.com', knowledgeBase: 'demo', login: 'admin', - password: 'secret', + password: PLACEHOLDER_PASSWORD, table: 'contracts', recordId: '42', fieldName: 'attachments', @@ -40,7 +43,7 @@ function mockSecureFetchResponse(body: { statusText: '', headers: body.headers ?? new Headers(), body: null, - text: async () => body.text ?? '', + text: async () => body.text ?? JSON.stringify(body.json ?? {}), json: async () => body.json ?? {}, arrayBuffer: async () => body.arrayBuffer ?? new ArrayBuffer(0), } @@ -89,24 +92,21 @@ describe('POST /api/tools/agiloft/retrieve', () => { expect(inputValidationMockFns.mockSecureFetchWithPinnedIP).not.toHaveBeenCalled() }) - it('pins the resolved IP for login, retrieve, and logout (TOCTOU fix)', async () => { + it('retrieves on the pinned IP in a single call (TOCTOU fix)', async () => { const fileBytes = Buffer.from('hello-attachment', 'utf-8') - inputValidationMockFns.mockSecureFetchWithPinnedIP - .mockResolvedValueOnce(mockSecureFetchResponse({ json: { access_token: 'tok-xyz' } })) - .mockResolvedValueOnce( - mockSecureFetchResponse({ - arrayBuffer: fileBytes.buffer.slice( - fileBytes.byteOffset, - fileBytes.byteOffset + fileBytes.byteLength - ) as ArrayBuffer, - headers: new Headers({ - 'content-type': 'text/plain', - 'content-disposition': 'attachment; filename="report.txt"', - }), - }) - ) - .mockResolvedValueOnce(mockSecureFetchResponse({})) + inputValidationMockFns.mockSecureFetchWithPinnedIP.mockResolvedValueOnce( + mockSecureFetchResponse({ + arrayBuffer: fileBytes.buffer.slice( + fileBytes.byteOffset, + fileBytes.byteOffset + fileBytes.byteLength + ) as ArrayBuffer, + headers: new Headers({ + 'content-type': 'text/plain', + 'content-disposition': 'attachment; filename="report.txt"', + }), + }) + ) const response = await POST(createMockRequest('POST', baseBody)) expect(response.status).toBe(200) @@ -121,43 +121,53 @@ describe('POST /api/tools/agiloft/retrieve', () => { expect(Buffer.from(data.output.file.data, 'base64').toString('utf-8')).toBe('hello-attachment') const calls = inputValidationMockFns.mockSecureFetchWithPinnedIP.mock.calls - expect(calls).toHaveLength(3) - - // All three outbound calls must use the pre-resolved IP. - for (const call of calls) { - expect(call[1]).toBe(PINNED_IP) - } + /** + * EWRetrieve authenticates from inline credentials, so there is no + * login/logout pair — one call, on the pre-resolved IP. + */ + expect(calls).toHaveLength(1) + expect(calls[0][1]).toBe(PINNED_IP) // Original hostname is preserved in the URL (so TLS SNI works). - expect(calls[0][0]).toContain('https://example.agiloft.com/ewws/EWLogin') - expect(calls[1][0]).toContain('https://example.agiloft.com/ewws/EWRetrieve') - expect(calls[1][2]).toMatchObject({ - method: 'GET', - headers: { Authorization: 'Bearer tok-xyz' }, - }) - expect(calls[2][0]).toContain('https://example.agiloft.com/ewws/EWLogout') + expect(calls[0][0]).toContain('https://example.agiloft.com/ewws/EWRetrieve') + expect(calls[0][0]).toContain('&filePosition=') + expect(calls[0][2]).toMatchObject({ method: 'GET' }) + expect(calls[0][2].headers?.Authorization).toBeUndefined() + // Attachment downloads are byte-capped rather than inheriting the global default. + expect(calls[0][2].maxResponseBytes).toBe(25 * 1024 * 1024) // DNS only resolved once — no second lookup that could rebind. expect(inputValidationMockFns.mockValidateUrlWithDNS).toHaveBeenCalledTimes(1) }) - it('propagates upstream errors and still calls logout', async () => { - inputValidationMockFns.mockSecureFetchWithPinnedIP - .mockResolvedValueOnce(mockSecureFetchResponse({ json: { access_token: 'tok-err' } })) - .mockResolvedValueOnce( - mockSecureFetchResponse({ ok: false, status: 404, text: 'Record not found' }) - ) - .mockResolvedValueOnce(mockSecureFetchResponse({})) + it('propagates upstream errors', async () => { + inputValidationMockFns.mockSecureFetchWithPinnedIP.mockResolvedValueOnce( + mockSecureFetchResponse({ ok: false, status: 404, text: 'Record not found' }) + ) const response = await POST(createMockRequest('POST', baseBody)) expect(response.status).toBe(404) const data = (await response.json()) as { success: false; error: string } expect(data.error).toContain('Record not found') + }) - // Logout still runs. - expect(inputValidationMockFns.mockSecureFetchWithPinnedIP).toHaveBeenCalledTimes(3) - expect(inputValidationMockFns.mockSecureFetchWithPinnedIP.mock.calls[2][0]).toContain( - '/ewws/EWLogout' + it('rejects an EWREST error document returned in place of file bytes', async () => { + const errorBody = Buffer.from( + "EWREST_error='Search in table contracts returns no records for key 1';" + ) + inputValidationMockFns.mockSecureFetchWithPinnedIP.mockResolvedValueOnce( + mockSecureFetchResponse({ + arrayBuffer: errorBody.buffer.slice( + errorBody.byteOffset, + errorBody.byteOffset + errorBody.byteLength + ) as ArrayBuffer, + headers: new Headers({ 'content-type': 'text/html' }), + }) ) + + const response = await POST(createMockRequest('POST', baseBody)) + expect(response.status).toBe(502) + const data = (await response.json()) as { success: false; error: string } + expect(data.error).toContain('no records for key') }) }) diff --git a/apps/sim/app/api/tools/agiloft/retrieve/route.ts b/apps/sim/app/api/tools/agiloft/retrieve/route.ts index 539f0bf7c2e..1b09ce16944 100644 --- a/apps/sim/app/api/tools/agiloft/retrieve/route.ts +++ b/apps/sim/app/api/tools/agiloft/retrieve/route.ts @@ -7,12 +7,13 @@ import { checkInternalAuth } from '@/lib/auth/hybrid' import { secureFetchWithPinnedIP } from '@/lib/core/security/input-validation.server' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { buildRetrieveAttachmentUrl } from '@/tools/agiloft/utils' +import { isEwRestBody } from '@/tools/agiloft/ewrest' import { - agiloftLoginPinned, - agiloftLogoutPinned, - resolveAgiloftInstance, -} from '@/tools/agiloft/utils.server' + AGILOFT_MAX_ATTACHMENT_BYTES, + buildRetrieveAttachmentUrl, + describeAgiloftError, +} from '@/tools/agiloft/utils' +import { resolveAgiloftInstance } from '@/tools/agiloft/utils.server' export const dynamic = 'force-dynamic' @@ -63,10 +64,15 @@ export const POST = withRouteHandler(async (request: NextRequest) => { return NextResponse.json({ success: false, error: toError(error).message }, { status: 400 }) } - const token = await agiloftLoginPinned(data, resolvedIP) const base = data.instanceUrl.replace(/\/$/, '') - try { + { + /** + * EWRetrieve is the documented endpoint for this: GET on the legacy + * surface, authenticating from inline credentials, answering with the + * raw file bytes. It needs no login/logout pair, which also avoids two + * extra round trips against Agiloft's one-second per-call delay. + */ const url = buildRetrieveAttachmentUrl(base, data) logger.info(`[${requestId}] Downloading attachment from Agiloft`, { @@ -77,9 +83,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { const agiloftResponse = await secureFetchWithPinnedIP(url, resolvedIP, { method: 'GET', - headers: { - Authorization: `Bearer ${token}`, - }, + maxResponseBytes: AGILOFT_MAX_ATTACHMENT_BYTES, }) if (!agiloftResponse.ok) { @@ -88,7 +92,10 @@ export const POST = withRouteHandler(async (request: NextRequest) => { `[${requestId}] Agiloft retrieve error: ${agiloftResponse.status} - ${errorText}` ) return NextResponse.json( - { success: false, error: `Agiloft error: ${agiloftResponse.status} - ${errorText}` }, + { + success: false, + error: `Agiloft error ${agiloftResponse.status}: ${describeAgiloftError(errorText)}`, + }, { status: agiloftResponse.status } ) } @@ -107,6 +114,20 @@ export const POST = withRouteHandler(async (request: NextRequest) => { const arrayBuffer = await agiloftResponse.arrayBuffer() const fileBuffer = Buffer.from(arrayBuffer) + /** + * A refusal comes back as an EWREST assignment or a plain-text error + * rather than file bytes, so a body that parses as one is an error + * document, not an attachment. + */ + if (isEwRestBody(fileBuffer.subarray(0, 512).toString('utf8'))) { + const envelope = fileBuffer.toString('utf8').slice(0, 300) + logger.error(`[${requestId}] Agiloft refused the attachment retrieve`, { envelope }) + return NextResponse.json( + { success: false, error: `Agiloft error: ${envelope}` }, + { status: 502 } + ) + } + logger.info(`[${requestId}] Attachment downloaded successfully`, { name: fileName, size: fileBuffer.length, @@ -126,8 +147,6 @@ export const POST = withRouteHandler(async (request: NextRequest) => { }, }, }) - } finally { - await agiloftLogoutPinned(data.instanceUrl, data.knowledgeBase, token, resolvedIP) } } catch (error) { logger.error(`[${requestId}] Error retrieving Agiloft attachment:`, error) diff --git a/apps/sim/app/api/tools/agiloft/run_action_button/route.ts b/apps/sim/app/api/tools/agiloft/run_action_button/route.ts index d5cb673d5ac..f27fbe0685b 100644 --- a/apps/sim/app/api/tools/agiloft/run_action_button/route.ts +++ b/apps/sim/app/api/tools/agiloft/run_action_button/route.ts @@ -8,8 +8,8 @@ import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { parseEwRest } from '@/tools/agiloft/ewrest' import type { AgiloftRunActionButtonResponse } from '@/tools/agiloft/types' -import { buildRunActionButtonUrl } from '@/tools/agiloft/utils' -import { executeAgiloftRequest } from '@/tools/agiloft/utils.server' +import { buildRunActionButtonUrl, describeAgiloftError } from '@/tools/agiloft/utils' +import { executeEwRequest } from '@/tools/agiloft/utils.server' export const dynamic = 'force-dynamic' @@ -52,7 +52,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { if (!parsed.success) return parsed.response const params = parsed.data.body - const result = await executeAgiloftRequest( + const result = await executeEwRequest( params, (base) => ({ url: buildRunActionButtonUrl(base, params), @@ -68,7 +68,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { return { success: false, output: { recordId, callbackId: null }, - error: `Agiloft error: ${response.status} - ${body}`, + error: `Agiloft error ${response.status}: ${describeAgiloftError(body)}`, } } diff --git a/apps/sim/app/api/tools/agiloft/saved_search/route.ts b/apps/sim/app/api/tools/agiloft/saved_search/route.ts new file mode 100644 index 00000000000..008156693b8 --- /dev/null +++ b/apps/sim/app/api/tools/agiloft/saved_search/route.ts @@ -0,0 +1,108 @@ +import { createLogger } from '@sim/logger' +import { toError } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { agiloftSavedSearchContract } from '@/lib/api/contracts/tools/agiloft' +import { getValidationErrorMessage, parseRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import type { AgiloftSavedSearchResponse } from '@/tools/agiloft/types' +import { buildSavedSearchUrl } from '@/tools/agiloft/utils' +import { + executeAgiloftRequest, + isAgiloftRefusal, + readAlrestJson, +} from '@/tools/agiloft/utils.server' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('AgiloftSavedSearchAPI') + +interface SavedSearchRow { + name?: string + label?: string + id?: number + description?: string +} + +export const POST = withRouteHandler(async (request: NextRequest) => { + const requestId = generateRequestId() + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + + if (!authResult.success || !authResult.userId) { + logger.warn(`[${requestId}] Unauthorized Agiloft saved_search attempt: ${authResult.error}`) + return NextResponse.json( + { success: false, error: authResult.error || 'Authentication required' }, + { status: 401 } + ) + } + + const parsed = await parseRequest( + agiloftSavedSearchContract, + request, + {}, + { + validationErrorResponse: (error) => { + logger.warn(`[${requestId}] Invalid request data`, { errors: error.issues }) + return NextResponse.json( + { + success: false, + error: getValidationErrorMessage(error, 'Invalid request data'), + details: error.issues, + }, + { status: 400 } + ) + }, + } + ) + if (!parsed.success) return parsed.response + const params = parsed.data.body + + /** + * EWSavedSearch must run under EWLogin or OAuth authorization, so this goes + * through the token-bearing executor rather than the inline-credential one + * the other legacy operations use. + */ + const result = await executeAgiloftRequest( + params, + (base) => ({ + url: buildSavedSearchUrl(base, params), + method: 'GET', + headers: { Accept: 'application/json' }, + }), + async (response) => { + const rows = (await readAlrestJson(response)) ?? [] + + const searches = rows.map((row) => ({ + name: row.name ?? '', + label: row.label ?? row.name ?? '', + id: row.id ?? null, + description: row.description ?? null, + })) + + return { success: true, output: { searches, totalCount: searches.length } } + } + ) + + return NextResponse.json(result) + } catch (error) { + /** + * A refusal Agiloft already decided on is a final answer, not a transient + * fault — returning 500 would make the tool runner retry it. + */ + if (isAgiloftRefusal(error)) { + logger.warn(`[${requestId}] Agiloft refused the request`, { error: error.message }) + return NextResponse.json({ + success: false, + output: { searches: [], totalCount: 0 }, + error: error.message, + }) + } + + logger.error(`[${requestId}] Error listing Agiloft saved searches:`, error) + + return NextResponse.json({ success: false, error: toError(error).message }, { status: 500 }) + } +}) diff --git a/apps/sim/app/api/tools/agiloft/search_records/route.ts b/apps/sim/app/api/tools/agiloft/search_records/route.ts index 48d94e8135a..2e7396deee6 100644 --- a/apps/sim/app/api/tools/agiloft/search_records/route.ts +++ b/apps/sim/app/api/tools/agiloft/search_records/route.ts @@ -1,15 +1,19 @@ import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' +import { filterUndefined } from '@sim/utils/object' import { type NextRequest, NextResponse } from 'next/server' import { agiloftSearchRecordsContract } from '@/lib/api/contracts/tools/agiloft' import { getValidationErrorMessage, parseRequest } from '@/lib/api/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { parseEwRest, toSearchRecords } from '@/tools/agiloft/ewrest' import type { AgiloftSearchResponse } from '@/tools/agiloft/types' -import { buildSearchRecordsUrl } from '@/tools/agiloft/utils' -import { executeAgiloftRequest } from '@/tools/agiloft/utils.server' +import { AGILOFT_MAX_SEARCH_RECORDS, alrestSearchUrl, parseFieldList } from '@/tools/agiloft/utils' +import { + executeAlrestRequest, + isAgiloftRefusal, + readAlrestJson, +} from '@/tools/agiloft/utils.server' export const dynamic = 'force-dynamic' @@ -50,52 +54,64 @@ export const POST = withRouteHandler(async (request: NextRequest) => { if (!parsed.success) return parsed.response const params = parsed.data.body - const result = await executeAgiloftRequest( + const page = params.page ? Number(params.page) : 0 + const limit = params.limit ? Number(params.limit) : 0 + + const result = await executeAlrestRequest( params, (base) => ({ - url: buildSearchRecordsUrl(base, params), - method: 'GET', + url: alrestSearchUrl(base, params.table), + method: 'POST', + headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, + body: JSON.stringify( + filterUndefined({ + search: params.search?.trim() || undefined, + query: params.query?.trim() || undefined, + field: parseFieldList(params.fields), + page: params.page ? page : undefined, + limit: params.limit ? limit : undefined, + }) + ), }), async (response) => { - const body = await response.text() - const page = params.page ? Number(params.page) : 0 - const limit = params.limit ? Number(params.limit) : 0 - - if (!response.ok) { - return { - success: false, - output: { records: [], totalCount: 0, page, limit }, - error: `Agiloft error: ${response.status} - ${body}`, - } - } + const returned = (await readAlrestJson[]>(response)) ?? [] + const records = returned.slice(0, AGILOFT_MAX_SEARCH_RECORDS) - /** - * EWSearch answers with EWREST_length plus one EWREST__ - * assignment per field per row, and reports an empty result set as - * EWREST_id_length = '0'. A body with no assignments at all is therefore - * never a legitimate empty search — it is a refusal Agiloft returned - * with HTTP 200, such as an invalid query or an unknown saved search. - */ - const values = parseEwRest(body) - if (values.size === 0) { - return { - success: false, - output: { records: [], totalCount: 0, page, limit }, - error: `Agiloft did not return search results: ${body.trim() || '(empty response)'}`, - } + if (returned.length > records.length) { + logger.warn( + `[${requestId}] Agiloft search returned ${returned.length} records; truncated to ${AGILOFT_MAX_SEARCH_RECORDS}`, + { table: params.table } + ) } - const { records, count } = toSearchRecords(values) - return { success: true, - output: { records, totalCount: count, page, limit }, + output: { + records, + totalCount: records.length, + page, + limit, + truncated: returned.length > records.length, + }, } } ) return NextResponse.json(result) } catch (error) { + /** + * A refusal Agiloft already decided on is a final answer, not a transient + * fault — returning 500 would make the tool runner retry it. + */ + if (isAgiloftRefusal(error)) { + logger.warn(`[${requestId}] Agiloft refused the request`, { error: error.message }) + return NextResponse.json({ + success: false, + output: { records: [], totalCount: 0, page: 0, limit: 0, truncated: false }, + error: error.message, + }) + } + logger.error(`[${requestId}] Error searching Agiloft records:`, error) return NextResponse.json({ success: false, error: toError(error).message }, { status: 500 }) diff --git a/apps/sim/app/api/tools/agiloft/select_records/route.ts b/apps/sim/app/api/tools/agiloft/select_records/route.ts index db1a546b2b4..dd7be15039e 100644 --- a/apps/sim/app/api/tools/agiloft/select_records/route.ts +++ b/apps/sim/app/api/tools/agiloft/select_records/route.ts @@ -8,8 +8,13 @@ import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { parseEwRest, toRecordIds } from '@/tools/agiloft/ewrest' import type { AgiloftSelectResponse } from '@/tools/agiloft/types' -import { buildSelectRecordsUrl } from '@/tools/agiloft/utils' -import { executeAgiloftRequest } from '@/tools/agiloft/utils.server' +import { + AGILOFT_MAX_SELECT_IDS, + buildSelectRecordsUrl, + describeAgiloftError, + ewCredentialBody, +} from '@/tools/agiloft/utils' +import { executeEwRequest } from '@/tools/agiloft/utils.server' export const dynamic = 'force-dynamic' @@ -50,11 +55,18 @@ export const POST = withRouteHandler(async (request: NextRequest) => { if (!parsed.success) return parsed.response const params = parsed.data.body - const result = await executeAgiloftRequest( + const result = await executeEwRequest( params, (base) => ({ url: buildSelectRecordsUrl(base, params), - method: 'GET', + /** + * POST with the credentials in the body: EWSelect is one of the five + * operations that support it, and it keeps the password out of the URL + * and out of the server's access logs. + */ + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: ewCredentialBody(params), }), async (response) => { const body = await response.text() @@ -62,8 +74,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => { if (!response.ok) { return { success: false, - output: { recordIds: [], totalCount: 0 }, - error: `Agiloft error: ${response.status} - ${body}`, + output: { recordIds: [], totalCount: 0, truncated: false }, + error: `Agiloft error ${response.status}: ${describeAgiloftError(body)}`, } } @@ -78,14 +90,29 @@ export const POST = withRouteHandler(async (request: NextRequest) => { if (values.size === 0) { return { success: false, - output: { recordIds: [], totalCount: 0 }, + output: { recordIds: [], totalCount: 0, truncated: false }, error: `Agiloft did not return a result set: ${body.trim() || '(empty response)'}`, } } - const { recordIds, count } = toRecordIds(values) + const { recordIds } = toRecordIds(values) + const capped = recordIds.slice(0, AGILOFT_MAX_SELECT_IDS) + + if (recordIds.length > capped.length) { + logger.warn( + `[${requestId}] Agiloft select returned ${recordIds.length} IDs; truncated to ${AGILOFT_MAX_SELECT_IDS}`, + { table: params.table } + ) + } - return { success: true, output: { recordIds, totalCount: count } } + return { + success: true, + output: { + recordIds: capped, + totalCount: capped.length, + truncated: recordIds.length > capped.length, + }, + } } ) diff --git a/apps/sim/app/api/tools/agiloft/update_record/route.ts b/apps/sim/app/api/tools/agiloft/update_record/route.ts index c8ceaba5d0a..2ff0102200a 100644 --- a/apps/sim/app/api/tools/agiloft/update_record/route.ts +++ b/apps/sim/app/api/tools/agiloft/update_record/route.ts @@ -6,10 +6,13 @@ import { getValidationErrorMessage, parseRequest } from '@/lib/api/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { parseEwRest, toRecord } from '@/tools/agiloft/ewrest' import type { AgiloftRecordResponse } from '@/tools/agiloft/types' -import { buildUpdateRecordUrl, recordUrlLengthError } from '@/tools/agiloft/utils' -import { executeAgiloftRequest } from '@/tools/agiloft/utils.server' +import { alrestRecordUrl } from '@/tools/agiloft/utils' +import { + executeAlrestRequest, + isAgiloftRefusal, + readAlrestJson, +} from '@/tools/agiloft/utils.server' export const dynamic = 'force-dynamic' @@ -65,52 +68,40 @@ export const POST = withRouteHandler(async (request: NextRequest) => { }) } - const oversized = recordUrlLengthError(params.instanceUrl, (base) => - buildUpdateRecordUrl(base, params, fieldValues) - ) - if (oversized) { - return NextResponse.json({ - success: false, - output: { id: null, fields: {} }, - error: oversized, - }) - } - - const result = await executeAgiloftRequest( + const result = await executeAlrestRequest( params, (base) => ({ - url: buildUpdateRecordUrl(base, params, fieldValues), - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + url: alrestRecordUrl(base, params.table, params.recordId), + method: 'PUT', + headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, + body: JSON.stringify(fieldValues), }), async (response) => { - const body = await response.text() - - if (!response.ok) { - return { - success: false, - output: { id: null, fields: {} }, - error: `Agiloft error: ${response.status} - ${body}`, - } - } + const record = await readAlrestJson>(response) + const id = record?.id ?? params.recordId.trim() - /** EWUpdate echoes the whole record back as EWREST_ assignments. */ - const values = parseEwRest(body) - if (values.size === 0) { - return { - success: false, - output: { id: null, fields: {} }, - error: `Agiloft returned no record data: ${body.trim() || '(empty response)'}`, - } + return { + success: true, + output: { id: String(id), fields: record ?? {} }, } - - const { id, fields } = toRecord(values) - return { success: true, output: { id, fields } } } ) return NextResponse.json(result) } catch (error) { + /** + * A refusal Agiloft already decided on is a final answer, not a transient + * fault — returning 500 would make the tool runner retry it. + */ + if (isAgiloftRefusal(error)) { + logger.warn(`[${requestId}] Agiloft refused the request`, { error: error.message }) + return NextResponse.json({ + success: false, + output: { id: null, fields: {} }, + error: error.message, + }) + } + logger.error(`[${requestId}] Error updating Agiloft record:`, error) return NextResponse.json({ success: false, error: toError(error).message }, { status: 500 }) diff --git a/apps/sim/app/api/tools/agiloft/upsert_record/route.ts b/apps/sim/app/api/tools/agiloft/upsert_record/route.ts new file mode 100644 index 00000000000..891b9c91c84 --- /dev/null +++ b/apps/sim/app/api/tools/agiloft/upsert_record/route.ts @@ -0,0 +1,149 @@ +import { createLogger } from '@sim/logger' +import { toError } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { agiloftUpsertRecordContract } from '@/lib/api/contracts/tools/agiloft' +import { getValidationErrorMessage, parseRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { parseEwRest } from '@/tools/agiloft/ewrest' +import type { AgiloftUpsertRecordResponse } from '@/tools/agiloft/types' +import { + buildUpsertRecordBody, + buildUpsertRecordUrl, + describeAgiloftError, +} from '@/tools/agiloft/utils' +import { executeEwRequest } from '@/tools/agiloft/utils.server' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('AgiloftUpsertRecordAPI') + +export const POST = withRouteHandler(async (request: NextRequest) => { + const requestId = generateRequestId() + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + + if (!authResult.success || !authResult.userId) { + logger.warn(`[${requestId}] Unauthorized Agiloft upsert_record attempt: ${authResult.error}`) + return NextResponse.json( + { success: false, error: authResult.error || 'Authentication required' }, + { status: 401 } + ) + } + + const parsed = await parseRequest( + agiloftUpsertRecordContract, + request, + {}, + { + validationErrorResponse: (error) => { + logger.warn(`[${requestId}] Invalid request data`, { errors: error.issues }) + return NextResponse.json( + { + success: false, + error: getValidationErrorMessage(error, 'Invalid request data'), + details: error.issues, + }, + { status: 400 } + ) + }, + } + ) + if (!parsed.success) return parsed.response + const params = parsed.data.body + + let fieldValues: Record + try { + const parsedData = JSON.parse(params.data) + if (typeof parsedData !== 'object' || parsedData === null || Array.isArray(parsedData)) { + throw new Error('not an object') + } + fieldValues = parsedData as Record + } catch { + return NextResponse.json({ + success: false, + output: { id: null, created: false, callbackId: null }, + error: 'The data parameter must be a JSON object of field names to values', + }) + } + + let body: string + try { + body = buildUpsertRecordBody(params, fieldValues) + } catch (error) { + return NextResponse.json({ + success: false, + output: { id: null, created: false, callbackId: null }, + error: toError(error).message, + }) + } + + const result = await executeEwRequest( + params, + (base) => ({ + url: buildUpsertRecordUrl(base), + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body, + }), + async (response) => { + const body = await response.text() + + /** 409 means the match criteria selected more than one record. */ + if (response.status === 409) { + return { + success: false, + output: { id: null, created: false, callbackId: null }, + error: `Agiloft found more than one record matching "${params.match}", so it did not write: ${body.trim()}`, + } + } + + if (!response.ok) { + return { + success: false, + output: { id: null, created: false, callbackId: null }, + error: `Agiloft error ${response.status}: ${describeAgiloftError(body)}`, + } + } + + /** + * 202 is the documented acknowledgement for an asynchronous upsert. It + * carries no record ID, so it must not be read as a failed write. + */ + if (response.status === 202) { + const callbackId = parseEwRest(body).get('EWCALLBACK_ID') ?? null + + if (!callbackId) { + logger.warn( + `[${requestId}] Agiloft queued the upsert without a callback ID; the result cannot be polled`, + { body: body.slice(0, 200) } + ) + } + + return { success: true, output: { id: null, created: false, callbackId } } + } + + /** Documented response: EWREST_id='353'; with 201 on create, 200 on update. */ + const id = parseEwRest(body).get('id') + + if (id === undefined) { + return { + success: false, + output: { id: null, created: false, callbackId: null }, + error: `Agiloft did not return a record ID: ${body.trim() || '(empty response)'}`, + } + } + + return { success: true, output: { id, created: response.status === 201, callbackId: null } } + } + ) + + return NextResponse.json(result) + } catch (error) { + logger.error(`[${requestId}] Error upserting Agiloft record:`, error) + + return NextResponse.json({ success: false, error: toError(error).message }, { status: 500 }) + } +}) diff --git a/apps/sim/blocks/blocks/agiloft.ts b/apps/sim/blocks/blocks/agiloft.ts index 5f64f03b3dc..e98b61d6ba4 100644 --- a/apps/sim/blocks/blocks/agiloft.ts +++ b/apps/sim/blocks/blocks/agiloft.ts @@ -25,6 +25,10 @@ export const AgiloftBlock: BlockConfig = { defaultTitle: 'Agiloft', sentences: { byOperation: { + list_tables: [ + { text: 'List tables and fields in', field: 'knowledgeBase', core: true }, + { text: ', for', field: 'table' }, + ], create_record: [ { text: 'Create a record in', field: 'table', core: true }, { text: ', with', field: 'data' }, @@ -39,6 +43,11 @@ export const AgiloftBlock: BlockConfig = { { text: 'in', field: 'table' }, { text: ', setting', field: 'data' }, ], + upsert_record: [ + { text: 'Upsert a record in', field: 'table', core: true }, + { text: ', matching on', field: 'match' }, + { text: ', with', field: 'data' }, + ], delete_record: [ { text: 'Delete record', field: 'recordId', core: true }, { text: 'from', field: 'table' }, @@ -50,6 +59,11 @@ export const AgiloftBlock: BlockConfig = { { text: ', using saved search', field: 'search' }, { text: ', up to', field: 'limit', after: 'records' }, ], + nlp_search: [ + { text: 'Search', field: 'knowledgeBase', core: true }, + { text: 'for', field: 'nlpQuery' }, + { text: ', returning', field: 'fields' }, + ], select_records: [ { text: 'Select record IDs from', field: 'table', core: true }, { text: ', where', field: 'where' }, @@ -91,6 +105,11 @@ export const AgiloftBlock: BlockConfig = { { text: 'Run action button', field: 'actionButtonField', core: true }, { text: 'on record', field: 'recordId', core: true }, ], + saved_search: [{ text: 'List saved searches on', field: 'table', core: true }], + async_status: [ + { text: 'Check async call', field: 'callbackId', core: true }, + { text: 'on', field: 'table' }, + ], get_choice_line_id: [ { text: 'Resolve the internal ID of choice', field: 'value', core: true }, { text: 'on field', field: 'fieldName' }, @@ -106,11 +125,14 @@ export const AgiloftBlock: BlockConfig = { title: 'Operation', type: 'dropdown', options: [ + { label: 'List Tables & Fields', id: 'list_tables' }, { label: 'Create Record', id: 'create_record' }, { label: 'Read Record', id: 'read_record' }, { label: 'Update Record', id: 'update_record' }, + { label: 'Upsert Record', id: 'upsert_record' }, { label: 'Delete Record', id: 'delete_record' }, { label: 'Search Records', id: 'search_records' }, + { label: 'Natural Language Search', id: 'nlp_search' }, { label: 'Select Records', id: 'select_records' }, { label: 'Attach File', id: 'attach_file' }, { label: 'Retrieve Attachment', id: 'retrieve_attachment' }, @@ -118,6 +140,8 @@ export const AgiloftBlock: BlockConfig = { { label: 'Attachment Info', id: 'attachment_info' }, { label: 'Lock Record', id: 'lock_record' }, { label: 'Run Action Button', id: 'run_action_button' }, + { label: 'Async Status', id: 'async_status' }, + { label: 'Saved Search', id: 'saved_search' }, { label: 'Get Choice Line ID', id: 'get_choice_line_id' }, ], value: () => 'search_records', @@ -156,7 +180,12 @@ export const AgiloftBlock: BlockConfig = { title: 'Table', type: 'short-input', placeholder: 'e.g., contracts, contacts.employees', - required: true, + /** + * Optional only for List Tables, where an empty value means "describe + * every table" — that operation exists precisely for users who do not yet + * know the logical names. + */ + required: { field: 'operation', value: ['list_tables', 'nlp_search'], not: true }, }, { id: 'recordId', @@ -197,8 +226,8 @@ export const AgiloftBlock: BlockConfig = { title: 'Record Data', type: 'long-input', placeholder: '{"field_name": "value", "another_field": "value"}', - condition: { field: 'operation', value: ['create_record', 'update_record'] }, - required: { field: 'operation', value: ['create_record', 'update_record'] }, + condition: { field: 'operation', value: ['create_record', 'update_record', 'upsert_record'] }, + required: { field: 'operation', value: ['create_record', 'update_record', 'upsert_record'] }, wandConfig: { enabled: true, prompt: @@ -206,6 +235,38 @@ export const AgiloftBlock: BlockConfig = { generationType: 'json-object', }, }, + { + id: 'match', + title: 'Match Field', + type: 'short-input', + placeholder: 'e.g., ext_id', + condition: { field: 'operation', value: 'upsert_record' }, + required: { field: 'operation', value: 'upsert_record' }, + }, + { + id: 'includeLinkedInfo', + title: 'Include Linked Field Details', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + mode: 'advanced', + condition: { field: 'operation', value: 'list_tables' }, + }, + { + id: 'skipColumnsInfo', + title: 'Table Names Only', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + mode: 'advanced', + condition: { field: 'operation', value: 'list_tables' }, + }, { id: 'query', title: 'Search Query', @@ -215,9 +276,64 @@ export const AgiloftBlock: BlockConfig = { wandConfig: { enabled: true, prompt: - "Generate an Agiloft EWSearch query. Use field_name='value' for exact match, field_name~='value' for contains, != for not equals, and <, <=, >, >= for comparisons. Combine conditions with && for and, || for or. Quote every value in single quotes, and quote field labels that contain spaces. Return ONLY the query string - no explanations, no extra text.", + "Generate an Agiloft EWSearch query. Use field_name='value' for exact match, field_name~='value' for contains, != for not equals, and <, <=, >, >= for comparisons. Combine conditions with && for and, || for or. Quote every value in single quotes, quote field labels that contain spaces, and use null to match an empty field. Return ONLY the query string - no explanations, no extra text.", }, }, + { + id: 'nlpQuery', + title: 'Natural Language Query', + type: 'long-input', + placeholder: 'Active NDAs submitted last month', + condition: { field: 'operation', value: 'nlp_search' }, + required: { field: 'operation', value: 'nlp_search' }, + mode: 'advanced', + wandConfig: { + enabled: true, + prompt: + 'Rewrite the request as a plain-language Agiloft search, e.g. "Show me open, high-priority contracts". Do not use field names or operators. Return ONLY the sentence - no explanations, no extra text.', + }, + }, + { + id: 'substituteIds', + title: 'Substitute Record IDs', + type: 'short-input', + placeholder: 'Comma-separated IDs that adopt the dependants', + mode: 'advanced', + condition: { + field: 'operation', + value: 'delete_record', + and: { field: 'deleteRule', value: 'REPLACE_WITH_ANOTHER' }, + }, + required: { + field: 'operation', + value: 'delete_record', + and: { field: 'deleteRule', value: 'REPLACE_WITH_ANOTHER' }, + }, + }, + { + id: 'overwrite', + title: 'Replace Existing Files', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + mode: 'advanced', + condition: { field: 'operation', value: 'attach_file' }, + }, + { + id: 'async', + title: 'Queue Instead of Waiting', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + mode: 'advanced', + condition: { field: 'operation', value: 'upsert_record' }, + }, { id: 'search', title: 'Saved Search', @@ -272,6 +388,14 @@ export const AgiloftBlock: BlockConfig = { condition: { field: 'operation', value: 'run_action_button' }, required: { field: 'operation', value: 'run_action_button' }, }, + { + id: 'callbackId', + title: 'Callback ID', + type: 'short-input', + placeholder: 'e.g., 10100_1', + condition: { field: 'operation', value: 'async_status' }, + required: { field: 'operation', value: 'async_status' }, + }, { id: 'value', title: 'Choice Value', @@ -346,6 +470,7 @@ export const AgiloftBlock: BlockConfig = { { label: 'Delete, otherwise unlink', id: 'DELETE_WHERE_POSSIBLE_OTHERWISE_UNLINK' }, { label: 'Unlink dependents', id: 'APPLY_UNLINK' }, { label: 'Unlink, otherwise delete', id: 'UNLINK_WHERE_POSSIBLE_OTHERWISE_DELETE' }, + { label: 'Reassign dependents to another record', id: 'REPLACE_WITH_ANOTHER' }, ], value: () => 'ERROR_IF_DEPENDANTS', condition: { field: 'operation', value: 'delete_record' }, @@ -370,13 +495,19 @@ export const AgiloftBlock: BlockConfig = { id: 'fields', title: 'Fields', type: 'short-input', - placeholder: 'Comma-separated field names to return', + placeholder: 'e.g., id, contract_title1, company_name', + /** + * Advanced for the operations where it narrows an existing result, but + * Natural Language Search cannot run without it — the endpoint requires + * the field list. + */ + condition: { field: 'operation', value: ['read_record', 'search_records', 'nlp_search'] }, + required: { field: 'operation', value: 'nlp_search' }, mode: 'advanced', - condition: { field: 'operation', value: ['read_record', 'search_records'] }, wandConfig: { enabled: true, prompt: - 'Generate a comma-separated list of Agiloft table field names to include in the response. Return ONLY the comma-separated list - no explanations, no extra text.', + 'Generate a comma-separated list of Agiloft table field names to return. Keeping this short matters — an unfiltered contract record is roughly 184KB. Return ONLY the comma-separated list - no explanations, no extra text.', }, }, { @@ -384,6 +515,7 @@ export const AgiloftBlock: BlockConfig = { title: 'Page', type: 'short-input', placeholder: '0', + description: 'Zero-based page number', mode: 'advanced', condition: { field: 'operation', value: 'search_records' }, }, @@ -392,6 +524,7 @@ export const AgiloftBlock: BlockConfig = { title: 'Limit', type: 'short-input', placeholder: '25', + description: 'Records per page. 0 means every record, returned on page 0.', mode: 'advanced', condition: { field: 'operation', value: 'search_records' }, }, @@ -399,21 +532,24 @@ export const AgiloftBlock: BlockConfig = { tools: { access: [ + 'agiloft_async_status', 'agiloft_attach_file', 'agiloft_attachment_info', 'agiloft_create_record', 'agiloft_delete_record', 'agiloft_get_choice_line_id', + 'agiloft_list_tables', 'agiloft_lock_record', + 'agiloft_nlp_search', 'agiloft_read_record', 'agiloft_remove_attachment', 'agiloft_retrieve_attachment', 'agiloft_run_action_button', - // Retired, but retained so blocks saved with operation='saved_search' still resolve. 'agiloft_saved_search', 'agiloft_search_records', 'agiloft_select_records', 'agiloft_update_record', + 'agiloft_upsert_record', ], config: { tool: (params) => `agiloft_${params.operation}`, @@ -424,8 +560,16 @@ export const AgiloftBlock: BlockConfig = { if (normalizedFile) { params.file = normalizedFile } - if (params.force !== undefined) { - params.force = params.force === true || params.force === 'true' + for (const flag of [ + 'force', + 'includeLinkedInfo', + 'skipColumnsInfo', + 'overwrite', + 'async', + ] as const) { + if (params[flag] !== undefined) { + params[flag] = params[flag] === true || params[flag] === 'true' + } } return params }, @@ -441,11 +585,28 @@ export const AgiloftBlock: BlockConfig = { table: { type: 'string', description: 'Table name' }, recordId: { type: 'string', description: 'Record ID' }, data: { type: 'string', description: 'Record data as JSON' }, + match: { type: 'string', description: 'Field used to match an existing record on upsert' }, + includeLinkedInfo: { + type: 'boolean', + description: 'Include the source table and column behind each linked field', + }, + skipColumnsInfo: { type: 'boolean', description: 'Return table names without field details' }, query: { type: 'string', description: 'Ad hoc EWSearch query' }, search: { type: 'string', description: 'Label of a saved search defined on the table' }, + nlpQuery: { type: 'string', description: 'Plain-language description of the records to find' }, + substituteIds: { + type: 'string', + description: 'Records that adopt dependants when the delete rule reassigns them', + }, + overwrite: { + type: 'boolean', + description: 'Replace the attachment field instead of appending', + }, + async: { type: 'boolean', description: 'Queue the upsert rather than waiting for it' }, where: { type: 'string', description: 'SQL WHERE clause for select' }, fieldName: { type: 'string', description: 'Attachment field name or choice field name' }, value: { type: 'string', description: 'Choice value to resolve to its line ID' }, + callbackId: { type: 'string', description: 'Callback ID of an asynchronous Agiloft call' }, actionButtonField: { type: 'string', description: 'Logical name of the field holding the action button to run', @@ -454,12 +615,15 @@ export const AgiloftBlock: BlockConfig = { fileName: { type: 'string', description: 'Name for the attached file' }, position: { type: 'string', description: 'Attachment position index' }, lockAction: { type: 'string', description: 'Lock action (lock, unlock, check)' }, - force: { type: 'boolean', description: 'Force an unlock held by another user (admins only)' }, + force: { type: 'boolean', description: 'Force an unlock held by another user' }, deleteRule: { type: 'string', description: 'How EWDelete treats records that depend on the one being deleted', }, - fields: { type: 'string', description: 'Fields to return' }, + fields: { + type: 'string', + description: 'Comma-separated fields to return; strongly recommended on large records', + }, page: { type: 'string', description: 'Page number' }, limit: { type: 'string', description: 'Results per page' }, }, @@ -470,7 +634,14 @@ export const AgiloftBlock: BlockConfig = { description: 'Record ID', condition: { field: 'operation', - value: ['create_record', 'read_record', 'update_record', 'delete_record', 'lock_record'], + value: [ + 'create_record', + 'read_record', + 'update_record', + 'upsert_record', + 'delete_record', + 'lock_record', + ], }, }, fields: { @@ -489,15 +660,22 @@ export const AgiloftBlock: BlockConfig = { records: { type: 'json', description: 'Array of matching records', - condition: { field: 'operation', value: 'search_records' }, + condition: { field: 'operation', value: ['search_records', 'nlp_search'] }, }, totalCount: { type: 'number', description: - 'Number of matching results. For a paginated search this counts the current page only', + 'Number of items returned by this call, which may be capped — not a total match count', condition: { field: 'operation', - value: ['search_records', 'select_records', 'attachment_info'], + value: [ + 'search_records', + 'select_records', + 'attachment_info', + 'saved_search', + 'list_tables', + 'nlp_search', + ], }, }, page: { @@ -508,7 +686,12 @@ export const AgiloftBlock: BlockConfig = { limit: { type: 'number', description: 'Page size that was requested; 0 when Agiloft chose the page size', - condition: { field: 'operation', value: 'search_records' }, + condition: { field: 'operation', value: ['search_records', 'nlp_search'] }, + }, + truncated: { + type: 'boolean', + description: 'True when the result was capped and more records exist upstream', + condition: { field: 'operation', value: ['search_records', 'select_records', 'nlp_search'] }, }, recordIds: { type: 'json', @@ -553,6 +736,11 @@ export const AgiloftBlock: BlockConfig = { description: 'Number of attachments remaining after removal', condition: { field: 'operation', value: 'remove_attachment' }, }, + tableId: { + type: 'number', + description: 'Numeric system identifier of the table holding the locked record', + condition: { field: 'operation', value: 'lock_record' }, + }, lockStatus: { type: 'string', description: 'Lock status: LOCKED when the record is held, NO_LOCK when it is free', @@ -568,10 +756,43 @@ export const AgiloftBlock: BlockConfig = { description: 'Minutes until the lock expires', condition: { field: 'operation', value: 'lock_record' }, }, + tables: { + type: 'json', + description: 'Tables and their fields (label, logicalName, fields[])', + condition: { field: 'operation', value: 'list_tables' }, + }, + created: { + type: 'boolean', + description: 'Whether the upsert created a new record rather than updating one', + condition: { field: 'operation', value: 'upsert_record' }, + }, + searches: { + type: 'json', + description: 'Saved searches on the table (name, label, id, description)', + condition: { field: 'operation', value: 'saved_search' }, + }, callbackId: { type: 'string', - description: 'Callback identifier Agiloft returns for the asynchronous action-button run', - condition: { field: 'operation', value: 'run_action_button' }, + description: 'Callback identifier for an asynchronous call, to pass to Async Status', + condition: { + field: 'operation', + value: ['run_action_button', 'async_status', 'upsert_record'], + }, + }, + statusCode: { + type: 'number', + description: 'Raw status code Agiloft returned for the asynchronous call', + condition: { field: 'operation', value: 'async_status' }, + }, + status: { + type: 'string', + description: 'completed, queued, in_progress, failed, or unknown_callback', + condition: { field: 'operation', value: 'async_status' }, + }, + complete: { + type: 'boolean', + description: 'True when the asynchronous operation has finished', + condition: { field: 'operation', value: 'async_status' }, }, choiceLineId: { type: 'number', diff --git a/apps/sim/lib/api/contracts/tools/agiloft.ts b/apps/sim/lib/api/contracts/tools/agiloft.ts index b86a13e0500..720a1e252e6 100644 --- a/apps/sim/lib/api/contracts/tools/agiloft.ts +++ b/apps/sim/lib/api/contracts/tools/agiloft.ts @@ -7,6 +7,17 @@ import type { import { defineRouteContract } from '@/lib/api/contracts/types' import { FileInputSchema } from '@/lib/uploads/utils/file-schemas' +/** + * Optional string inputs arrive as `null` when a block leaves the field blank, + * which a bare `z.string().optional()` rejects with "expected string, received + * null" before any request is made. Normalizing null to undefined keeps those + * fields genuinely optional. + */ +const optionalText = z + .string() + .nullish() + .transform((value) => value ?? undefined) + const agiloftFileOutputSchema = z.object({ name: z.string(), mimeType: z.string(), @@ -51,7 +62,14 @@ export const agiloftAttachBodySchema = z.object({ recordId: z.string().min(1, 'Record ID is required'), fieldName: z.string().min(1, 'Field name is required'), file: FileInputSchema.optional(), - fileName: z.string().optional(), + fileName: z + .string() + .nullish() + .transform((value) => value ?? undefined), + overwrite: z + .boolean() + .nullish() + .transform((value) => value ?? undefined), }) export const agiloftRetrieveContract = defineRouteContract({ @@ -90,10 +108,12 @@ export const agiloftCreateRecordBodySchema = z.object({ export const agiloftCreateRecordResponseSchema = z.object({ success: z.boolean(), - output: z.object({ - id: z.string().nullable(), - fields: z.record(z.string(), z.unknown()), - }), + output: z + .object({ + id: z.string().nullable(), + fields: z.record(z.string(), z.unknown()), + }) + .optional(), error: z.string().optional(), }) @@ -111,15 +131,17 @@ export type AgiloftCreateRecordResponse = ContractJsonResponse export type AgiloftUpdateRecordResponse = ContractJsonResponse -/** - * EWDelete requires a delete rule naming how dependent records are handled. - * `REPLACE_WITH_ANOTHER` is deliberately excluded — it additionally needs a - * `subs` list of substitute record IDs, which this tool does not model. - */ +/** EWDelete requires a delete rule naming how dependent records are handled. */ export const agiloftDeleteRecordBodySchema = z.object({ ...agiloftBaseFields, recordId: z.string().min(1, 'Record ID is required'), @@ -175,16 +195,21 @@ export const agiloftDeleteRecordBodySchema = z.object({ 'DELETE_WHERE_POSSIBLE_OTHERWISE_UNLINK', 'APPLY_UNLINK', 'UNLINK_WHERE_POSSIBLE_OTHERWISE_DELETE', + 'REPLACE_WITH_ANOTHER', ]) - .default('ERROR_IF_DEPENDANTS'), + .nullish() + .transform((value) => value ?? 'ERROR_IF_DEPENDANTS'), + substituteIds: optionalText, }) export const agiloftDeleteRecordResponseSchema = z.object({ success: z.boolean(), - output: z.object({ - id: z.string(), - deleted: z.boolean(), - }), + output: z + .object({ + id: z.string(), + deleted: z.boolean(), + }) + .optional(), error: z.string().optional(), }) @@ -205,17 +230,24 @@ export const agiloftLockRecordBodySchema = z.object({ lockAction: z.enum(['lock', 'unlock', 'check'], { message: 'Lock action must be "lock", "unlock", or "check"', }), - force: z.boolean().optional(), + force: z + .boolean() + .nullish() + .transform((value) => value ?? undefined), }) export const agiloftLockRecordResponseSchema = z.object({ success: z.boolean(), - output: z.object({ - id: z.string(), - lockStatus: z.string(), - lockedBy: z.string().nullable(), - lockExpiresInMinutes: z.number().nullable(), - }), + output: z + .object({ + id: z.string(), + tableId: z.number().nullable(), + /** Documented values are LOCKED and NO_LOCK; empty on a failed call. */ + lockStatus: z.string(), + lockedBy: z.string().nullable(), + lockExpiresInMinutes: z.number().nullable(), + }) + .optional(), error: z.string().optional(), }) @@ -231,18 +263,18 @@ export type AgiloftLockRecordBodyInput = ContractBodyInput /** - * EWSearch accepts a saved-search label (`search`), an ad hoc `query`, or both. + * Search accepts a saved-search label (`search`), an ad hoc `query`, or both. * At least one must be present, otherwise the call degenerates into an * unbounded scan of the whole table. */ export const agiloftSearchRecordsBodySchema = z .object({ ...agiloftBaseFields, - query: z.string().optional(), - search: z.string().optional(), - fields: z.string().optional(), - page: z.string().optional(), - limit: z.string().optional(), + query: optionalText, + search: optionalText, + fields: optionalText, + page: optionalText, + limit: optionalText, }) .superRefine((value, ctx) => { if (!value.query?.trim() && !value.search?.trim()) { @@ -256,12 +288,15 @@ export const agiloftSearchRecordsBodySchema = z export const agiloftSearchRecordsResponseSchema = z.object({ success: z.boolean(), - output: z.object({ - records: z.array(z.record(z.string(), z.unknown())), - totalCount: z.number(), - page: z.number(), - limit: z.number(), - }), + output: z + .object({ + records: z.array(z.record(z.string(), z.unknown())), + totalCount: z.number(), + page: z.number(), + limit: z.number(), + truncated: z.boolean(), + }) + .optional(), error: z.string().optional(), }) @@ -283,10 +318,13 @@ export const agiloftSelectRecordsBodySchema = z.object({ export const agiloftSelectRecordsResponseSchema = z.object({ success: z.boolean(), - output: z.object({ - recordIds: z.array(z.string()), - totalCount: z.number(), - }), + output: z + .object({ + recordIds: z.array(z.string()), + totalCount: z.number(), + truncated: z.boolean(), + }) + .optional(), error: z.string().optional(), }) @@ -309,16 +347,18 @@ export const agiloftAttachmentInfoBodySchema = z.object({ export const agiloftAttachmentInfoResponseSchema = z.object({ success: z.boolean(), - output: z.object({ - attachments: z.array( - z.object({ - position: z.number(), - name: z.string(), - size: z.number(), - }) - ), - totalCount: z.number(), - }), + output: z + .object({ + attachments: z.array( + z.object({ + position: z.number(), + name: z.string(), + size: z.number(), + }) + ), + totalCount: z.number(), + }) + .optional(), error: z.string().optional(), }) @@ -344,11 +384,13 @@ export const agiloftRemoveAttachmentBodySchema = z.object({ export const agiloftRemoveAttachmentResponseSchema = z.object({ success: z.boolean(), - output: z.object({ - recordId: z.string(), - fieldName: z.string(), - remainingAttachments: z.number(), - }), + output: z + .object({ + recordId: z.string(), + fieldName: z.string(), + remainingAttachments: z.number(), + }) + .optional(), error: z.string().optional(), }) @@ -375,9 +417,11 @@ export const agiloftGetChoiceLineIdBodySchema = z.object({ export const agiloftGetChoiceLineIdResponseSchema = z.object({ success: z.boolean(), - output: z.object({ - choiceLineId: z.number().nullable(), - }), + output: z + .object({ + choiceLineId: z.number().nullable(), + }) + .optional(), error: z.string().optional(), }) @@ -404,10 +448,12 @@ export const agiloftRunActionButtonBodySchema = z.object({ export const agiloftRunActionButtonResponseSchema = z.object({ success: z.boolean(), - output: z.object({ - recordId: z.string(), - callbackId: z.string().nullable(), - }), + output: z + .object({ + recordId: z.string(), + callbackId: z.string().nullable(), + }) + .optional(), error: z.string().optional(), }) @@ -425,3 +471,186 @@ export type AgiloftRunActionButtonBodyInput = ContractBodyInput< export type AgiloftRunActionButtonResponse = ContractJsonResponse< typeof agiloftRunActionButtonContract > + +export const agiloftSavedSearchBodySchema = z.object({ ...agiloftBaseFields }) + +export const agiloftSavedSearchResponseSchema = z.object({ + success: z.boolean(), + output: z + .object({ + searches: z.array( + z.object({ + name: z.string(), + label: z.string(), + id: z.number().nullable(), + description: z.string().nullable(), + }) + ), + totalCount: z.number(), + }) + .optional(), + error: z.string().optional(), +}) + +export const agiloftSavedSearchContract = defineRouteContract({ + method: 'POST', + path: '/api/tools/agiloft/saved_search', + body: agiloftSavedSearchBodySchema, + response: { mode: 'json', schema: agiloftSavedSearchResponseSchema }, +}) + +export type AgiloftSavedSearchBody = ContractBody +export type AgiloftSavedSearchBodyInput = ContractBodyInput +export type AgiloftSavedSearchResponse = ContractJsonResponse + +const agiloftCredentialFields = { + instanceUrl: z.string().min(1, 'Instance URL is required'), + knowledgeBase: z.string().min(1, 'Knowledge base is required'), + login: z.string().min(1, 'Login is required'), + password: z.string().min(1, 'Password is required'), +} as const + +/** EWTable is KB-scoped; `table` narrows the result rather than selecting it. */ +export const agiloftListTablesBodySchema = z.object({ + ...agiloftCredentialFields, + table: optionalText, + includeLinkedInfo: z + .boolean() + .nullish() + .transform((value) => value ?? undefined), + skipColumnsInfo: z + .boolean() + .nullish() + .transform((value) => value ?? undefined), +}) + +export const agiloftListTablesResponseSchema = z.object({ + success: z.boolean(), + output: z + .object({ + tables: z.array( + z.object({ + label: z.string(), + logicalName: z.string(), + fields: z.array( + z.object({ + columnName: z.string(), + columnLabel: z.string(), + columnType: z.string(), + columnTypeDomain: z.string(), + isLinked: z.boolean(), + }) + ), + }) + ), + totalCount: z.number(), + }) + .optional(), + error: z.string().optional(), +}) + +export const agiloftListTablesContract = defineRouteContract({ + method: 'POST', + path: '/api/tools/agiloft/list_tables', + body: agiloftListTablesBodySchema, + response: { mode: 'json', schema: agiloftListTablesResponseSchema }, +}) + +export type AgiloftListTablesBody = ContractBody +export type AgiloftListTablesBodyInput = ContractBodyInput +export type AgiloftListTablesResponse = ContractJsonResponse + +export const agiloftUpsertRecordBodySchema = z.object({ + ...agiloftBaseFields, + match: z.string().min(1, 'A match field is required'), + data: z.string().min(1, 'Data is required'), + async: z + .boolean() + .nullish() + .transform((value) => value ?? undefined), +}) + +export const agiloftUpsertRecordResponseSchema = z.object({ + success: z.boolean(), + output: z + .object({ + id: z.string().nullable(), + created: z.boolean(), + /** Present only for a queued (202) upsert, to poll with Async Status. */ + callbackId: z.string().nullable(), + }) + .optional(), + error: z.string().optional(), +}) + +export const agiloftUpsertRecordContract = defineRouteContract({ + method: 'POST', + path: '/api/tools/agiloft/upsert_record', + body: agiloftUpsertRecordBodySchema, + response: { mode: 'json', schema: agiloftUpsertRecordResponseSchema }, +}) + +export type AgiloftUpsertRecordBody = ContractBody +export type AgiloftUpsertRecordBodyInput = ContractBodyInput +export type AgiloftUpsertRecordResponse = ContractJsonResponse + +export const agiloftAsyncStatusBodySchema = z.object({ + ...agiloftBaseFields, + callbackId: z.string().min(1, 'Callback ID is required'), +}) + +export const agiloftAsyncStatusResponseSchema = z.object({ + success: z.boolean(), + output: z + .object({ + callbackId: z.string(), + statusCode: z.number(), + status: z.string(), + complete: z.boolean(), + }) + .optional(), + error: z.string().optional(), +}) + +export const agiloftAsyncStatusContract = defineRouteContract({ + method: 'POST', + path: '/api/tools/agiloft/async_status', + body: agiloftAsyncStatusBodySchema, + response: { mode: 'json', schema: agiloftAsyncStatusResponseSchema }, +}) + +export type AgiloftAsyncStatusBody = ContractBody +export type AgiloftAsyncStatusBodyInput = ContractBodyInput +export type AgiloftAsyncStatusResponse = ContractJsonResponse + +/** EWNLPSearch is knowledge-base scoped and takes no table. */ +export const agiloftNlpSearchBodySchema = z.object({ + ...agiloftCredentialFields, + nlpQuery: z.string().min(1, 'A natural language query is required'), + fields: z.string().min(1, 'At least one field to return is required'), + page: optionalText, + limit: optionalText, +}) + +export const agiloftNlpSearchResponseSchema = z.object({ + success: z.boolean(), + output: z + .object({ + records: z.array(z.record(z.string(), z.unknown())), + totalCount: z.number(), + truncated: z.boolean(), + }) + .optional(), + error: z.string().optional(), +}) + +export const agiloftNlpSearchContract = defineRouteContract({ + method: 'POST', + path: '/api/tools/agiloft/nlp_search', + body: agiloftNlpSearchBodySchema, + response: { mode: 'json', schema: agiloftNlpSearchResponseSchema }, +}) + +export type AgiloftNlpSearchBody = ContractBody +export type AgiloftNlpSearchBodyInput = ContractBodyInput +export type AgiloftNlpSearchResponse = ContractJsonResponse diff --git a/apps/sim/lib/integrations/integrations.json b/apps/sim/lib/integrations/integrations.json index b9b231d17bd..8895c5a4c87 100644 --- a/apps/sim/lib/integrations/integrations.json +++ b/apps/sim/lib/integrations/integrations.json @@ -280,6 +280,10 @@ "iconName": "AgiloftIcon", "docsUrl": "https://docs.sim.ai/integrations/agiloft", "operations": [ + { + "name": "List Tables & Fields", + "description": "List the tables and fields in an Agiloft knowledge base, to discover the logical names other operations need." + }, { "name": "Create Record", "description": "Create a new record in an Agiloft table." @@ -292,6 +296,10 @@ "name": "Update Record", "description": "Update an existing record in an Agiloft table." }, + { + "name": "Upsert Record", + "description": "Create an Agiloft record, or update it when a record already matches the given fields." + }, { "name": "Delete Record", "description": "Delete a record from an Agiloft table." @@ -300,6 +308,10 @@ "name": "Search Records", "description": "Search for records in an Agiloft table using a query." }, + { + "name": "Natural Language Search", + "description": "Search Agiloft records by describing what you want in plain language, such as \"active NDAs submitted last month\"." + }, { "name": "Select Records", "description": "Select record IDs matching a SQL WHERE clause from an Agiloft table." @@ -328,12 +340,20 @@ "name": "Run Action Button", "description": "Run an action button on an Agiloft record, such as an approval or send-for-signature step." }, + { + "name": "Async Status", + "description": "Check whether an asynchronous Agiloft call, such as a run action button, has completed." + }, + { + "name": "Saved Search", + "description": "List the saved searches defined for an Agiloft table." + }, { "name": "Get Choice Line ID", "description": "Resolve the internal numeric ID of a choice-list value, for use in EWSelect WHERE clauses against choice fields." } ], - "operationCount": 13, + "operationCount": 18, "triggers": [], "triggerCount": 0, "authType": "api-key", diff --git a/apps/sim/tools/agiloft/async_status.ts b/apps/sim/tools/agiloft/async_status.ts new file mode 100644 index 00000000000..2c702ced9cf --- /dev/null +++ b/apps/sim/tools/agiloft/async_status.ts @@ -0,0 +1,88 @@ +import type { AgiloftAsyncStatusParams, AgiloftAsyncStatusResponse } from '@/tools/agiloft/types' +import type { ToolConfig } from '@/tools/types' + +export const agiloftAsyncStatusTool: ToolConfig< + AgiloftAsyncStatusParams, + AgiloftAsyncStatusResponse +> = { + id: 'agiloft_async_status', + name: 'Agiloft Async Status', + description: + 'Check whether an asynchronous Agiloft call, such as a run action button, has completed.', + version: '1.0.0', + + params: { + instanceUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Agiloft instance URL (e.g., https://mycompany.agiloft.com)', + }, + knowledgeBase: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Knowledge base name', + }, + login: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Agiloft username', + }, + password: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Agiloft password', + }, + table: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Table the asynchronous call was made against', + }, + callbackId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Callback ID returned by the asynchronous call, e.g. from Run Action Button', + }, + }, + + request: { + url: () => '/api/tools/agiloft/async_status', + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => ({ + instanceUrl: params.instanceUrl, + knowledgeBase: params.knowledgeBase, + login: params.login, + password: params.password, + table: params.table, + callbackId: params.callbackId, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: data.success ?? true, + output: data.output, + ...(data.error ? { error: data.error } : {}), + } + }, + + outputs: { + callbackId: { type: 'string', description: 'Callback ID that was checked' }, + statusCode: { type: 'number', description: 'Raw status code Agiloft returned' }, + status: { + type: 'string', + description: 'completed, queued, in_progress, failed, or unknown_callback', + }, + complete: { + type: 'boolean', + description: 'True when the operation has finished, whether it succeeded or failed', + }, + }, +} diff --git a/apps/sim/tools/agiloft/attach_file.ts b/apps/sim/tools/agiloft/attach_file.ts index 03aa6ca74c0..1001968439b 100644 --- a/apps/sim/tools/agiloft/attach_file.ts +++ b/apps/sim/tools/agiloft/attach_file.ts @@ -53,7 +53,7 @@ export const agiloftAttachFileTool: ToolConfig { expect(values.size).toBe(1) expect(values.get('id')).toBe('353') }) - - it('reports a plain-text error body as not being an EWREST response', () => { - expect(isEwRestBody('Error executing query, please consult logs')).toBe(false) - expect(isEwRestBody(CREATE_BODY)).toBe(true) - }) -}) - -describe('toRecord', () => { - it('surfaces the id while keeping it among the fields', () => { - const { id, fields } = toRecord(parseEwRest(READ_BODY)) - - expect(id).toBe('358') - expect(fields.first_name).toBe('John') - expect(fields.id).toBe('358') - }) - - it('reports a missing id as null rather than inventing one', () => { - expect(toRecord(parseEwRest("EWREST_summary='no id here';")).id).toBeNull() - }) }) describe('toRecordIds', () => { @@ -100,46 +75,3 @@ describe('toRecordIds', () => { expect(toRecordIds(parseEwRest(SELECT_EMPTY_BODY))).toEqual({ recordIds: [], count: 0 }) }) }) - -describe('toSearchRecords', () => { - it('regroups the flat field_index assignments into one object per row', () => { - const { records, count } = toSearchRecords(parseEwRest(SEARCH_BODY)) - - expect(count).toBe(4) - expect(records).toHaveLength(4) - expect(records[0]).toEqual({ - summary: 'Here is a new service request with some tasks', - priority: 'High', - }) - expect(records[3].summary).toBe('Need New Wireless Card for Laptop') - }) - - it('keeps rows in index order regardless of assignment order', () => { - const shuffled = `EWREST_length = '2'; -EWREST_name_1='second'; -EWREST_name_0='first';` - - expect(toSearchRecords(parseEwRest(shuffled)).records.map((r) => r.name)).toEqual([ - 'first', - 'second', - ]) - }) - - it('does not mistake the length line for a record field', () => { - const { records } = toSearchRecords(parseEwRest(SEARCH_BODY)) - - for (const record of records) { - expect(record).not.toHaveProperty('length') - } - }) - - it('preserves fields whose own names end in a number', () => { - /** EWRead's documented sample includes _1576_company_name0. */ - const body = `EWREST_length = '1'; -EWREST__1576_company_name0_0='IBM';` - - expect(toSearchRecords(parseEwRest(body)).records[0]).toEqual({ - _1576_company_name0: 'IBM', - }) - }) -}) diff --git a/apps/sim/tools/agiloft/ewrest.ts b/apps/sim/tools/agiloft/ewrest.ts index 8c5b29c1563..de89b5101ff 100644 --- a/apps/sim/tools/agiloft/ewrest.ts +++ b/apps/sim/tools/agiloft/ewrest.ts @@ -1,7 +1,7 @@ /** * Parser for Agiloft's `EWREST_` response format. * - * Every `/ewws/EW*` operation answers with a body of JavaScript assignments + * The legacy `/ewws/EW*` operations answer with a body of JavaScript assignments * rather than JSON — the interface was designed to be `eval`-ed by a browser * client. The documented shapes are: * @@ -18,7 +18,13 @@ * accepted here. */ -const ASSIGNMENT = /^EWREST_(?[^=\s]+)\s*=\s*'(?[\s\S]*)';?$/ +/** + * Matches one `EWREST_key='value';` assignment anywhere in the body. Most + * responses put one per line, but EWActionButton documents both of its + * assignments on a single line, so the scan is not line-anchored. The value is + * non-greedy up to the closing `';` for the same reason. + */ +const ASSIGNMENT = /EWREST_(?[^=\s']+)\s*=\s*'(?[\s\S]*?)'\s*;/g /** * Parses a body into its raw `EWREST_` key/value pairs, preserving document @@ -28,14 +34,9 @@ const ASSIGNMENT = /^EWREST_(?[^=\s]+)\s*=\s*'(?[\s\S]*)';?$/ export function parseEwRest(body: string): Map { const values = new Map() - for (const rawLine of body.split(/\r?\n/)) { - const line = rawLine.trim() - if (!line) continue - - const match = ASSIGNMENT.exec(line) - const key = match?.groups?.key + for (const match of body.matchAll(ASSIGNMENT)) { + const key = match.groups?.key if (!key) continue - values.set(key, match.groups?.value ?? '') } @@ -43,74 +44,19 @@ export function parseEwRest(body: string): Map { } /** - * True when the body carries at least one `EWREST_` assignment. Agiloft answers - * some failures with HTTP 200 and a plain-text error, so callers use this to - * tell "no data" apart from "not an EWREST response at all". - */ -export function isEwRestBody(body: string): boolean { - return parseEwRest(body).size > 0 -} - -/** - * Splits a parsed record into its ID and the remaining field values. EWRead and - * EWUpdate both return the whole record with `id` among the fields. + * Reads the `EWREST_id_length` / `EWREST_id_` pairs EWSelect returns. A + * result of zero records is reported as `EWREST_id_length = '0';` with no + * indexed entries. */ -export function toRecord(values: Map): { - id: string | null - fields: Record -} { - const fields: Record = {} - for (const [key, value] of values) { - fields[key] = value - } - return { id: fields.id ?? null, fields } -} - /** - * Regroups the flat `EWREST__` assignments EWSearch returns into - * one object per record. `EWREST_length` gives the row count for the current - * page; when it is absent the highest observed index is used instead so a - * partial body still yields the rows it did contain. + * True when the body carries at least one `EWREST_` assignment. Used where a + * binary payload is expected, since a refusal arrives as an assignment or + * plain text instead of file bytes. */ -export function toSearchRecords(values: Map): { - records: Record[] - count: number -} { - const INDEXED = /^(?.+)_(?\d+)$/ - const byIndex = new Map>() - - for (const [key, value] of values) { - if (key === 'length' || key === 'id_length') continue - - const match = INDEXED.exec(key) - const field = match?.groups?.field - if (!field) continue - - const index = Number(match.groups?.index) - let record = byIndex.get(index) - if (!record) { - record = {} - byIndex.set(index, record) - } - record[field] = value - } - - const declared = Number(values.get('length') ?? values.get('id_length')) - const count = Number.isFinite(declared) ? declared : byIndex.size - - const records: Record[] = [] - for (const index of [...byIndex.keys()].sort((a, b) => a - b)) { - records.push(byIndex.get(index) as Record) - } - - return { records, count } +export function isEwRestBody(body: string): boolean { + return parseEwRest(body).size > 0 } -/** - * Reads the `EWREST_id_length` / `EWREST_id_` pairs EWSelect returns. A - * result of zero records is reported as `EWREST_id_length = '0';` with no - * indexed entries. - */ export function toRecordIds(values: Map): { recordIds: string[] count: number @@ -122,9 +68,11 @@ export function toRecordIds(values: Map): { recordIds.push(id) } - const declared = Number(values.get('id_length')) - return { - recordIds, - count: Number.isFinite(declared) ? declared : recordIds.length, - } + /** + * Report what was actually parsed rather than the declared length. A + * declared count that disagrees with the rows present means the body was + * truncated, and returning the larger number would hide that from callers + * who compare `totalCount` against `recordIds.length`. + */ + return { recordIds, count: recordIds.length } } diff --git a/apps/sim/tools/agiloft/index.ts b/apps/sim/tools/agiloft/index.ts index 9cea98f63b6..88f8cb30b0f 100644 --- a/apps/sim/tools/agiloft/index.ts +++ b/apps/sim/tools/agiloft/index.ts @@ -1,9 +1,12 @@ +export { agiloftAsyncStatusTool } from '@/tools/agiloft/async_status' export { agiloftAttachFileTool } from '@/tools/agiloft/attach_file' export { agiloftAttachmentInfoTool } from '@/tools/agiloft/attachment_info' export { agiloftCreateRecordTool } from '@/tools/agiloft/create_record' export { agiloftDeleteRecordTool } from '@/tools/agiloft/delete_record' export { agiloftGetChoiceLineIdTool } from '@/tools/agiloft/get_choice_line_id' +export { agiloftListTablesTool } from '@/tools/agiloft/list_tables' export { agiloftLockRecordTool } from '@/tools/agiloft/lock_record' +export { agiloftNlpSearchTool } from '@/tools/agiloft/nlp_search' export { agiloftReadRecordTool } from '@/tools/agiloft/read_record' export { agiloftRemoveAttachmentTool } from '@/tools/agiloft/remove_attachment' export { agiloftRetrieveAttachmentTool } from '@/tools/agiloft/retrieve_attachment' @@ -13,3 +16,4 @@ export { agiloftSearchRecordsTool } from '@/tools/agiloft/search_records' export { agiloftSelectRecordsTool } from '@/tools/agiloft/select_records' export * from '@/tools/agiloft/types' export { agiloftUpdateRecordTool } from '@/tools/agiloft/update_record' +export { agiloftUpsertRecordTool } from '@/tools/agiloft/upsert_record' diff --git a/apps/sim/tools/agiloft/list_tables.ts b/apps/sim/tools/agiloft/list_tables.ts new file mode 100644 index 00000000000..5beb97e94d7 --- /dev/null +++ b/apps/sim/tools/agiloft/list_tables.ts @@ -0,0 +1,131 @@ +import type { AgiloftListTablesParams, AgiloftListTablesResponse } from '@/tools/agiloft/types' +import type { ToolConfig } from '@/tools/types' + +export const agiloftListTablesTool: ToolConfig = + { + id: 'agiloft_list_tables', + name: 'Agiloft List Tables', + description: + 'List the tables and fields in an Agiloft knowledge base, to discover the logical names other operations need.', + version: '1.0.0', + + params: { + instanceUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Agiloft instance URL (e.g., https://mycompany.agiloft.com)', + }, + knowledgeBase: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Knowledge base name', + }, + login: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Agiloft username', + }, + password: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Agiloft password', + }, + table: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Logical name of a single table to describe (e.g., "contacts"). Leave empty to list every table in the knowledge base.', + }, + includeLinkedInfo: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Include the source table and column behind each linked field', + }, + skipColumnsInfo: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Return table names only, omitting field details, for a much smaller response', + }, + }, + + request: { + url: () => '/api/tools/agiloft/list_tables', + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => ({ + instanceUrl: params.instanceUrl, + knowledgeBase: params.knowledgeBase, + login: params.login, + password: params.password, + table: params.table, + includeLinkedInfo: params.includeLinkedInfo, + skipColumnsInfo: params.skipColumnsInfo, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: data.success ?? true, + output: data.output, + ...(data.error ? { error: data.error } : {}), + } + }, + + outputs: { + tables: { + type: 'array', + description: 'Tables in the knowledge base with their fields', + items: { + type: 'object', + properties: { + label: { type: 'string', description: 'Display name of the table' }, + logicalName: { + type: 'string', + description: 'Logical table name, as other Agiloft operations expect it', + }, + fields: { + type: 'array', + description: 'Fields on the table', + items: { + type: 'object', + properties: { + columnName: { type: 'string', description: 'Logical field name' }, + columnLabel: { type: 'string', description: 'Display label' }, + columnType: { type: 'string', description: 'SQL column type' }, + columnTypeDomain: { type: 'string', description: 'Agiloft field type' }, + required: { type: 'boolean', description: 'Whether the field is mandatory' }, + isLinked: { type: 'boolean', description: 'Whether the field is a linked field' }, + linkedInfo: { + type: 'array', + description: + 'Source table and column, when linked-field details were requested', + items: { + type: 'object', + properties: { + linkedTable: { type: 'string', description: 'Source table' }, + linkedColumn: { type: 'string', description: 'Source column' }, + }, + }, + }, + textFieldType: { + type: 'string', + description: 'Content type for text fields, e.g. text/plain', + optional: true, + }, + }, + }, + }, + }, + }, + }, + totalCount: { type: 'number', description: 'Number of tables returned' }, + }, + } diff --git a/apps/sim/tools/agiloft/lock_record.ts b/apps/sim/tools/agiloft/lock_record.ts index 582e884a376..0a4aec0e2ce 100644 --- a/apps/sim/tools/agiloft/lock_record.ts +++ b/apps/sim/tools/agiloft/lock_record.ts @@ -54,8 +54,7 @@ export const agiloftLockRecordTool: ToolConfig = { + id: 'agiloft_nlp_search', + name: 'Agiloft Natural Language Search', + description: + 'Search Agiloft records by describing what you want in plain language, such as "active NDAs submitted last month".', + version: '1.0.0', + + params: { + instanceUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Agiloft instance URL (e.g., https://mycompany.agiloft.com)', + }, + knowledgeBase: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Knowledge base name', + }, + login: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Agiloft username', + }, + password: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Agiloft password', + }, + nlpQuery: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'The request in plain language, e.g. "Show me open, high-priority contracts". Structured field filters are not accepted — use Search Records for those.', + }, + fields: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Comma-separated field names to return, e.g. "id, contract_title1, company_name"', + }, + page: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page number, starting from 0', + }, + limit: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Records per page', + }, + }, + + request: { + url: () => '/api/tools/agiloft/nlp_search', + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => ({ + instanceUrl: params.instanceUrl, + knowledgeBase: params.knowledgeBase, + login: params.login, + password: params.password, + nlpQuery: params.nlpQuery, + fields: params.fields, + page: params.page, + limit: params.limit, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: data.success ?? true, + output: data.output, + ...(data.error ? { error: data.error } : {}), + } + }, + + outputs: { + records: { type: 'json', description: 'Matching records with the requested field values' }, + totalCount: { type: 'number', description: 'Number of records in this response' }, + truncated: { + type: 'boolean', + description: 'True when more records were returned upstream than this call reports', + }, + }, +} diff --git a/apps/sim/tools/agiloft/run_action_button.ts b/apps/sim/tools/agiloft/run_action_button.ts index cd38841f62f..a1d9b927746 100644 --- a/apps/sim/tools/agiloft/run_action_button.ts +++ b/apps/sim/tools/agiloft/run_action_button.ts @@ -90,6 +90,7 @@ export const agiloftRunActionButtonTool: ToolConfig< }, callbackId: { type: 'string', + optional: true, description: 'Callback identifier for the asynchronous run, which Agiloft returns as EWCALLBACK_ID', }, diff --git a/apps/sim/tools/agiloft/saved_search.test.ts b/apps/sim/tools/agiloft/saved_search.test.ts deleted file mode 100644 index 2c29cb6dfca..00000000000 --- a/apps/sim/tools/agiloft/saved_search.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * @vitest-environment node - */ -import { describe, expect, it } from 'vitest' -import { agiloftSavedSearchTool } from '@/tools/agiloft/saved_search' -import toolIds from '@/tools/generated/tool-ids' - -describe('retired agiloft_saved_search', () => { - it('keeps its id registered so workflows saved with that operation still resolve a tool', () => { - expect(toolIds).toContain('agiloft_saved_search') - expect(agiloftSavedSearchTool.id).toBe('agiloft_saved_search') - }) - - it('fails with a migration hint instead of calling an undocumented endpoint', async () => { - const result = await agiloftSavedSearchTool.directExecution?.({}) - - expect(result?.success).toBe(false) - expect(result?.error).toContain('Search Records') - }) -}) diff --git a/apps/sim/tools/agiloft/saved_search.ts b/apps/sim/tools/agiloft/saved_search.ts index 76e7662a3e7..7b98077c560 100644 --- a/apps/sim/tools/agiloft/saved_search.ts +++ b/apps/sim/tools/agiloft/saved_search.ts @@ -1,88 +1,84 @@ import type { AgiloftSavedSearchParams, AgiloftSavedSearchResponse } from '@/tools/agiloft/types' import type { ToolConfig } from '@/tools/types' -/** - * Retired operation, kept registered so workflows saved while it was offered - * still resolve a tool instead of failing with "Tool not found". - * - * `EWSavedSearch` appears in Agiloft's Scope Parameter operation list, so the - * endpoint exists, but it has no documentation page — neither its URL - * parameters nor its response shape can be verified. The previous - * implementation guessed both and could only ever report an empty list, which - * reads as "this table has no saved searches". Running a saved search is now - * supported for real through the Search Records operation's Saved Search - * field, so this fails fast and points there rather than issuing a request - * whose behavior nobody can predict. - */ export const agiloftSavedSearchTool: ToolConfig< AgiloftSavedSearchParams, AgiloftSavedSearchResponse > = { id: 'agiloft_saved_search', - name: 'Agiloft Saved Search (retired)', - description: - 'Retired. Agiloft does not document an endpoint for listing saved searches — use the Search Records operation and set its Saved Search field instead.', + name: 'Agiloft Saved Search', + description: 'List the saved searches defined for an Agiloft table.', version: '1.0.0', params: { instanceUrl: { type: 'string', - required: false, + required: true, visibility: 'user-only', - description: 'Agiloft instance URL', + description: 'Agiloft instance URL (e.g., https://mycompany.agiloft.com)', }, knowledgeBase: { type: 'string', - required: false, + required: true, visibility: 'user-only', description: 'Knowledge base name', }, login: { type: 'string', - required: false, + required: true, visibility: 'user-only', description: 'Agiloft username', }, password: { type: 'string', - required: false, + required: true, visibility: 'user-only', description: 'Agiloft password', }, table: { type: 'string', - required: false, + required: true, visibility: 'user-or-llm', - description: 'Table name', + description: 'Logical table name to list saved searches for (e.g., "contract")', }, }, - /** Fails without a network call — there is no endpoint we can correctly call. */ - directExecution: async () => ({ - success: false, - output: { searches: [] }, - error: - 'The Agiloft "Saved Search" operation has been retired because Agiloft does not document an endpoint for listing saved searches. Switch this block to the "Search Records" operation and enter the saved search name in its Saved Search field.', - }), - request: { - url: () => '/api/tools/agiloft/search_records', + url: () => '/api/tools/agiloft/saved_search', method: 'POST', headers: () => ({ 'Content-Type': 'application/json' }), - body: () => ({}), + body: (params) => ({ + instanceUrl: params.instanceUrl, + knowledgeBase: params.knowledgeBase, + login: params.login, + password: params.password, + table: params.table, + }), }, - transformResponse: async () => ({ - success: false, - output: { searches: [] }, - error: 'The Agiloft "Saved Search" operation has been retired.', - }), + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: data.success ?? true, + output: data.output, + ...(data.error ? { error: data.error } : {}), + } + }, outputs: { searches: { type: 'array', - description: 'Always empty; this operation is retired', - items: { type: 'object' }, + description: 'Saved searches defined on the table', + items: { + type: 'object', + properties: { + name: { type: 'string', description: 'Internal saved search name' }, + label: { type: 'string', description: 'Display label, as used by Search Records' }, + id: { type: 'number', description: 'Saved search identifier in the Agiloft database' }, + description: { type: 'string', description: 'Saved search description' }, + }, + }, }, + totalCount: { type: 'number', description: 'Number of saved searches returned' }, }, } diff --git a/apps/sim/tools/agiloft/search_records.ts b/apps/sim/tools/agiloft/search_records.ts index dd5a0254b97..1bd31bbbd17 100644 --- a/apps/sim/tools/agiloft/search_records.ts +++ b/apps/sim/tools/agiloft/search_records.ts @@ -104,6 +104,10 @@ export const agiloftSearchRecordsTool: ToolConfig< }, outputs: { + truncated: { + type: 'boolean', + description: 'True when more records were returned upstream than this call reports', + }, records: { type: 'json', description: 'Array of matching records with their field values', @@ -111,7 +115,7 @@ export const agiloftSearchRecordsTool: ToolConfig< totalCount: { type: 'number', description: - 'Number of records reported by EWSearch. When paginating this is the count for the current page, not the whole result set.', + 'Number of records in this response. Not a total match count — compare with `truncated`.', }, page: { type: 'number', diff --git a/apps/sim/tools/agiloft/select_records.ts b/apps/sim/tools/agiloft/select_records.ts index 3db29587d19..c392ddc0b6f 100644 --- a/apps/sim/tools/agiloft/select_records.ts +++ b/apps/sim/tools/agiloft/select_records.ts @@ -74,6 +74,10 @@ export const agiloftSelectRecordsTool: ToolConfig< }, outputs: { + truncated: { + type: 'boolean', + description: 'True when more IDs matched than this call reports', + }, recordIds: { type: 'array', description: 'Array of record IDs matching the query', @@ -83,7 +87,7 @@ export const agiloftSelectRecordsTool: ToolConfig< }, totalCount: { type: 'number', - description: 'Total number of matching records', + description: 'Number of IDs in this response — compare with `truncated`', }, }, } diff --git a/apps/sim/tools/agiloft/types.ts b/apps/sim/tools/agiloft/types.ts index 3b70e5418dc..f21eb5c265c 100644 --- a/apps/sim/tools/agiloft/types.ts +++ b/apps/sim/tools/agiloft/types.ts @@ -1,14 +1,20 @@ import type { ToolResponse } from '@/tools/types' /** - * Base parameters shared by all Agiloft tools. - * Agiloft authenticates via instance URL, KB name, and user credentials. + * Connection and credentials. `table` is optional here because EWLogin is + * KB-scoped and EWTable operates across the whole knowledge base; every + * table-scoped operation uses `AgiloftBaseParams` below instead. */ -export interface AgiloftBaseParams { +export interface AgiloftCredentials { instanceUrl: string knowledgeBase: string login: string password: string + table?: string +} + +/** Credentials plus the table a table-scoped operation acts on. */ +export interface AgiloftBaseParams extends AgiloftCredentials { table: string } @@ -33,10 +39,13 @@ export type AgiloftDeleteRule = | 'DELETE_WHERE_POSSIBLE_OTHERWISE_UNLINK' | 'APPLY_UNLINK' | 'UNLINK_WHERE_POSSIBLE_OTHERWISE_DELETE' + | 'REPLACE_WITH_ANOTHER' export interface AgiloftDeleteRecordParams extends AgiloftBaseParams { recordId: string deleteRule?: AgiloftDeleteRule + /** Comma-separated substitute record IDs; read only under REPLACE_WITH_ANOTHER. */ + substituteIds?: string } export interface AgiloftSearchRecordsParams extends AgiloftBaseParams { @@ -82,6 +91,7 @@ export interface AgiloftSearchResponse extends ToolResponse { totalCount: number page: number limit: number + truncated: boolean } } @@ -89,6 +99,7 @@ export interface AgiloftSelectResponse extends ToolResponse { output: { recordIds: string[] totalCount: number + truncated: boolean } } @@ -106,6 +117,7 @@ export interface AgiloftAttachmentInfoResponse extends ToolResponse { export interface AgiloftLockResponse extends ToolResponse { output: { id: string + tableId: number | null lockStatus: string lockedBy: string | null lockExpiresInMinutes: number | null @@ -117,6 +129,7 @@ export interface AgiloftAttachFileParams extends AgiloftBaseParams { fieldName: string file?: unknown fileName?: string + overwrite?: boolean } export interface AgiloftAttachFileResponse extends ToolResponse { @@ -182,10 +195,92 @@ export interface AgiloftRunActionButtonResponse extends ToolResponse { } } -export type AgiloftSavedSearchParams = Partial +export type AgiloftSavedSearchParams = AgiloftBaseParams export interface AgiloftSavedSearchResponse extends ToolResponse { output: { - searches: unknown[] + searches: Array<{ + name: string + label: string + id: number | null + description: string | null + }> + totalCount: number + } +} + +/** + * EWTable is knowledge-base scoped: `table` narrows the result to one logical + * table rather than selecting the target, and is passed as `table` — not + * `$table` — per the documented example. + */ +export interface AgiloftListTablesParams extends AgiloftCredentials { + includeLinkedInfo?: boolean + skipColumnsInfo?: boolean +} + +export interface AgiloftTableField { + columnName: string + columnLabel: string + columnType: string + columnTypeDomain: string + required: boolean + isLinked: boolean + /** Populated only when includeLinkedInfo was requested and the field is linked. */ + linkedInfo: Array<{ linkedTable: string; linkedColumn: string }> + textFieldType: string | null +} + +export interface AgiloftListTablesResponse extends ToolResponse { + output: { + tables: Array<{ + label: string + logicalName: string + fields: AgiloftTableField[] + }> + totalCount: number + } +} + +export interface AgiloftUpsertRecordParams extends AgiloftBaseParams { + match: string + data: string + async?: boolean +} + +export interface AgiloftUpsertRecordResponse extends ToolResponse { + output: { + id: string | null + created: boolean + callbackId: string | null + } +} + +export interface AgiloftAsyncStatusParams extends AgiloftBaseParams { + callbackId: string +} + +export interface AgiloftAsyncStatusResponse extends ToolResponse { + output: { + callbackId: string + statusCode: number + status: string + complete: boolean + } +} + +/** EWNLPSearch is KB-scoped; the table comes from the KB's chat-search configuration. */ +export interface AgiloftNlpSearchParams extends AgiloftCredentials { + nlpQuery: string + fields: string + page?: string + limit?: string +} + +export interface AgiloftNlpSearchResponse extends ToolResponse { + output: { + records: Record[] + totalCount: number + truncated: boolean } } diff --git a/apps/sim/tools/agiloft/upsert_record.ts b/apps/sim/tools/agiloft/upsert_record.ts new file mode 100644 index 00000000000..d8421ea510c --- /dev/null +++ b/apps/sim/tools/agiloft/upsert_record.ts @@ -0,0 +1,105 @@ +import type { AgiloftUpsertRecordParams, AgiloftUpsertRecordResponse } from '@/tools/agiloft/types' +import type { ToolConfig } from '@/tools/types' + +export const agiloftUpsertRecordTool: ToolConfig< + AgiloftUpsertRecordParams, + AgiloftUpsertRecordResponse +> = { + id: 'agiloft_upsert_record', + name: 'Agiloft Upsert Record', + description: + 'Create an Agiloft record, or update it when a record already matches the given fields.', + version: '1.0.0', + + params: { + instanceUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Agiloft instance URL (e.g., https://mycompany.agiloft.com)', + }, + knowledgeBase: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Knowledge base name', + }, + login: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Agiloft username', + }, + password: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Agiloft password', + }, + table: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Table name (e.g., "contracts", "contacts.employees")', + }, + match: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Field used to find an existing record (e.g., "ext_id"). Pick something that identifies a record uniquely — if more than one record matches, Agiloft writes nothing and returns a conflict.', + }, + async: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: + 'Queue the write instead of waiting for it. Returns a callback ID instead of a record ID; pass that to Async Status to poll the result.', + }, + data: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Field values as a JSON object. On create these populate the new record; on update only the supplied fields change.', + }, + }, + + request: { + url: () => '/api/tools/agiloft/upsert_record', + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => ({ + instanceUrl: params.instanceUrl, + knowledgeBase: params.knowledgeBase, + login: params.login, + password: params.password, + table: params.table, + match: params.match, + data: params.data, + async: params.async, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: data.success ?? true, + output: data.output, + ...(data.error ? { error: data.error } : {}), + } + }, + + outputs: { + id: { type: 'string', description: 'ID of the created or updated record' }, + created: { + type: 'boolean', + description: 'True when a new record was created, false when an existing one was updated', + }, + callbackId: { + type: 'string', + description: 'Returned for a queued upsert; pass it to Async Status to poll the result', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/agiloft/utils.server.test.ts b/apps/sim/tools/agiloft/utils.server.test.ts index 9d207b06276..d47cb256828 100644 --- a/apps/sim/tools/agiloft/utils.server.test.ts +++ b/apps/sim/tools/agiloft/utils.server.test.ts @@ -3,6 +3,9 @@ */ import { beforeEach, describe, expect, it, vi } from 'vitest' +/** Obvious non-secret so credential scanners do not flag these fixtures. */ +const PLACEHOLDER_PASSWORD = 'not-a-real-password' + const { mockValidateUrlWithDNS, mockSecureFetch } = vi.hoisted(() => ({ mockValidateUrlWithDNS: vi.fn(), mockSecureFetch: vi.fn(), @@ -19,7 +22,7 @@ const baseParams = { instanceUrl: 'https://example.agiloft.com', knowledgeBase: 'demo', login: 'admin', - password: 'secret', + password: PLACEHOLDER_PASSWORD, table: 'contracts', } @@ -30,7 +33,7 @@ function mockResponse(body: { ok?: boolean; status?: number; json?: unknown; tex statusText: '', headers: { get: () => null, getSetCookie: () => [], toRecord: () => ({}) }, body: null, - text: async () => body.text ?? '', + text: async () => body.text ?? JSON.stringify(body.json ?? {}), json: async () => body.json ?? {}, arrayBuffer: async () => new ArrayBuffer(0), } @@ -74,11 +77,18 @@ describe('executeAgiloftRequest', () => { const calls = mockSecureFetch.mock.calls expect(calls).toHaveLength(3) - expect(calls[0][0]).toBe( - 'https://example.agiloft.com/ewws/EWLogin?$KB=demo&$login=admin&$password=secret' - ) + // Credentials go in a form body, never the URL. + expect(calls[0][0]).toBe('https://example.agiloft.com/ewws/EWLogin') + expect(calls[0][2]).toMatchObject({ + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + }) + const sent = new URLSearchParams(calls[0][2].body as string) + expect(sent.get('$KB')).toBe('demo') + expect(sent.get('$table')).toBe('contracts') + expect(sent.get('$lang')).toBe('en') expect(calls[1][0]).toBe('https://example.agiloft.com/ewws/REST/demo/contracts/42') - expect(calls[2][0]).toBe('https://example.agiloft.com/ewws/EWLogout?$KB=demo') + expect(calls[2][0]).toBe('https://example.agiloft.com/ewws/EWLogout?$KB=demo&$lang=en') for (const call of calls) { expect(call[1]).toBe('203.0.113.10') diff --git a/apps/sim/tools/agiloft/utils.server.ts b/apps/sim/tools/agiloft/utils.server.ts index 4e48db85254..119a8f9e567 100644 --- a/apps/sim/tools/agiloft/utils.server.ts +++ b/apps/sim/tools/agiloft/utils.server.ts @@ -1,15 +1,18 @@ import { createLogger } from '@sim/logger' +import { filterUndefined } from '@sim/utils/object' +import { truncate } from '@sim/utils/string' import { type SecureFetchResponse, secureFetchWithPinnedIP, validateUrlWithDNS, } from '@/lib/core/security/input-validation.server' -import type { AgiloftBaseParams } from '@/tools/agiloft/types' +import type { AgiloftBaseParams, AgiloftCredentials } from '@/tools/agiloft/types' +import { AGILOFT_LANG, agiloftAlrestBase, describeAgiloftError } from '@/tools/agiloft/utils' import type { HttpMethod, ToolResponse } from '@/tools/types' const logger = createLogger('AgiloftAuthServer') -interface AgiloftRequestConfig { +export interface AgiloftRequestConfig { url: string method: HttpMethod headers?: Record @@ -31,35 +34,79 @@ export async function resolveAgiloftInstance(instanceUrl: string): Promise): string { + return Object.entries(fields) + .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) + .join('&') +} + +export interface AgiloftSession { + /** Ready-to-send Authorization header value, e.g. `Bearer eyJ...`. */ + authorization: string + token: string +} + +/** + * DNS-pinned login. Requires a pre-resolved IP so the connection cannot be + * steered to a different host between validation and the actual TCP connect. + * + * `$table` and `$lang` are as mandatory as `$KB` here even though only `$KB`, + * `$login`, `$password`, and `$lang` are documented — a live instance rejects + * the call with: + * + * EWWrongDataException ... One has to specify $table, $KB, $lang parameters + * + * The scheme is read back from `authentication_scheme` rather than hardcoded, + * because Agiloft returns it with a trailing space ("Bearer ") and naively + * concatenating it yields a malformed `Bearer ` header. */ export async function agiloftLoginPinned( - params: AgiloftBaseParams, + params: AgiloftCredentials, resolvedIP: string -): Promise { +): Promise { const base = params.instanceUrl.replace(/\/$/, '') - const kb = encodeURIComponent(params.knowledgeBase) - const login = encodeURIComponent(params.login) - const password = encodeURIComponent(params.password) - const url = `${base}/ewws/EWLogin?$KB=${kb}&$login=${login}&$password=${password}` - const response = await secureFetchWithPinnedIP(url, resolvedIP, { method: 'POST' }) + const response = await secureFetchWithPinnedIP(`${base}/ewws/EWLogin`, resolvedIP, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: formEncode( + filterUndefined({ + $KB: params.knowledgeBase, + $login: params.login, + $password: params.password, + // Undocumented on EWLogin, but a live instance rejects the call without + // it. Omitted for KB-scoped operations that have no table. + $table: params.table || undefined, + $lang: AGILOFT_LANG, + }) + ), + }) + + const text = await response.text() if (!response.ok) { - const errorText = await response.text() - throw new Error(`Agiloft login failed: ${response.status} - ${errorText}`) + throw new Error(`Agiloft login failed (${response.status}): ${describeAgiloftError(text)}`) } - const data = (await response.json()) as { access_token?: string } - const token = data.access_token + let data: { access_token?: string; authentication_scheme?: string } + try { + data = JSON.parse(text) + } catch { + throw new Error(`Agiloft login returned a non-JSON response: ${truncate(text, 200)}`) + } - if (!token) { - throw new Error('Agiloft login did not return an access token') + if (!data.access_token) { + throw new Error(`Agiloft login did not return an access token: ${truncate(text, 200)}`) } - return token + const scheme = (data.authentication_scheme || 'Bearer').trim() || 'Bearer' + return { authorization: `${scheme} ${data.access_token}`, token: data.access_token } } /** @@ -69,16 +116,20 @@ export async function agiloftLoginPinned( export async function agiloftLogoutPinned( instanceUrl: string, knowledgeBase: string, - token: string, + authorization: string, resolvedIP: string ): Promise { try { const base = instanceUrl.replace(/\/$/, '') const kb = encodeURIComponent(knowledgeBase) - await secureFetchWithPinnedIP(`${base}/ewws/EWLogout?$KB=${kb}`, resolvedIP, { - method: 'POST', - headers: { Authorization: `Bearer ${token}` }, - }) + await secureFetchWithPinnedIP( + `${base}/ewws/EWLogout?$KB=${kb}&$lang=${AGILOFT_LANG}`, + resolvedIP, + { + method: 'POST', + headers: { Authorization: authorization }, + } + ) } catch (error) { logger.warn('Agiloft logout failed (best-effort)', { error }) } @@ -105,12 +156,12 @@ export async function agiloftLogoutPinned( * Server-only — uses node:dns/promises and node:http(s) via the pinned fetch. */ export async function executeAgiloftRequest( - params: AgiloftBaseParams, + params: AgiloftCredentials, buildRequest: (base: string) => AgiloftRequestConfig, transformResponse: (response: SecureFetchResponse) => Promise ): Promise { const resolvedIP = await resolveAgiloftInstance(params.instanceUrl) - const token = await agiloftLoginPinned(params, resolvedIP) + const session = await agiloftLoginPinned(params, resolvedIP) const base = params.instanceUrl.replace(/\/$/, '') try { @@ -119,14 +170,127 @@ export async function executeAgiloftRequest( method: req.method, headers: { ...req.headers, - Authorization: `Bearer ${token}`, + Authorization: session.authorization, }, body: req.body, }) return await transformResponse(response) } finally { - await agiloftLogoutPinned(params.instanceUrl, params.knowledgeBase, token, resolvedIP) + await agiloftLogoutPinned( + params.instanceUrl, + params.knowledgeBase, + session.authorization, + resolvedIP + ) } } export type { SecureFetchResponse } + +/** + * Shape every `/ewws/alrest` endpoint answers with. + * + * The surface reports failures as HTTP 200 with `success: false`, so checking + * `response.ok` alone silently turns an upstream refusal into a successful + * empty result. `readAlrestJson` is the only sanctioned way to read one. + */ +export interface AlrestEnvelope { + success?: boolean + message?: string + errors?: Array<{ message?: string }> + result?: T +} + +export class AgiloftAlrestError extends Error {} + +/** + * True for an upstream refusal Agiloft already decided on — a validation error, + * a permission denial, a conflicting match. + * + * These must not surface as HTTP 500: the tool runner treats 500 as retryable, + * and retrying a refused create can duplicate a record rather than converge. + */ +export function isAgiloftRefusal(error: unknown): error is AgiloftAlrestError { + return error instanceof AgiloftAlrestError +} + +/** + * Parses an alrest envelope, throwing `AgiloftAlrestError` when the call failed + * — whether it failed by status code, by `success: false`, or by returning + * something that is not JSON at all. + */ +export async function readAlrestJson(response: SecureFetchResponse): Promise { + const text = await response.text() + + let envelope: AlrestEnvelope + try { + envelope = JSON.parse(text) + } catch { + throw new AgiloftAlrestError( + `Agiloft returned a non-JSON response (${response.status}): ${truncate(text, 300)}` + ) + } + + if (!response.ok || envelope.success === false) { + const detail = + envelope.errors + ?.map((entry) => entry?.message) + .filter(Boolean) + .join('; ') || + envelope.message || + describeAgiloftError(truncate(text, 300)) + throw new AgiloftAlrestError(`Agiloft error: ${detail}`) + } + + return envelope.result +} + +/** + * Runs a single authenticated `/ewws/alrest/{KB}` call. `buildRequest` receives + * the KB-scoped base URL, so callers compose `${base}/{table}/...` paths. + */ +export async function executeAlrestRequest( + params: AgiloftBaseParams, + buildRequest: (base: string) => AgiloftRequestConfig, + transformResponse: (response: SecureFetchResponse) => Promise +): Promise { + const resolvedIP = await resolveAgiloftInstance(params.instanceUrl) + const session = await agiloftLoginPinned(params, resolvedIP) + + try { + const req = buildRequest(agiloftAlrestBase(params.instanceUrl, params.knowledgeBase)) + const response = await secureFetchWithPinnedIP(req.url, resolvedIP, { + method: req.method, + headers: { ...req.headers, Authorization: session.authorization }, + body: req.body, + }) + return await transformResponse(response) + } finally { + await agiloftLogoutPinned( + params.instanceUrl, + params.knowledgeBase, + session.authorization, + resolvedIP + ) + } +} + +/** + * Runs a single `/ewws/EW*` call. No login round-trip: that surface rejects the + * bearer token and authenticates from the inline `$login`/`$password` already + * present in the URL built by the caller. + */ +export async function executeEwRequest( + params: AgiloftCredentials, + buildRequest: (base: string) => AgiloftRequestConfig, + transformResponse: (response: SecureFetchResponse) => Promise +): Promise { + const resolvedIP = await resolveAgiloftInstance(params.instanceUrl) + const req = buildRequest(params.instanceUrl.replace(/\/$/, '')) + const response = await secureFetchWithPinnedIP(req.url, resolvedIP, { + method: req.method, + headers: req.headers, + body: req.body, + }) + return await transformResponse(response) +} diff --git a/apps/sim/tools/agiloft/utils.test.ts b/apps/sim/tools/agiloft/utils.test.ts index bfffee607da..fe4cfef2302 100644 --- a/apps/sim/tools/agiloft/utils.test.ts +++ b/apps/sim/tools/agiloft/utils.test.ts @@ -3,174 +3,207 @@ */ import { describe, expect, it } from 'vitest' import { - buildCreateRecordUrl, - buildDeleteRecordUrl, + agiloftAlrestBase, + alrestDeleteRecordUrl, + alrestRecordCollectionUrl, + alrestRecordUrl, + alrestSearchUrl, + buildAttachFileUrl, buildLockRecordUrl, - buildReadRecordUrl, - buildRunActionButtonUrl, - buildSearchRecordsUrl, - buildUpdateRecordUrl, - recordUrlLengthError, + buildRetrieveAttachmentUrl, + buildSavedSearchUrl, + buildSelectRecordsUrl, + describeAgiloftError, + ewCredentialBody, + parseFieldList, } from '@/tools/agiloft/utils' -const BASE = 'https://example.agiloft.com' +/** Obvious non-secret so credential scanners do not flag these fixtures. */ +const PLACEHOLDER_PASSWORD = 'not-a-real-password' + +const INSTANCE = 'https://example.agiloft.com' const baseParams = { - instanceUrl: BASE, - knowledgeBase: 'Demo', - login: 'admin', - password: 'secret', - table: 'helpdesk_case', + instanceUrl: INSTANCE, + knowledgeBase: 'Russell Investments', + login: 'svc.user', + password: PLACEHOLDER_PASSWORD, + table: 'contract', } -describe('CRUD endpoints', () => { - it('targets the documented EW* operations rather than the /ewws/REST path', () => { - expect(buildCreateRecordUrl(BASE, baseParams, {})).toContain('/ewws/EWCreate?') - expect(buildReadRecordUrl(BASE, { ...baseParams, recordId: '358' })).toContain('/ewws/EWRead?') - expect(buildUpdateRecordUrl(BASE, { ...baseParams, recordId: '358' }, {})).toContain( - '/ewws/EWUpdate?' - ) - expect(buildDeleteRecordUrl(BASE, { ...baseParams, recordId: '358' })).toContain( - '/ewws/EWDelete?' - ) - }) +const BASE = agiloftAlrestBase(INSTANCE, baseParams.knowledgeBase) - it('passes record data as the &field=value pairs EWCreate reads off the query string', () => { - const url = buildCreateRecordUrl(BASE, baseParams, { - first_name: 'John', - last_name: 'Doe', - }) - - expect(url).toContain('&first_name=John') - expect(url).toContain('&last_name=Doe') +describe('agiloftAlrestBase', () => { + it('targets the alrest surface that accepts the EWLogin token', () => { + expect(BASE).toBe('https://example.agiloft.com/ewws/alrest/Russell%20Investments') }) - it('percent-encodes field names and values so separators cannot be injected', () => { - const url = buildCreateRecordUrl(BASE, baseParams, { - summary: "Acme & Co: 100% 'done'", - }) + it('encodes the KB name and tolerates a trailing slash on the instance URL', () => { + expect(agiloftAlrestBase('https://example.agiloft.com/', 'A B&C')).toBe( + 'https://example.agiloft.com/ewws/alrest/A%20B%26C' + ) + }) +}) - expect(url).toContain("&summary=Acme%20%26%20Co%3A%20100%25%20'done'") - expect(url).not.toContain('Acme & Co') +describe('alrest record routes', () => { + it('builds collection, item, and search paths under the KB base', () => { + expect(alrestRecordCollectionUrl(BASE, 'contract')).toBe(`${BASE}/contract?lang=en`) + expect(alrestRecordUrl(BASE, 'contract', ' 6342 ')).toBe(`${BASE}/contract/6342?lang=en`) + expect(alrestSearchUrl(BASE, 'contract')).toBe(`${BASE}/contract/search?lang=en`) }) - it('skips null and undefined field values instead of sending the literal text', () => { - const url = buildCreateRecordUrl(BASE, baseParams, { - keep: 'yes', - skipNull: null, - skipUndefined: undefined, + it('retrieves attachments through the documented EWRetrieve endpoint', () => { + const url = buildRetrieveAttachmentUrl(INSTANCE, { + ...baseParams, + recordId: '1234', + fieldName: 'someField', + position: '1', }) - expect(url).toContain('&keep=yes') - expect(url).not.toContain('skipNull') - expect(url).not.toContain('skipUndefined') + expect(url).toContain('/ewws/EWRetrieve?') + expect(url).toContain('&id=1234') + expect(url).toContain('&field=someField') + // Documented parameter name is filePosition, not position. + expect(url).toContain('&filePosition=1') }) - it('always sends a delete rule, defaulting to the non-cascading one', () => { - expect(buildDeleteRecordUrl(BASE, { ...baseParams, recordId: '358' })).toContain( - '&deleteRule=ERROR_IF_DEPENDANTS' + it('always carries a delete rule so linked-record behavior is explicit', () => { + expect(alrestDeleteRecordUrl(BASE, 'contract', '6342', 'ERROR_IF_DEPENDANTS')).toBe( + `${BASE}/contract/6342?lang=en&deleteRule=ERROR_IF_DEPENDANTS` ) - expect( - buildDeleteRecordUrl(BASE, { - ...baseParams, - recordId: '358', - deleteRule: 'APPLY_UNLINK', - }) - ).toContain('&deleteRule=APPLY_UNLINK') }) +}) - it('does not ask for the .json variant on operations whose JSON shape is undocumented', () => { - expect(buildSearchRecordsUrl(BASE, { ...baseParams, query: "a='b'" })).not.toContain('.json') - expect( - buildLockRecordUrl(BASE, { ...baseParams, recordId: '18', lockAction: 'check' }) - ).not.toContain('.json') +describe('parseFieldList', () => { + it('splits and trims a comma-separated projection', () => { + expect(parseFieldList(' id , contract_title1 ,, ')).toEqual(['id', 'contract_title1']) + }) + + it('returns undefined when nothing usable was given, so no projection is sent', () => { + expect(parseFieldList(undefined)).toBeUndefined() + expect(parseFieldList(' ')).toBeUndefined() + expect(parseFieldList(',,')).toBeUndefined() }) }) -describe('recordUrlLengthError', () => { - it('accepts an ordinary record payload', () => { - const error = recordUrlLengthError(BASE, (base) => - buildCreateRecordUrl(base, baseParams, { summary: 'A normal contract title' }) - ) +describe('legacy EW* endpoints', () => { + it('keeps credentials out of the EWSelect URL, which supports a POST body', () => { + const url = buildSelectRecordsUrl(INSTANCE, { ...baseParams, where: "id='1'" }) - expect(error).toBeNull() + expect(url).toContain('/ewws/EWSelect?') + expect(url).toContain('&$lang=en') + expect(url).not.toContain('$login') + expect(url).not.toContain('$password') }) - it('explains the URL ceiling rather than letting Agiloft answer 414', () => { - const error = recordUrlLengthError(BASE, (base) => - buildCreateRecordUrl(base, baseParams, { description: 'x'.repeat(7000) }) - ) + it('form-encodes credentials for the operations that accept a body', () => { + const body = ewCredentialBody(baseParams) - expect(error).toContain('too large') - expect(error).toContain('carry field values in the URL') + const sent = new URLSearchParams(body) + expect(sent.get('$login')).toBe('svc.user') + expect(sent.get('$password')).toBe(PLACEHOLDER_PASSWORD) }) -}) -describe('buildRunActionButtonUrl', () => { - it('uses the /ewws/async prefix EWActionButton is documented under', () => { - const url = buildRunActionButtonUrl(BASE, { + it('percent-encodes credentials in the URL for operations with no body option', () => { + const url = buildLockRecordUrl(INSTANCE, { ...baseParams, - recordId: '82', - actionButtonField: 'ab_field', + login: 'a&b=c', + password: 'placeholder&pass=word', + recordId: '18', + lockAction: 'check', }) - expect(url).toContain('/ewws/async/EWActionButton?') - expect(url).toContain('&name=ab_field') - expect(url).toContain('&id=82') + expect(url).toContain('&$login=a%26b%3Dc') + expect(url).toContain('&$password=placeholder%26pass%3Dword') }) -}) -describe('buildSearchRecordsUrl', () => { - it('sends the saved search label as the documented "search" parameter', () => { - const url = buildSearchRecordsUrl(BASE, { - ...baseParams, - search: 'C: Status is Closed', - }) + it('adds force only when unlocking', () => { + expect( + buildLockRecordUrl(INSTANCE, { + ...baseParams, + recordId: '18', + lockAction: 'unlock', + force: true, + }) + ).toContain('&force=true') - expect(url).toContain('&search=C%3A%20Status%20is%20Closed') - expect(url).not.toContain('query=') + expect( + buildLockRecordUrl(INSTANCE, { + ...baseParams, + recordId: '18', + lockAction: 'lock', + force: true, + }) + ).not.toContain('force=true') }) +}) - it('combines a saved search with an ad hoc query, as EWSearch allows', () => { - const url = buildSearchRecordsUrl(BASE, { - ...baseParams, - search: 'C: Status is Closed', - query: "priority='High'", - }) +describe('documented response keys', () => { + it('builds the EWSavedSearch URL with the mandatory .json decorator and no credentials', () => { + const url = buildSavedSearchUrl(INSTANCE, baseParams) - expect(url).toContain('&search=C%3A%20Status%20is%20Closed') - expect(url).toContain("&query=priority%3D'High'") + expect(url).toContain('/ewws/EWSavedSearch/.json?') + expect(url).toContain('$table=contract') + // Must run under EWLogin/OAuth, so inline credentials are not appended. + expect(url).not.toContain('$login') + expect(url).not.toContain('$password') }) +}) - it('omits the query parameter entirely when no query is supplied', () => { - const url = buildSearchRecordsUrl(BASE, { ...baseParams, search: 'All Open' }) +describe('describeAgiloftError', () => { + it('reduces the HTML-wrapped exception to its message', () => { + const body = + 'ErrorEWWrongDataException has occurred: ' + + '[default task-70331][1786479740423] One has to specify $table, $KB, $lang parameters' + + '' + + expect(describeAgiloftError(body)).toBe( + 'EWWrongDataException: One has to specify $table, $KB, $lang parameters' + ) + }) - expect(url).not.toMatch(/[?&]query=/) + it('passes through a body that is not a typed exception', () => { + expect(describeAgiloftError('Error executing query, please consult logs')).toBe( + 'Error executing query, please consult logs' + ) }) }) -describe('buildLockRecordUrl', () => { - it('adds force only on unlock, where EWLock accepts it', () => { - const unlock = buildLockRecordUrl(BASE, { - ...baseParams, - recordId: '18', - lockAction: 'unlock', - force: true, - }) +describe('documented optional parameters', () => { + it('adds subs only for the delete rule that reads it', () => { + const withReplace = alrestDeleteRecordUrl( + BASE, + 'contract', + '6342', + 'REPLACE_WITH_ANOTHER', + '7,8' + ) + expect(withReplace).toContain('&subs=7') + expect(withReplace).toContain('&subs=8') - expect(unlock).toContain('&force=true') + const withoutReplace = alrestDeleteRecordUrl(BASE, 'contract', '6342', 'APPLY_UNLINK', '7,8') + expect(withoutReplace).not.toContain('subs') }) - it('never sends force on lock or status checks', () => { - for (const lockAction of ['lock', 'check'] as const) { - const url = buildLockRecordUrl(BASE, { - ...baseParams, - recordId: '18', - lockAction, - force: true, - }) + it('asks the JSON decorator for real status codes', () => { + expect(buildSavedSearchUrl(INSTANCE, baseParams)).toContain('err_code_resp=1') + }) +}) - expect(url).not.toContain('force') - } +describe('attach overwrite', () => { + it('sends the documented fieldName$overwrite marker only when requested', () => { + const on = buildAttachFileUrl( + INSTANCE, + { ...baseParams, recordId: '1', fieldName: 'docs', overwrite: true }, + 'a.pdf' + ) + expect(on).toContain('docs%24overwrite=true') + + const off = buildAttachFileUrl( + INSTANCE, + { ...baseParams, recordId: '1', fieldName: 'docs' }, + 'a.pdf' + ) + expect(off).not.toContain('overwrite') }) }) diff --git a/apps/sim/tools/agiloft/utils.ts b/apps/sim/tools/agiloft/utils.ts index 8f9d084cde9..dfe236ac23b 100644 --- a/apps/sim/tools/agiloft/utils.ts +++ b/apps/sim/tools/agiloft/utils.ts @@ -1,19 +1,144 @@ import type { + AgiloftAsyncStatusParams, AgiloftAttachmentInfoParams, AgiloftBaseParams, - AgiloftDeleteRecordParams, + AgiloftCredentials, AgiloftGetChoiceLineIdParams, + AgiloftListTablesParams, AgiloftLockRecordParams, - AgiloftReadRecordParams, AgiloftRemoveAttachmentParams, AgiloftRetrieveAttachmentParams, AgiloftRunActionButtonParams, - AgiloftSearchRecordsParams, AgiloftSelectRecordsParams, + AgiloftUpsertRecordParams, } from '@/tools/agiloft/types' import type { HttpMethod } from '@/tools/types' -/** URL builders (credential-free -- auth is via Bearer token header) */ +/** + * Asks the JSON decorator for real HTTP status codes. Without it Agiloft + * answers 200 even on failure, which is what forces callers to infer errors + * from the body shape. + */ +export const AGILOFT_JSON_ERROR_CODES = 'err_code_resp=1' + +/** + * Reduces an Agiloft error body to its message. + * + * Failures arrive as an HTML document wrapping a typed exception and an + * internal task id — `…EWWrongDataException has occurred: + * [default task-70331][1786479740423] One has to specify $table…` — + * none of which helps the person reading the workflow log. + */ +export function describeAgiloftError(body: string): string { + const text = body + .replace(/<[^>]*>/g, ' ') + .replace(/\s+/g, ' ') + .trim() + + const typed = /(EW[A-Za-z]*Exception)\s*(?:has occurred)?\s*:?\s*(.*)/.exec(text) + if (!typed) return text + + const detail = typed[2].replace(/^(\[[^\]]*\]\s*)+/, '').trim() + return detail ? `${typed[1]}: ${detail}` : typed[1] +} + +/** Language sent on every Agiloft call; EWLogin rejects the request without it. */ +export const AGILOFT_LANG = 'en' + +/** + * Base URL for the `/ewws/alrest/{KB}` REST surface, which is the one that + * accepts the token EWLogin issues. The legacy `/ewws/EW*` endpoints expect + * inline `$login`/`$password` credentials instead and reject a bearer token. + */ +export function agiloftAlrestBase(instanceUrl: string, knowledgeBase: string): string { + return `${instanceUrl.replace(/\/$/, '')}/ewws/alrest/${encodeURIComponent(knowledgeBase)}` +} + +/** Table segment of an alrest path. */ +function tableSegment(table: string): string { + return encodeURIComponent(table.trim()) +} + +export function alrestRecordCollectionUrl(base: string, table: string): string { + return `${base}/${tableSegment(table)}?lang=${AGILOFT_LANG}` +} + +export function alrestRecordUrl(base: string, table: string, recordId: string): string { + return `${base}/${tableSegment(table)}/${encodeURIComponent(recordId.trim())}?lang=${AGILOFT_LANG}` +} + +/** + * EWDelete's dependent-record strategy carries over to alrest as a query + * parameter; omitting it leaves the behavior for linked records unspecified. + */ +export function alrestDeleteRecordUrl( + base: string, + table: string, + recordId: string, + deleteRule: string, + substituteIds?: string +): string { + let url = `${alrestRecordUrl(base, table, recordId)}&deleteRule=${encodeURIComponent(deleteRule)}` + + /** + * `subs` is read only under REPLACE_WITH_ANOTHER, and names records from the + * same table that adopt the dependants of the one being deleted. + */ + if (deleteRule === 'REPLACE_WITH_ANOTHER' && substituteIds) { + for (const id of substituteIds + .split(',') + .map((value) => value.trim()) + .filter(Boolean)) { + url += `&subs=${encodeURIComponent(id)}` + } + } + + return url +} + +export function alrestSearchUrl(base: string, table: string): string { + return `${base}/${tableSegment(table)}/search?lang=${AGILOFT_LANG}` +} + +/** + * Hard ceiling on records returned from a search. + * + * Whether alrest honours `page`/`limit` in the request body is unverified — the + * names carry over from the legacy EWSearch query string. If it ignores them a + * broad query returns the whole table, and an unfiltered contract record runs + * to roughly 184 KB, so the result is capped here rather than trusting the + * server to bound it. + */ +export const AGILOFT_MAX_SEARCH_RECORDS = 200 + +/** + * Byte ceiling for a single attachment download. The route base64-encodes the + * body into a JSON response, so peak memory is several times the file size; + * without a cap it inherits the shared 100 MiB default. + */ +export const AGILOFT_MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024 + +/** + * Ceiling on record IDs returned by EWSelect. The operation has no page size + * of its own — the documented way to bound it is a database `limit` inside the + * WHERE clause — so an unqualified clause returns every matching ID. + */ +export const AGILOFT_MAX_SELECT_IDS = 1000 + +/** + * Splits a comma-separated field list into the `field` array alrest search + * accepts. Field selection is the only way to keep a response small — a single + * contract record runs to roughly 184 KB unfiltered. + */ +export function parseFieldList(fields?: string): string[] | undefined { + const list = fields + ?.split(',') + .map((field) => field.trim()) + .filter(Boolean) + return list?.length ? list : undefined +} + +/** URL builders for the legacy `/ewws/EW*` surface (inline-credential auth) */ function encodeTable(params: AgiloftBaseParams) { return { @@ -22,107 +147,123 @@ function encodeTable(params: AgiloftBaseParams) { } } +/** Non-secret query prefix shared by the legacy `/ewws/EW*` endpoints. */ function buildEwBaseQuery(params: AgiloftBaseParams): string { const { kb, table } = encodeTable(params) - return `$KB=${kb}&$table=${table}&$lang=en` + return `$KB=${kb}&$table=${table}&$lang=${AGILOFT_LANG}` } /** - * EWCreate and EWUpdate carry record data in the query string, so an oversized - * payload hits the server's request-line limit rather than a body limit. Tomcat - * allows 8 KB for the whole request line by default; this leaves headroom for - * the method, protocol, and surrounding headers. + * Credentials appended to an EW* URL. + * + * "Every REST call should contain the user's credentials in the form + * login={login}&password={password}" — the legacy surface authenticates this + * way rather than from the bearer token EWLogin issues. Used only for the + * operations that cannot carry them in a body instead; see + * `ewCredentialBody`. */ -export const AGILOFT_MAX_RECORD_URL_LENGTH = 6000 +function ewCredentialQuery(params: AgiloftCredentials): string { + const login = encodeURIComponent(params.login) + const password = encodeURIComponent(params.password) + return `&$login=${login}&$password=${password}` +} /** - * Serializes a record's field values as the `&field=value` pairs EWCreate and - * EWUpdate expect. Agiloft reads record data straight off the query string; - * there is no documented JSON body form of these operations. + * Credentials as a form-encoded POST body, which is how Agiloft recommends + * production systems pass them: "you can avoid passing the login or password + * in REST calls by using POST instead of GET to pass the parameters in the + * request body." + * + * Only EWRead, EWSelect, EWCreate, EWUpdate, and EWDelete accept credentials + * this way. Every other EW* operation has to keep them in the query string. */ -function encodeRecordData(data: Record): string { - let encoded = '' - for (const [field, value] of Object.entries(data)) { - if (value === undefined || value === null) continue - encoded += `&${encodeURIComponent(field)}=${encodeURIComponent(String(value))}` - } - return encoded +export function ewCredentialBody(params: AgiloftCredentials): string { + return `$login=${encodeURIComponent(params.login)}&$password=${encodeURIComponent(params.password)}` } /** - * Returns an explanatory message when a record's field data would push the - * request line past what Agiloft accepts, so the caller reports the real cause - * instead of surfacing an opaque 414 from the server. + * EWSelect is one of the five operations that accept credentials in a POST + * body, so the URL deliberately carries no `$login`/`$password`. */ -export function recordUrlLengthError( - instanceUrl: string, - build: (base: string) => string -): string | null { - const url = build(instanceUrl.replace(/\/$/, '')) - if (url.length <= AGILOFT_MAX_RECORD_URL_LENGTH) return null - return `Record data is too large: Agiloft's create and update operations carry field values in the URL, and this request is ${url.length} characters against a ${AGILOFT_MAX_RECORD_URL_LENGTH} limit. Split it into smaller updates or move long text into an attachment.` -} +/** + * EWSavedSearch answers JSON only, so the `.json` decorator is mandatory. It + * also has to run under EWLogin or OAuth authorization rather than inline + * credentials, so no `$login`/`$password` are appended here. + */ +/** + * EWTable is knowledge-base scoped, so the query carries `$KB` and `$lang` but + * no `$table`. Narrowing to one table uses the plain `table` parameter, and + * JSON is the only supported output, hence the mandatory `.json` decorator. + * Runs under EWLogin or OAuth authorization, so no inline credentials. + */ +export function buildListTablesUrl(base: string, params: AgiloftListTablesParams): string { + const kb = encodeURIComponent(params.knowledgeBase) + let url = `${base}/ewws/EWTable/.json?${AGILOFT_JSON_ERROR_CODES}&$KB=${kb}&$lang=${AGILOFT_LANG}` -export function buildCreateRecordUrl( - base: string, - params: AgiloftBaseParams, - data: Record -): string { - return `${base}/ewws/EWCreate?${buildEwBaseQuery(params)}${encodeRecordData(data)}` + const table = params.table?.trim() + if (table) url += `&table=${encodeURIComponent(table)}` + if (params.includeLinkedInfo) url += '&includelinkedinfo=true' + if (params.skipColumnsInfo) url += '&skipColumnsInfo=true' + + return url } -export function buildReadRecordUrl(base: string, params: AgiloftReadRecordParams): string { - const id = encodeURIComponent(params.recordId.trim()) - return `${base}/ewws/EWRead?${buildEwBaseQuery(params)}&id=${id}` +/** + * EWUpsert takes every parameter — credentials included — in a form-encoded + * body, so nothing sensitive reaches the URL and there is no request-line + * length ceiling on the record data. + */ +export function buildUpsertRecordUrl(base: string): string { + return `${base}/ewws/EWUpsert` } -export function buildUpdateRecordUrl( - base: string, - params: AgiloftBaseParams & { recordId: string }, +export function buildUpsertRecordBody( + params: AgiloftUpsertRecordParams, data: Record ): string { - const id = encodeURIComponent(params.recordId.trim()) - return `${base}/ewws/EWUpdate?${buildEwBaseQuery(params)}&id=${id}${encodeRecordData(data)}` -} + const fields: Array<[string, string]> = [ + ['$KB', params.knowledgeBase], + ['$table', params.table], + ['$login', params.login], + ['$password', params.password], + ['$lang', AGILOFT_LANG], + ['$match', params.match.trim()], + ] -/** - * EWDelete requires a delete rule naming the strategy for dependent records; - * omitting it is a malformed request. - */ -export function buildDeleteRecordUrl(base: string, params: AgiloftDeleteRecordParams): string { - const id = encodeURIComponent(params.recordId.trim()) - const deleteRule = encodeURIComponent(params.deleteRule ?? 'ERROR_IF_DEPENDANTS') - return `${base}/ewws/EWDelete?${buildEwBaseQuery(params)}&id=${id}&deleteRule=${deleteRule}` -} + if (params.async) fields.push(['$async', 'true']) -export function buildSearchRecordsUrl(base: string, params: AgiloftSearchRecordsParams): string { - let url = `${base}/ewws/EWSearch?${buildEwBaseQuery(params)}` + for (const [field, value] of Object.entries(data)) { + if (value === undefined || value === null) continue - if (params.search) { - url += `&search=${encodeURIComponent(params.search.trim())}` - } - if (params.query) { - url += `&query=${encodeURIComponent(params.query)}` - } + /** + * Multi-value fields are encoded as repeated key/value pairs, not as a + * joined string. Objects have no documented encoding at all, and + * String()-ing one silently writes "[object Object]" into the record. + */ + if (Array.isArray(value)) { + for (const entry of value) { + if (entry === undefined || entry === null) continue + fields.push([field, String(entry)]) + } + continue + } - if (params.fields) { - const fieldList = params.fields - .split(',') - .map((f) => f.trim()) - .filter(Boolean) - for (const field of fieldList) { - url += `&field=${encodeURIComponent(field)}` + if (typeof value === 'object') { + throw new TypeError( + `Field "${field}" is an object, which Agiloft has no encoding for. Use a string, a number, or an array of values.` + ) } - } - if (params.page) { - url += `&page=${encodeURIComponent(params.page)}` - } - if (params.limit) { - url += `&limit=${encodeURIComponent(params.limit)}` + fields.push([field, String(value)]) } - return url + return fields + .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) + .join('&') +} + +export function buildSavedSearchUrl(base: string, params: AgiloftBaseParams): string { + return `${base}/ewws/EWSavedSearch/.json?${AGILOFT_JSON_ERROR_CODES}&${buildEwBaseQuery(params)}` } export function buildSelectRecordsUrl(base: string, params: AgiloftSelectRecordsParams): string { @@ -136,8 +277,8 @@ export function buildRetrieveAttachmentUrl( ): string { const id = encodeURIComponent(params.recordId.trim()) const field = encodeURIComponent(params.fieldName.trim()) - const position = encodeURIComponent(params.position) - return `${base}/ewws/EWRetrieve?${buildEwBaseQuery(params)}&id=${id}&field=${field}&filePosition=${position}` + const position = encodeURIComponent(params.position.trim()) + return `${base}/ewws/EWRetrieve?${buildEwBaseQuery(params)}${ewCredentialQuery(params)}&id=${id}&field=${field}&filePosition=${position}` } export function buildRemoveAttachmentUrl( @@ -147,13 +288,13 @@ export function buildRemoveAttachmentUrl( const id = encodeURIComponent(params.recordId.trim()) const field = encodeURIComponent(params.fieldName.trim()) const position = encodeURIComponent(params.position) - return `${base}/ewws/EWRemoveAttachment?${buildEwBaseQuery(params)}&id=${id}&field=${field}&filePosition=${position}` + return `${base}/ewws/EWRemoveAttachment?${buildEwBaseQuery(params)}${ewCredentialQuery(params)}&id=${id}&field=${field}&filePosition=${position}` } export function buildAttachmentInfoUrl(base: string, params: AgiloftAttachmentInfoParams): string { const id = encodeURIComponent(params.recordId.trim()) const fieldName = encodeURIComponent(params.fieldName.trim()) - return `${base}/ewws/EWAttachInfo/.json?${buildEwBaseQuery(params)}&id=${id}&field=${fieldName}` + return `${base}/ewws/EWAttachInfo/.json?${AGILOFT_JSON_ERROR_CODES}&${buildEwBaseQuery(params)}${ewCredentialQuery(params)}&id=${id}&field=${fieldName}` } /** @@ -162,23 +303,34 @@ export function buildAttachmentInfoUrl(base: string, params: AgiloftAttachmentIn */ export function buildLockRecordUrl(base: string, params: AgiloftLockRecordParams): string { const id = encodeURIComponent(params.recordId.trim()) - let url = `${base}/ewws/EWLock?${buildEwBaseQuery(params)}&id=${id}` + let url = `${base}/ewws/EWLock?${buildEwBaseQuery(params)}${ewCredentialQuery(params)}&id=${id}` if (params.lockAction === 'unlock' && params.force) { url += '&force=true' } return url } +/** + * EWAttach carries the file as the raw request body, so its credentials have to + * travel in the query string like the rest of the EW* surface — there is no + * room for a form-encoded credential body here. + */ export function buildAttachFileUrl( base: string, - params: AgiloftBaseParams & { recordId: string; fieldName: string }, + params: AgiloftBaseParams & { recordId: string; fieldName: string; overwrite?: boolean }, fileName: string ): string { - const { kb, table } = encodeTable(params) const recordId = encodeURIComponent(params.recordId.trim()) const fieldName = encodeURIComponent(params.fieldName.trim()) const encodedFileName = encodeURIComponent(fileName) - return `${base}/ewws/EWAttach?$KB=${kb}&$table=${table}&$lang=en&id=${recordId}&field=${fieldName}&fileName=${encodedFileName}` + let url = `${base}/ewws/EWAttach?${buildEwBaseQuery(params)}${ewCredentialQuery(params)}&id=${recordId}&field=${fieldName}&fileName=${encodedFileName}` + + /** `$overwrite` replaces the field's contents instead of appending. */ + if (params.overwrite) { + url += `&${fieldName}%24overwrite=true` + } + + return url } export function buildGetChoiceLineIdUrl( @@ -187,7 +339,34 @@ export function buildGetChoiceLineIdUrl( ): string { const field = encodeURIComponent(params.fieldName.trim()) const value = encodeURIComponent(params.value.trim()) - return `${base}/ewws/EWGetChoiceLineId?${buildEwBaseQuery(params)}&field=${field}&value=${value}` + return `${base}/ewws/EWGetChoiceLineId?${buildEwBaseQuery(params)}${ewCredentialQuery(params)}&field=${field}&value=${value}` +} + +/** + * EWAsyncStatus reports the outcome of a call made through the `/ewws/async` + * prefix — including EWActionButton, whose callback ID is otherwise unusable. + */ +export function buildAsyncStatusUrl(base: string, params: AgiloftAsyncStatusParams): string { + const callbackId = encodeURIComponent(params.callbackId.trim()) + return `${base}/ewws/EWAsyncStatus?${buildEwBaseQuery(params)}${ewCredentialQuery(params)}&callback_id=${callbackId}` +} + +/** Documented EWAsyncStatus response codes. */ +export const AGILOFT_ASYNC_STATUS: Record = { + 200: { status: 'completed', complete: true }, + 201: { status: 'queued', complete: false }, + 202: { status: 'in_progress', complete: false }, + 501: { status: 'failed', complete: true }, + 523: { status: 'unknown_callback', complete: true }, +} + +/** + * EWNLPSearch answers semantic queries and returns records in the same shape as + * search. It is KB-scoped — the table is chosen by the KB's chat-search + * configuration rather than by the caller. + */ +export function buildNlpSearchUrl(base: string): string { + return `${base}/ewws/EWNLPSearch` } export function getLockHttpMethod(lockAction: string): HttpMethod { @@ -211,5 +390,5 @@ export function buildRunActionButtonUrl( ): string { const id = encodeURIComponent(params.recordId.trim()) const name = encodeURIComponent(params.actionButtonField.trim()) - return `${base}/ewws/async/EWActionButton?${buildEwBaseQuery(params)}&name=${name}&id=${id}` + return `${base}/ewws/async/EWActionButton?${buildEwBaseQuery(params)}${ewCredentialQuery(params)}&name=${name}&id=${id}` } diff --git a/apps/sim/tools/generated/tool-ids.ts b/apps/sim/tools/generated/tool-ids.ts index 96873aa405c..a665246d33f 100644 --- a/apps/sim/tools/generated/tool-ids.ts +++ b/apps/sim/tools/generated/tool-ids.ts @@ -3,7 +3,7 @@ /** Every registered tool id, including versioned variants. */ const toolIds: string[] = JSON.parse( - '["a2a_cancel_task","a2a_get_agent_card","a2a_get_task","a2a_send_message","agentmail_create_draft","agentmail_create_inbox","agentmail_delete_draft","agentmail_delete_inbox","agentmail_delete_thread","agentmail_forward_message","agentmail_get_draft","agentmail_get_inbox","agentmail_get_message","agentmail_get_thread","agentmail_list_drafts","agentmail_list_inboxes","agentmail_list_messages","agentmail_list_threads","agentmail_reply_message","agentmail_send_draft","agentmail_send_message","agentmail_update_draft","agentmail_update_inbox","agentmail_update_message","agentmail_update_thread","agentphone_create_call","agentphone_create_contact","agentphone_create_number","agentphone_delete_contact","agentphone_get_call","agentphone_get_call_transcript","agentphone_get_contact","agentphone_get_conversation","agentphone_get_conversation_messages","agentphone_get_number_messages","agentphone_get_usage","agentphone_get_usage_daily","agentphone_get_usage_monthly","agentphone_list_calls","agentphone_list_contacts","agentphone_list_conversations","agentphone_list_numbers","agentphone_react_to_message","agentphone_release_number","agentphone_send_message","agentphone_update_contact","agentphone_update_conversation","agiloft_attach_file","agiloft_attachment_info","agiloft_create_record","agiloft_delete_record","agiloft_get_choice_line_id","agiloft_lock_record","agiloft_read_record","agiloft_remove_attachment","agiloft_retrieve_attachment","agiloft_run_action_button","agiloft_saved_search","agiloft_search_records","agiloft_select_records","agiloft_update_record","ahrefs_anchors","ahrefs_backlinks","ahrefs_backlinks_stats","ahrefs_batch_analysis","ahrefs_broken_backlinks","ahrefs_domain_rating","ahrefs_domain_rating_history","ahrefs_keyword_overview","ahrefs_keywords_history","ahrefs_metrics","ahrefs_metrics_history","ahrefs_organic_competitors","ahrefs_organic_keywords","ahrefs_paid_pages","ahrefs_rank_tracker_competitors_overview","ahrefs_rank_tracker_competitors_stats","ahrefs_rank_tracker_overview","ahrefs_rank_tracker_serp_overview","ahrefs_refdomains_history","ahrefs_referring_domains","ahrefs_related_terms","ahrefs_site_audit_page_explorer","ahrefs_top_pages","airtable_create_records","airtable_delete_records","airtable_get_base_schema","airtable_get_record","airtable_list_bases","airtable_list_records","airtable_list_tables","airtable_update_multiple_records","airtable_update_record","airtable_upsert_records","airweave_search","algolia_add_record","algolia_batch_operations","algolia_browse_records","algolia_clear_records","algolia_copy_move_index","algolia_delete_by_filter","algolia_delete_index","algolia_delete_record","algolia_get_record","algolia_get_records","algolia_get_settings","algolia_get_task_status","algolia_list_indices","algolia_partial_update_record","algolia_search","algolia_update_settings","amplitude_event_segmentation","amplitude_funnels","amplitude_get_active_users","amplitude_get_revenue","amplitude_group_identify","amplitude_identify_user","amplitude_list_events","amplitude_realtime_active_users","amplitude_retention","amplitude_send_event","amplitude_user_activity","amplitude_user_profile","amplitude_user_search","apify_get_dataset_items","apify_get_run","apify_run_actor_async","apify_run_actor_sync","apify_run_task","apollo_account_bulk_create","apollo_account_bulk_update","apollo_account_create","apollo_account_search","apollo_account_update","apollo_contact_bulk_create","apollo_contact_bulk_update","apollo_contact_create","apollo_contact_search","apollo_contact_update","apollo_email_accounts","apollo_opportunity_create","apollo_opportunity_get","apollo_opportunity_search","apollo_opportunity_update","apollo_organization_bulk_enrich","apollo_organization_enrich","apollo_organization_search","apollo_people_bulk_enrich","apollo_people_enrich","apollo_people_search","apollo_sequence_add_contacts","apollo_sequence_search","apollo_task_create","apollo_task_search","appconfig_create_application","appconfig_create_configuration_profile","appconfig_create_environment","appconfig_create_hosted_configuration_version","appconfig_delete_application","appconfig_delete_configuration_profile","appconfig_delete_environment","appconfig_delete_hosted_configuration_version","appconfig_get_application","appconfig_get_configuration","appconfig_get_configuration_profile","appconfig_get_deployment","appconfig_get_environment","appconfig_get_hosted_configuration_version","appconfig_list_applications","appconfig_list_configuration_profiles","appconfig_list_deployment_strategies","appconfig_list_deployments","appconfig_list_environments","appconfig_list_hosted_configuration_versions","appconfig_start_deployment","appconfig_stop_deployment","appconfig_update_application","appconfig_update_configuration_profile","appconfig_update_environment","arxiv_get_author_papers","arxiv_get_paper","arxiv_search","asana_add_comment","asana_add_followers","asana_create_project","asana_create_section","asana_create_subtask","asana_create_task","asana_delete_task","asana_get_project","asana_get_projects","asana_get_task","asana_list_sections","asana_list_workspaces","asana_search_tasks","asana_update_task","ashby_add_candidate_tag","ashby_change_application_stage","ashby_create_application","ashby_create_candidate","ashby_create_note","ashby_get_application","ashby_get_candidate","ashby_get_job","ashby_get_job_posting","ashby_get_offer","ashby_list_applications","ashby_list_archive_reasons","ashby_list_candidate_tags","ashby_list_candidates","ashby_list_custom_fields","ashby_list_departments","ashby_list_interviews","ashby_list_job_postings","ashby_list_jobs","ashby_list_locations","ashby_list_notes","ashby_list_offers","ashby_list_openings","ashby_list_sources","ashby_list_users","ashby_remove_candidate_tag","ashby_search_candidates","ashby_update_candidate","athena_batch_get_query_execution","athena_create_named_query","athena_delete_named_query","athena_get_named_query","athena_get_query_execution","athena_get_query_results","athena_list_databases","athena_list_named_queries","athena_list_query_executions","athena_list_table_metadata","athena_start_query","athena_stop_query","attio_assert_record","attio_create_attribute","attio_create_comment","attio_create_list","attio_create_list_entry","attio_create_note","attio_create_object","attio_create_record","attio_create_task","attio_create_webhook","attio_delete_comment","attio_delete_list_entry","attio_delete_note","attio_delete_record","attio_delete_task","attio_delete_webhook","attio_get_attribute","attio_get_comment","attio_get_list","attio_get_list_entry","attio_get_member","attio_get_note","attio_get_object","attio_get_record","attio_get_task","attio_get_thread","attio_get_webhook","attio_list_attributes","attio_list_lists","attio_list_members","attio_list_notes","attio_list_objects","attio_list_records","attio_list_tasks","attio_list_threads","attio_list_webhooks","attio_query_list_entries","attio_search_records","attio_update_attribute","attio_update_list","attio_update_list_entry","attio_update_object","attio_update_record","attio_update_task","attio_update_webhook","azure_devops_add_comment","azure_devops_create_work_item","azure_devops_get_build_log","azure_devops_get_build_timeline","azure_devops_get_comments","azure_devops_get_pipeline","azure_devops_get_pipeline_run","azure_devops_get_work_item","azure_devops_get_work_items_batch","azure_devops_get_work_items_between_builds","azure_devops_list_build_logs","azure_devops_list_builds","azure_devops_list_pipeline_runs","azure_devops_list_pipelines","azure_devops_query_work_items","azure_devops_update_work_item","box_copy_file","box_create_folder","box_delete_file","box_delete_folder","box_download_file","box_get_file_info","box_list_folder_items","box_search","box_sign_cancel_request","box_sign_create_request","box_sign_get_request","box_sign_list_requests","box_sign_resend_request","box_update_file","box_upload_file","brandfetch_get_brand","brandfetch_search","brex_archive_budget","brex_create_budget","brex_create_spend_limit","brex_create_transfer","brex_create_vendor","brex_get_budget","brex_get_cash_account","brex_get_company","brex_get_current_user","brex_get_expense","brex_get_spend_limit","brex_get_transfer","brex_get_user","brex_get_vendor","brex_list_budgets","brex_list_card_accounts","brex_list_card_statements","brex_list_card_transactions","brex_list_cards","brex_list_cash_accounts","brex_list_cash_statements","brex_list_cash_transactions","brex_list_departments","brex_list_expenses","brex_list_locations","brex_list_spend_limits","brex_list_titles","brex_list_transfers","brex_list_users","brex_list_vendors","brex_match_receipt","brex_update_expense","brex_update_vendor","brex_upload_receipt","brightdata_cancel_snapshot","brightdata_discover","brightdata_download_snapshot","brightdata_scrape_dataset","brightdata_scrape_url","brightdata_serp_search","brightdata_snapshot_status","brightdata_sync_scrape","browser_use_run_task","buffer_create_idea","buffer_create_post","buffer_delete_post","buffer_edit_post","buffer_get_account","buffer_get_channels","buffer_get_idea_groups","buffer_get_ideas","buffer_get_post","buffer_get_posts","calcom_cancel_booking","calcom_confirm_booking","calcom_create_booking","calcom_create_event_type","calcom_create_schedule","calcom_decline_booking","calcom_delete_event_type","calcom_delete_schedule","calcom_get_booking","calcom_get_default_schedule","calcom_get_event_type","calcom_get_schedule","calcom_get_slots","calcom_list_bookings","calcom_list_event_types","calcom_list_schedules","calcom_reschedule_booking","calcom_update_event_type","calcom_update_schedule","calendly_cancel_event","calendly_create_event_invitee","calendly_create_invitee_no_show","calendly_create_scheduling_link","calendly_create_webhook","calendly_delete_invitee_no_show","calendly_delete_webhook","calendly_get_current_user","calendly_get_event_invitee","calendly_get_event_type","calendly_get_scheduled_event","calendly_get_user","calendly_list_event_invitees","calendly_list_event_type_available_times","calendly_list_event_types","calendly_list_organization_memberships","calendly_list_routing_form_submissions","calendly_list_routing_forms","calendly_list_scheduled_events","calendly_list_user_availability_schedules","calendly_list_user_busy_times","calendly_list_webhooks","clay_populate","clerk_add_organization_member","clerk_ban_user","clerk_create_actor_token","clerk_create_allowlist_identifier","clerk_create_blocklist_identifier","clerk_create_organization","clerk_create_organization_invitation","clerk_create_user","clerk_delete_allowlist_identifier","clerk_delete_blocklist_identifier","clerk_delete_organization","clerk_delete_user","clerk_get_jwt_template","clerk_get_organization","clerk_get_session","clerk_get_user","clerk_get_user_oauth_token","clerk_list_allowlist_identifiers","clerk_list_blocklist_identifiers","clerk_list_jwt_templates","clerk_list_organization_invitations","clerk_list_organization_memberships","clerk_list_organizations","clerk_list_sessions","clerk_list_users","clerk_lock_user","clerk_remove_organization_member","clerk_revoke_actor_token","clerk_revoke_session","clerk_unban_user","clerk_unlock_user","clerk_update_organization","clerk_update_organization_membership","clerk_update_user","clickhouse_count_rows","clickhouse_create_database","clickhouse_create_table","clickhouse_delete","clickhouse_describe_table","clickhouse_drop_database","clickhouse_drop_partition","clickhouse_drop_table","clickhouse_execute","clickhouse_insert","clickhouse_insert_rows","clickhouse_introspect","clickhouse_kill_query","clickhouse_list_clusters","clickhouse_list_databases","clickhouse_list_mutations","clickhouse_list_partitions","clickhouse_list_running_queries","clickhouse_list_tables","clickhouse_optimize_table","clickhouse_query","clickhouse_rename_table","clickhouse_show_create_table","clickhouse_table_stats","clickhouse_truncate_table","clickhouse_update","clickup_add_tag_to_task","clickup_create_checklist","clickup_create_checklist_item","clickup_create_comment","clickup_create_folder","clickup_create_list","clickup_create_task","clickup_create_time_entry","clickup_delete_checklist","clickup_delete_checklist_item","clickup_delete_comment","clickup_delete_task","clickup_delete_time_entry","clickup_get_comments","clickup_get_custom_fields","clickup_get_folders","clickup_get_list_members","clickup_get_lists","clickup_get_running_timer","clickup_get_space_tags","clickup_get_spaces","clickup_get_task","clickup_get_task_members","clickup_get_tasks","clickup_get_time_entries","clickup_get_workspaces","clickup_remove_custom_field_value","clickup_remove_tag_from_task","clickup_search_tasks","clickup_set_custom_field_value","clickup_start_timer","clickup_stop_timer","clickup_update_checklist","clickup_update_checklist_item","clickup_update_comment","clickup_update_task","clickup_update_time_entry","clickup_upload_attachment","cloudflare_create_dns_record","cloudflare_create_zone","cloudflare_delete_dns_record","cloudflare_delete_zone","cloudflare_dns_analytics","cloudflare_get_zone","cloudflare_get_zone_settings","cloudflare_list_certificates","cloudflare_list_dns_records","cloudflare_list_zones","cloudflare_purge_cache","cloudflare_update_dns_record","cloudflare_update_zone_setting","cloudformation_cancel_update_stack","cloudformation_create_change_set","cloudformation_create_stack","cloudformation_delete_stack","cloudformation_describe_change_set","cloudformation_describe_stack_drift_detection_status","cloudformation_describe_stack_events","cloudformation_describe_stacks","cloudformation_detect_stack_drift","cloudformation_execute_change_set","cloudformation_get_template","cloudformation_get_template_summary","cloudformation_list_stack_resources","cloudformation_update_stack","cloudformation_validate_template","cloudwatch_describe_alarm_history","cloudwatch_describe_alarms","cloudwatch_describe_log_groups","cloudwatch_describe_log_streams","cloudwatch_filter_log_events","cloudwatch_get_log_events","cloudwatch_get_metric_statistics","cloudwatch_list_metrics","cloudwatch_mute_alarm","cloudwatch_put_log_group_retention","cloudwatch_put_metric_data","cloudwatch_query_logs","cloudwatch_unmute_alarm","codepipeline_disable_stage_transition","codepipeline_enable_stage_transition","codepipeline_get_pipeline","codepipeline_get_pipeline_execution","codepipeline_get_pipeline_state","codepipeline_list_action_executions","codepipeline_list_pipeline_executions","codepipeline_list_pipelines","codepipeline_put_approval_result","codepipeline_retry_stage_execution","codepipeline_start_execution","codepipeline_stop_execution","confluence_add_label","confluence_create_blogpost","confluence_create_comment","confluence_create_page","confluence_create_page_property","confluence_create_space","confluence_create_space_property","confluence_delete_attachment","confluence_delete_blogpost","confluence_delete_comment","confluence_delete_label","confluence_delete_page","confluence_delete_page_property","confluence_delete_space","confluence_delete_space_property","confluence_get_blogpost","confluence_get_page_ancestors","confluence_get_page_children","confluence_get_page_descendants","confluence_get_page_version","confluence_get_pages_by_label","confluence_get_space","confluence_get_task","confluence_get_user","confluence_list_attachments","confluence_list_blogposts","confluence_list_blogposts_in_space","confluence_list_comments","confluence_list_labels","confluence_list_page_properties","confluence_list_page_versions","confluence_list_pages_in_space","confluence_list_space_labels","confluence_list_space_permissions","confluence_list_space_properties","confluence_list_spaces","confluence_list_tasks","confluence_retrieve","confluence_search","confluence_search_in_space","confluence_update","confluence_update_blogpost","confluence_update_comment","confluence_update_space","confluence_update_task","confluence_upload_attachment","context_dev_classify_naics","context_dev_classify_sic","context_dev_crawl","context_dev_extract","context_dev_extract_product","context_dev_extract_products","context_dev_get_brand","context_dev_get_brand_by_email","context_dev_get_brand_by_name","context_dev_get_brand_by_ticker","context_dev_identify_transaction","context_dev_map","context_dev_scrape_fonts","context_dev_scrape_html","context_dev_scrape_images","context_dev_scrape_markdown","context_dev_scrape_styleguide","context_dev_screenshot","context_dev_search","convex_action","convex_document_deltas","convex_list_documents","convex_list_tables","convex_mutation","convex_query","convex_run_function","crowdstrike_get_sensor_aggregates","crowdstrike_get_sensor_details","crowdstrike_query_sensors","cursor_add_followup","cursor_add_followup_v2","cursor_delete_agent","cursor_delete_agent_v2","cursor_download_artifact","cursor_download_artifact_v2","cursor_get_agent","cursor_get_agent_v2","cursor_get_api_key_info","cursor_get_api_key_info_v2","cursor_get_conversation","cursor_get_conversation_v2","cursor_launch_agent","cursor_launch_agent_v2","cursor_list_agents","cursor_list_agents_v2","cursor_list_artifacts","cursor_list_artifacts_v2","cursor_list_models","cursor_list_models_v2","cursor_list_repositories","cursor_list_repositories_v2","cursor_stop_agent","cursor_stop_agent_v2","dagster_delete_run","dagster_get_asset","dagster_get_run","dagster_get_run_logs","dagster_launch_run","dagster_list_assets","dagster_list_jobs","dagster_list_runs","dagster_list_schedules","dagster_list_sensors","dagster_materialize_assets","dagster_reexecute_run","dagster_report_asset_materialization","dagster_start_schedule","dagster_start_sensor","dagster_stop_schedule","dagster_stop_sensor","dagster_terminate_run","dagster_wipe_asset","databricks_cancel_run","databricks_execute_sql","databricks_get_cluster","databricks_get_job","databricks_get_run","databricks_get_run_output","databricks_get_statement","databricks_list_clusters","databricks_list_jobs","databricks_list_runs","databricks_list_warehouses","databricks_run_job","datadog_cancel_downtime","datadog_create_downtime","datadog_create_event","datadog_create_monitor","datadog_get_monitor","datadog_list_downtimes","datadog_list_monitors","datadog_mute_monitor","datadog_query_logs","datadog_query_timeseries","datadog_send_logs","datadog_submit_metrics","datagma_enrich_company","datagma_enrich_person","datagma_find_email","datagma_find_phone","datagma_get_credits","daytona_create_sandbox","daytona_delete_sandbox","daytona_download_file","daytona_execute_command","daytona_get_sandbox","daytona_git_clone","daytona_list_files","daytona_list_sandboxes","daytona_run_code","daytona_start_sandbox","daytona_stop_sandbox","daytona_upload_file","deployed_block_executor","deployments_deploy","deployments_get_version","deployments_list_versions","deployments_promote","deployments_undeploy","devin_append_session_tags","devin_archive_session","devin_create_session","devin_get_session","devin_get_session_tags","devin_list_session_attachments","devin_list_session_messages","devin_list_sessions","devin_replace_session_tags","devin_send_message","devin_terminate_session","discord_add_reaction","discord_archive_thread","discord_assign_role","discord_ban_member","discord_bulk_delete_messages","discord_create_channel","discord_create_invite","discord_create_role","discord_create_thread","discord_create_webhook","discord_delete_channel","discord_delete_invite","discord_delete_message","discord_delete_role","discord_delete_webhook","discord_edit_message","discord_execute_webhook","discord_get_channel","discord_get_invite","discord_get_member","discord_get_messages","discord_get_pinned_messages","discord_get_server","discord_get_user","discord_get_webhook","discord_join_thread","discord_kick_member","discord_leave_thread","discord_list_channels","discord_list_roles","discord_pin_message","discord_remove_reaction","discord_remove_role","discord_send_message","discord_unban_member","discord_unpin_message","discord_update_channel","discord_update_member","discord_update_role","docusign_create_from_template","docusign_download_document","docusign_get_envelope","docusign_list_envelopes","docusign_list_recipients","docusign_list_templates","docusign_send_envelope","docusign_void_envelope","downdetector_get_company","downdetector_get_company_attribution","downdetector_get_company_baseline","downdetector_get_company_events","downdetector_get_company_incidents","downdetector_get_company_indicators","downdetector_get_company_last_15","downdetector_get_company_status","downdetector_get_provider","downdetector_get_reports","downdetector_get_site_companies","downdetector_list_categories","downdetector_list_incidents","downdetector_list_sites","downdetector_search_companies","dropbox_copy","dropbox_create_folder","dropbox_create_shared_link","dropbox_delete","dropbox_download","dropbox_get_metadata","dropbox_list_folder","dropbox_list_revisions","dropbox_list_shared_links","dropbox_move","dropbox_restore","dropbox_search","dropbox_upload","dropcontact_enrich_contact","dspy_chain_of_thought","dspy_predict","dspy_react","dub_bulk_create_links","dub_bulk_delete_links","dub_bulk_update_links","dub_create_link","dub_create_tag","dub_delete_link","dub_get_analytics","dub_get_events","dub_get_link","dub_get_links_count","dub_get_qr_code","dub_list_domains","dub_list_folders","dub_list_links","dub_list_tags","dub_update_link","dub_upsert_link","duckduckgo_search","dynamodb_delete","dynamodb_get","dynamodb_introspect","dynamodb_put","dynamodb_query","dynamodb_scan","dynamodb_update","dynatrace_add_problem_comment","dynatrace_add_tags","dynatrace_close_problem","dynatrace_create_settings_object","dynatrace_create_slo","dynatrace_delete_problem_comment","dynatrace_delete_settings_object","dynatrace_delete_slo","dynatrace_delete_tag","dynatrace_execute_synthetic_monitors","dynatrace_get_attack","dynatrace_get_audit_logs","dynatrace_get_entity","dynatrace_get_event","dynatrace_get_metric","dynatrace_get_problem","dynatrace_get_problem_comment","dynatrace_get_security_problem","dynatrace_get_settings_object","dynatrace_get_slo","dynatrace_get_synthetic_batch","dynatrace_ingest_event","dynatrace_ingest_logs","dynatrace_ingest_metrics","dynatrace_list_attacks","dynatrace_list_entities","dynatrace_list_entity_types","dynatrace_list_events","dynatrace_list_metrics","dynatrace_list_problem_comments","dynatrace_list_problems","dynatrace_list_remediation_items","dynatrace_list_security_problems","dynatrace_list_settings_objects","dynatrace_list_settings_schemas","dynatrace_list_slos","dynatrace_list_synthetic_monitors","dynatrace_list_tags","dynatrace_mute_security_problem","dynatrace_mute_security_problems","dynatrace_query_metrics","dynatrace_search_logs","dynatrace_unmute_security_problem","dynatrace_unmute_security_problems","dynatrace_update_problem_comment","dynatrace_update_settings_object","dynatrace_update_slo","elasticsearch_bulk","elasticsearch_cluster_health","elasticsearch_cluster_stats","elasticsearch_count","elasticsearch_create_index","elasticsearch_delete_document","elasticsearch_delete_index","elasticsearch_get_document","elasticsearch_get_index","elasticsearch_index_document","elasticsearch_list_indices","elasticsearch_search","elasticsearch_update_document","elevenlabs_audio_isolation","elevenlabs_edit_voice_settings","elevenlabs_get_user","elevenlabs_get_voice","elevenlabs_get_voice_settings","elevenlabs_list_models","elevenlabs_list_voices","elevenlabs_sound_effects","elevenlabs_speech_to_speech","elevenlabs_tts","emailbison_attach_leads_to_campaign","emailbison_attach_tags_to_leads","emailbison_create_campaign","emailbison_create_lead","emailbison_create_tag","emailbison_get_lead","emailbison_list_campaigns","emailbison_list_leads","emailbison_list_replies","emailbison_list_tags","emailbison_update_campaign","emailbison_update_campaign_status","emailbison_update_lead","embeddings_cohere","embeddings_gemini","embeddings_mistral","embeddings_openai","embeddings_openrouter","enrich_check_credits","enrich_company_funding","enrich_company_lookup","enrich_company_revenue","enrich_disposable_email_check","enrich_email_to_ip","enrich_email_to_person_lite","enrich_email_to_phone","enrich_email_to_profile","enrich_find_email","enrich_get_post_details","enrich_ip_to_company","enrich_linkedin_profile","enrich_linkedin_to_personal_email","enrich_linkedin_to_work_email","enrich_phone_finder","enrich_reverse_hash_lookup","enrich_sales_pointer_people","enrich_search_company","enrich_search_company_activities","enrich_search_company_employees","enrich_search_jobs","enrich_search_logo","enrich_search_people","enrich_search_people_activities","enrich_search_post_comments","enrich_search_post_comments_by_url","enrich_search_post_reactions","enrich_search_post_reactions_by_url","enrich_search_posts","enrich_search_similar_companies","enrich_verify_email","enrichment_run","enrow_find_email","enrow_verify_email","evernote_copy_note","evernote_create_note","evernote_create_notebook","evernote_create_tag","evernote_delete_note","evernote_get_note","evernote_get_notebook","evernote_list_notebooks","evernote_list_tags","evernote_search_notes","evernote_update_note","exa_agent","exa_answer","exa_find_similar_links","exa_get_contents","exa_search","extend_parser","extend_parser_v2","fathom_get_summary","fathom_get_transcript","fathom_list_meeting_types","fathom_list_meetings","fathom_list_team_members","fathom_list_teams","file_append","file_compress","file_decompress","file_fetch","file_get","file_get_content","file_manage_sharing","file_parser","file_parser_v2","file_parser_v3","file_read","file_write","findymail_find_email_from_linkedin","findymail_find_email_from_name","findymail_find_emails_by_domain","findymail_find_employees","findymail_find_phone","findymail_get_company","findymail_get_credits","findymail_lookup_technologies","findymail_reverse_email_lookup","findymail_search_technologies","findymail_verify_email","firecrawl_agent","firecrawl_batch_scrape","firecrawl_batch_scrape_status","firecrawl_cancel_crawl","firecrawl_crawl","firecrawl_crawl_status","firecrawl_credit_usage","firecrawl_extract","firecrawl_extract_status","firecrawl_map","firecrawl_parse","firecrawl_scrape","firecrawl_search","fireflies_add_to_live_meeting","fireflies_create_bite","fireflies_delete_transcript","fireflies_get_transcript","fireflies_get_user","fireflies_list_bites","fireflies_list_contacts","fireflies_list_transcripts","fireflies_list_users","fireflies_upload_audio","flint_create_task","flint_generate_pages","flint_get_task","function_execute","gamma_check_status","gamma_generate","gamma_generate_from_template","gamma_list_folders","gamma_list_themes","github_add_assignees","github_add_assignees_v2","github_add_labels","github_add_labels_v2","github_cancel_workflow_run","github_cancel_workflow_run_v2","github_check_star","github_check_star_v2","github_close_issue","github_close_issue_v2","github_close_pr","github_close_pr_v2","github_comment","github_comment_v2","github_compare_commits","github_compare_commits_v2","github_create_branch","github_create_branch_v2","github_create_comment_reaction","github_create_comment_reaction_v2","github_create_file","github_create_file_v2","github_create_gist","github_create_gist_v2","github_create_issue","github_create_issue_reaction","github_create_issue_reaction_v2","github_create_issue_v2","github_create_milestone","github_create_milestone_v2","github_create_pr","github_create_pr_review","github_create_pr_review_v2","github_create_pr_v2","github_create_project","github_create_project_v2","github_create_release","github_create_release_v2","github_delete_branch","github_delete_branch_v2","github_delete_comment","github_delete_comment_reaction","github_delete_comment_reaction_v2","github_delete_comment_v2","github_delete_file","github_delete_file_v2","github_delete_gist","github_delete_gist_v2","github_delete_issue_reaction","github_delete_issue_reaction_v2","github_delete_milestone","github_delete_milestone_v2","github_delete_project","github_delete_project_v2","github_delete_release","github_delete_release_v2","github_fork_gist","github_fork_gist_v2","github_fork_repo","github_fork_repo_v2","github_get_branch","github_get_branch_protection","github_get_branch_protection_v2","github_get_branch_v2","github_get_commit","github_get_commit_v2","github_get_file_content","github_get_file_content_v2","github_get_gist","github_get_gist_v2","github_get_issue","github_get_issue_v2","github_get_latest_release","github_get_latest_release_v2","github_get_milestone","github_get_milestone_v2","github_get_pr_files","github_get_pr_files_v2","github_get_project","github_get_project_v2","github_get_readme","github_get_readme_v2","github_get_release","github_get_release_v2","github_get_tree","github_get_tree_v2","github_get_workflow","github_get_workflow_run","github_get_workflow_run_v2","github_get_workflow_v2","github_issue_comment","github_issue_comment_v2","github_job_logs","github_latest_commit","github_latest_commit_v2","github_list_branches","github_list_branches_v2","github_list_commits","github_list_commits_v2","github_list_forks","github_list_forks_v2","github_list_gists","github_list_gists_v2","github_list_issue_comments","github_list_issue_comments_v2","github_list_issues","github_list_issues_v2","github_list_milestones","github_list_milestones_v2","github_list_pr_comments","github_list_pr_comments_v2","github_list_projects","github_list_projects_v2","github_list_prs","github_list_prs_v2","github_list_releases","github_list_releases_v2","github_list_review_threads","github_list_stargazers","github_list_stargazers_v2","github_list_tags","github_list_tags_v2","github_list_workflow_runs","github_list_workflow_runs_v2","github_list_workflows","github_list_workflows_v2","github_merge_pr","github_merge_pr_v2","github_pr","github_pr_v2","github_remove_label","github_remove_label_v2","github_reply_review_thread","github_repo_info","github_repo_info_v2","github_request_reviewers","github_request_reviewers_v2","github_rerun_workflow","github_rerun_workflow_v2","github_resolve_review_thread","github_search_code","github_search_code_v2","github_search_commits","github_search_commits_v2","github_search_issues","github_search_issues_v2","github_search_repos","github_search_repos_v2","github_search_users","github_search_users_v2","github_star_gist","github_star_gist_v2","github_star_repo","github_star_repo_v2","github_status_check_rollup","github_trigger_workflow","github_trigger_workflow_v2","github_unstar_gist","github_unstar_gist_v2","github_unstar_repo","github_unstar_repo_v2","github_update_branch_protection","github_update_branch_protection_v2","github_update_comment","github_update_comment_v2","github_update_file","github_update_file_v2","github_update_gist","github_update_gist_v2","github_update_issue","github_update_issue_v2","github_update_milestone","github_update_milestone_v2","github_update_pr","github_update_pr_v2","github_update_project","github_update_project_v2","github_update_release","github_update_release_v2","gitlab_activate_user","gitlab_add_member","gitlab_add_saml_group_link","gitlab_approve_access_request","gitlab_approve_merge_request","gitlab_approve_user","gitlab_ban_user","gitlab_block_user","gitlab_cancel_pipeline","gitlab_compare_branches","gitlab_create_branch","gitlab_create_file","gitlab_create_issue","gitlab_create_issue_note","gitlab_create_merge_request","gitlab_create_merge_request_note","gitlab_create_pipeline","gitlab_create_release","gitlab_create_user","gitlab_deactivate_user","gitlab_delete_branch","gitlab_delete_issue","gitlab_delete_saml_group_link","gitlab_delete_user","gitlab_delete_user_identity","gitlab_deny_access_request","gitlab_get_file","gitlab_get_group","gitlab_get_issue","gitlab_get_job_log","gitlab_get_merge_request","gitlab_get_merge_request_changes","gitlab_get_pipeline","gitlab_get_project","gitlab_invite_member","gitlab_list_access_requests","gitlab_list_branches","gitlab_list_commits","gitlab_list_groups","gitlab_list_invitations","gitlab_list_issues","gitlab_list_members","gitlab_list_merge_requests","gitlab_list_pipeline_jobs","gitlab_list_pipelines","gitlab_list_projects","gitlab_list_releases","gitlab_list_repository_tree","gitlab_list_saml_group_links","gitlab_list_user_memberships","gitlab_merge_merge_request","gitlab_play_job","gitlab_reject_user","gitlab_remove_member","gitlab_retry_pipeline","gitlab_revoke_invitation","gitlab_search_users","gitlab_unban_user","gitlab_unblock_user","gitlab_update_file","gitlab_update_invitation","gitlab_update_issue","gitlab_update_member","gitlab_update_merge_request","gitlab_update_user","gmail_add_label","gmail_add_label_v2","gmail_archive","gmail_archive_v2","gmail_create_label_v2","gmail_delete","gmail_delete_draft_v2","gmail_delete_label_v2","gmail_delete_v2","gmail_draft","gmail_draft_v2","gmail_edit_draft_v2","gmail_get_draft_v2","gmail_get_thread_v2","gmail_list_drafts_v2","gmail_list_labels_v2","gmail_list_threads_v2","gmail_mark_read","gmail_mark_read_v2","gmail_mark_unread","gmail_mark_unread_v2","gmail_move","gmail_move_v2","gmail_read","gmail_read_v2","gmail_remove_label","gmail_remove_label_v2","gmail_search","gmail_search_v2","gmail_send","gmail_send_v2","gmail_trash_thread_v2","gmail_unarchive","gmail_unarchive_v2","gmail_untrash_thread_v2","gmail_update_label_v2","gong_aggregate_activity","gong_aggregate_by_period","gong_answered_scorecards","gong_ask_anything","gong_assign_flow_prospects","gong_create_call","gong_day_by_day_activity","gong_get_brief","gong_get_call","gong_get_call_transcript","gong_get_coaching","gong_get_extensive_calls","gong_get_folder_content","gong_get_logs","gong_get_prospect_flows","gong_get_user","gong_interaction_stats","gong_list_calls","gong_list_flows","gong_list_library_folders","gong_list_scorecards","gong_list_trackers","gong_list_users","gong_list_workspaces","gong_lookup_email","gong_lookup_phone","gong_purge_email_address","gong_purge_phone_number","gong_unassign_flow_prospects","google_ads_ad_performance","google_ads_campaign_performance","google_ads_list_ad_groups","google_ads_list_campaigns","google_ads_list_customers","google_ads_search","google_appsheet_add_rows","google_appsheet_delete_rows","google_appsheet_edit_rows","google_appsheet_find_rows","google_bigquery_create_dataset","google_bigquery_create_table","google_bigquery_delete_dataset","google_bigquery_delete_table","google_bigquery_get_query_results","google_bigquery_get_table","google_bigquery_insert_rows","google_bigquery_list_datasets","google_bigquery_list_table_data","google_bigquery_list_tables","google_bigquery_query","google_books_volume_details","google_books_volume_search","google_calendar_create","google_calendar_create_calendar","google_calendar_create_calendar_v2","google_calendar_create_v2","google_calendar_delete","google_calendar_delete_calendar","google_calendar_delete_calendar_v2","google_calendar_delete_v2","google_calendar_freebusy","google_calendar_freebusy_v2","google_calendar_get","google_calendar_get_v2","google_calendar_instances","google_calendar_instances_v2","google_calendar_invite","google_calendar_invite_v2","google_calendar_list","google_calendar_list_acl","google_calendar_list_acl_v2","google_calendar_list_calendars","google_calendar_list_calendars_v2","google_calendar_list_v2","google_calendar_move","google_calendar_move_v2","google_calendar_quick_add","google_calendar_quick_add_v2","google_calendar_share_calendar","google_calendar_share_calendar_v2","google_calendar_unshare_calendar","google_calendar_unshare_calendar_v2","google_calendar_update","google_calendar_update_acl","google_calendar_update_acl_v2","google_calendar_update_calendar","google_calendar_update_calendar_v2","google_calendar_update_v2","google_contacts_create","google_contacts_delete","google_contacts_get","google_contacts_list","google_contacts_search","google_contacts_update","google_docs_create","google_docs_create_named_range","google_docs_create_paragraph_bullets","google_docs_delete_content_range","google_docs_delete_named_range","google_docs_delete_paragraph_bullets","google_docs_insert_image","google_docs_insert_page_break","google_docs_insert_table","google_docs_insert_text","google_docs_read","google_docs_replace_text","google_docs_update_paragraph_style","google_docs_update_text_style","google_docs_write","google_drive_copy","google_drive_create_comment","google_drive_create_folder","google_drive_delete","google_drive_delete_comment","google_drive_download","google_drive_export","google_drive_get_about","google_drive_get_content","google_drive_get_file","google_drive_get_revision","google_drive_list","google_drive_list_comments","google_drive_list_permissions","google_drive_list_revisions","google_drive_move","google_drive_search","google_drive_share","google_drive_trash","google_drive_unshare","google_drive_untrash","google_drive_update","google_drive_upload","google_forms_batch_update","google_forms_create_form","google_forms_create_watch","google_forms_delete_watch","google_forms_get_form","google_forms_get_responses","google_forms_list_watches","google_forms_renew_watch","google_forms_set_publish_settings","google_groups_add_alias","google_groups_add_member","google_groups_create_group","google_groups_delete_group","google_groups_get_group","google_groups_get_member","google_groups_get_settings","google_groups_has_member","google_groups_list_aliases","google_groups_list_groups","google_groups_list_members","google_groups_remove_alias","google_groups_remove_member","google_groups_update_group","google_groups_update_member","google_groups_update_settings","google_maps_air_quality","google_maps_directions","google_maps_distance_matrix","google_maps_elevation","google_maps_geocode","google_maps_geolocate","google_maps_place_details","google_maps_places_nearby","google_maps_places_search","google_maps_pollen","google_maps_reverse_geocode","google_maps_snap_to_roads","google_maps_solar","google_maps_speed_limits","google_maps_timezone","google_maps_validate_address","google_meet_create_space","google_meet_end_conference","google_meet_get_conference_record","google_meet_get_space","google_meet_list_conference_records","google_meet_list_participants","google_pagespeed_analyze","google_search","google_sheets_append","google_sheets_append_v2","google_sheets_batch_clear_v2","google_sheets_batch_get_v2","google_sheets_batch_update_v2","google_sheets_clear_v2","google_sheets_copy_sheet_v2","google_sheets_create_spreadsheet_v2","google_sheets_delete_rows_v2","google_sheets_delete_sheet_v2","google_sheets_delete_spreadsheet_v2","google_sheets_get_spreadsheet_v2","google_sheets_read","google_sheets_read_v2","google_sheets_update","google_sheets_update_v2","google_sheets_write","google_sheets_write_v2","google_slides_add_image","google_slides_add_slide","google_slides_batch_update","google_slides_copy_presentation","google_slides_create","google_slides_create_line","google_slides_create_paragraph_bullets","google_slides_create_shape","google_slides_create_sheets_chart","google_slides_create_table","google_slides_create_video","google_slides_delete_object","google_slides_delete_paragraph_bullets","google_slides_delete_table_column","google_slides_delete_table_row","google_slides_delete_text","google_slides_duplicate_object","google_slides_export_presentation","google_slides_get_page","google_slides_get_thumbnail","google_slides_group_objects","google_slides_insert_table_columns","google_slides_insert_table_rows","google_slides_insert_text","google_slides_merge_table_cells","google_slides_read","google_slides_refresh_sheets_chart","google_slides_replace_all_shapes_with_image","google_slides_replace_all_shapes_with_sheets_chart","google_slides_replace_all_text","google_slides_replace_image","google_slides_reroute_line","google_slides_ungroup_objects","google_slides_unmerge_table_cells","google_slides_update_image_properties","google_slides_update_line_category","google_slides_update_line_properties","google_slides_update_page_element_alt_text","google_slides_update_page_element_transform","google_slides_update_page_elements_z_order","google_slides_update_page_properties","google_slides_update_paragraph_style","google_slides_update_shape_properties","google_slides_update_slide_properties","google_slides_update_slides_position","google_slides_update_table_border_properties","google_slides_update_table_cell_properties","google_slides_update_table_column_properties","google_slides_update_table_row_properties","google_slides_update_text_style","google_slides_update_video_properties","google_slides_write","google_tasks_create","google_tasks_delete","google_tasks_get","google_tasks_list","google_tasks_list_task_lists","google_tasks_update","google_translate_detect","google_translate_text","google_vault_add_held_accounts","google_vault_add_matters_permissions","google_vault_close_matters","google_vault_create_matters","google_vault_create_matters_export","google_vault_create_matters_holds","google_vault_create_saved_query","google_vault_delete_matters","google_vault_delete_matters_export","google_vault_delete_matters_holds","google_vault_delete_saved_query","google_vault_download_export_file","google_vault_list_matters","google_vault_list_matters_export","google_vault_list_matters_holds","google_vault_list_saved_queries","google_vault_remove_held_accounts","google_vault_remove_matters_permissions","google_vault_reopen_matters","google_vault_undelete_matters","google_vault_update_matters","google_vault_update_matters_holds","grafana_check_data_source_health","grafana_create_alert_rule","grafana_create_annotation","grafana_create_contact_point","grafana_create_dashboard","grafana_create_folder","grafana_delete_alert_rule","grafana_delete_annotation","grafana_delete_dashboard","grafana_delete_folder","grafana_get_alert_rule","grafana_get_dashboard","grafana_get_data_source","grafana_get_folder","grafana_get_health","grafana_list_alert_rules","grafana_list_annotations","grafana_list_contact_points","grafana_list_dashboards","grafana_list_data_sources","grafana_list_folders","grafana_update_alert_rule","grafana_update_annotation","grafana_update_dashboard","grafana_update_folder","grain_create_hook","grain_create_hook_v2","grain_delete_hook","grain_delete_hook_v2","grain_get_recording","grain_get_transcript","grain_list_hooks","grain_list_hooks_v2","grain_list_meeting_types","grain_list_recordings","grain_list_teams","grain_list_views","granola_get_note","granola_list_folders","granola_list_notes","greenhouse_get_application","greenhouse_get_candidate","greenhouse_get_job","greenhouse_get_user","greenhouse_list_applications","greenhouse_list_candidates","greenhouse_list_departments","greenhouse_list_job_stages","greenhouse_list_jobs","greenhouse_list_offices","greenhouse_list_users","greptile_index_repo","greptile_query","greptile_search","greptile_status","guardrails_validate","hex_cancel_run","hex_create_collection","hex_create_group","hex_deactivate_user","hex_delete_group","hex_get_collection","hex_get_data_connection","hex_get_group","hex_get_project","hex_get_project_runs","hex_get_queried_tables","hex_get_run_status","hex_list_collections","hex_list_data_connections","hex_list_groups","hex_list_projects","hex_list_users","hex_run_project","hex_update_collection","hex_update_group","hex_update_project","http_request","hubspot_add_list_memberships","hubspot_create_appointment","hubspot_create_association","hubspot_create_company","hubspot_create_contact","hubspot_create_deal","hubspot_create_email","hubspot_create_line_item","hubspot_create_list","hubspot_create_note","hubspot_create_ticket","hubspot_delete_association","hubspot_delete_company","hubspot_delete_contact","hubspot_delete_deal","hubspot_delete_line_item","hubspot_delete_ticket","hubspot_get_appointment","hubspot_get_association_labels","hubspot_get_cart","hubspot_get_company","hubspot_get_contact","hubspot_get_deal","hubspot_get_email","hubspot_get_line_item","hubspot_get_list","hubspot_get_list_memberships","hubspot_get_marketing_event","hubspot_get_note","hubspot_get_properties","hubspot_get_quote","hubspot_get_ticket","hubspot_get_users","hubspot_list_appointments","hubspot_list_associations","hubspot_list_carts","hubspot_list_companies","hubspot_list_contacts","hubspot_list_deals","hubspot_list_emails","hubspot_list_line_items","hubspot_list_lists","hubspot_list_marketing_events","hubspot_list_notes","hubspot_list_owners","hubspot_list_quotes","hubspot_list_tickets","hubspot_remove_list_memberships","hubspot_search_companies","hubspot_search_contacts","hubspot_search_deals","hubspot_search_emails","hubspot_search_line_items","hubspot_search_notes","hubspot_search_quotes","hubspot_search_tickets","hubspot_update_appointment","hubspot_update_company","hubspot_update_contact","hubspot_update_deal","hubspot_update_line_item","hubspot_update_ticket","huggingface_chat","hunter_companies_find","hunter_discover","hunter_domain_search","hunter_email_count","hunter_email_finder","hunter_email_verifier","iam_add_user_to_group","iam_attach_role_policy","iam_attach_user_policy","iam_create_access_key","iam_create_role","iam_create_user","iam_delete_access_key","iam_delete_role","iam_delete_user","iam_detach_role_policy","iam_detach_user_policy","iam_get_role","iam_get_user","iam_list_attached_role_policies","iam_list_attached_user_policies","iam_list_groups","iam_list_policies","iam_list_roles","iam_list_users","iam_remove_user_from_group","iam_simulate_principal_policy","icypeas_find_email","icypeas_verify_email","identity_center_check_assignment_deletion_status","identity_center_check_assignment_status","identity_center_create_account_assignment","identity_center_delete_account_assignment","identity_center_describe_account","identity_center_get_group","identity_center_get_user","identity_center_list_account_assignments","identity_center_list_accounts","identity_center_list_groups","identity_center_list_instances","identity_center_list_permission_sets","image_generate","incidentio_actions_create","incidentio_actions_list","incidentio_actions_show","incidentio_actions_update","incidentio_alert_events_create","incidentio_alerts_list","incidentio_alerts_resolve","incidentio_alerts_show","incidentio_catalog_entries_list","incidentio_catalog_types_list","incidentio_custom_fields_create","incidentio_custom_fields_delete","incidentio_custom_fields_list","incidentio_custom_fields_show","incidentio_custom_fields_update","incidentio_escalation_paths_create","incidentio_escalation_paths_delete","incidentio_escalation_paths_list","incidentio_escalation_paths_show","incidentio_escalation_paths_update","incidentio_escalations_cancel","incidentio_escalations_create","incidentio_escalations_list","incidentio_escalations_show","incidentio_follow_ups_create","incidentio_follow_ups_list","incidentio_follow_ups_show","incidentio_follow_ups_update","incidentio_incident_alerts_list","incidentio_incident_memberships_create","incidentio_incident_memberships_revoke","incidentio_incident_participants_list","incidentio_incident_roles_create","incidentio_incident_roles_delete","incidentio_incident_roles_list","incidentio_incident_roles_show","incidentio_incident_roles_update","incidentio_incident_statuses_list","incidentio_incident_timestamps_list","incidentio_incident_timestamps_show","incidentio_incident_types_list","incidentio_incident_updates_list","incidentio_incidents_create","incidentio_incidents_list","incidentio_incidents_show","incidentio_incidents_update","incidentio_on_call_now","incidentio_schedule_entries_list","incidentio_schedule_overrides_create","incidentio_schedule_overrides_list","incidentio_schedules_create","incidentio_schedules_delete","incidentio_schedules_list","incidentio_schedules_show","incidentio_schedules_update","incidentio_severities_list","incidentio_teams_list","incidentio_teams_show","incidentio_users_list","incidentio_users_show","incidentio_workflows_create","incidentio_workflows_delete","incidentio_workflows_list","incidentio_workflows_show","incidentio_workflows_update","infisical_create_secret","infisical_delete_secret","infisical_get_secret","infisical_list_secrets","infisical_update_secret","instagram_delete_comment","instagram_download_media","instagram_get_account_insights","instagram_get_container_status","instagram_get_conversation_messages","instagram_get_media","instagram_get_media_insights","instagram_get_message","instagram_get_profile","instagram_get_publishing_limit","instagram_hide_comment","instagram_list_comments","instagram_list_conversations","instagram_list_media","instagram_list_stories","instagram_private_reply","instagram_publish_carousel","instagram_publish_image","instagram_publish_reel","instagram_publish_story","instagram_publish_video","instagram_reply_to_comment","instagram_send_text_message","instagram_set_comments_enabled","instantly_activate_campaign","instantly_create_campaign","instantly_create_lead","instantly_create_lead_list","instantly_delete_campaign","instantly_delete_leads","instantly_get_lead","instantly_list_campaigns","instantly_list_emails","instantly_list_lead_lists","instantly_list_leads","instantly_patch_campaign","instantly_patch_lead","instantly_pause_campaign","instantly_reply_to_email","instantly_update_lead_interest_status","intercom_assign_conversation_v2","intercom_attach_contact_to_company_v2","intercom_close_conversation_v2","intercom_create_company","intercom_create_company_v2","intercom_create_contact","intercom_create_contact_v2","intercom_create_event_v2","intercom_create_message","intercom_create_message_v2","intercom_create_note_v2","intercom_create_tag_v2","intercom_create_ticket","intercom_create_ticket_v2","intercom_delete_contact","intercom_delete_contact_v2","intercom_detach_contact_from_company_v2","intercom_get_company","intercom_get_company_v2","intercom_get_contact","intercom_get_contact_v2","intercom_get_conversation","intercom_get_conversation_v2","intercom_get_ticket","intercom_get_ticket_v2","intercom_list_admins_v2","intercom_list_companies","intercom_list_companies_v2","intercom_list_contacts","intercom_list_contacts_v2","intercom_list_conversations","intercom_list_conversations_v2","intercom_list_tags_v2","intercom_open_conversation_v2","intercom_reply_conversation","intercom_reply_conversation_v2","intercom_search_contacts","intercom_search_contacts_v2","intercom_search_conversations","intercom_search_conversations_v2","intercom_snooze_conversation_v2","intercom_tag_contact_v2","intercom_tag_conversation_v2","intercom_untag_contact_v2","intercom_update_contact","intercom_update_contact_v2","intercom_update_ticket_v2","jina_read_url","jina_search","jira_add_attachment","jira_add_comment","jira_add_watcher","jira_add_worklog","jira_assign_issue","jira_bulk_read","jira_create_issue_link","jira_delete_attachment","jira_delete_comment","jira_delete_issue","jira_delete_issue_link","jira_delete_worklog","jira_get_attachments","jira_get_comments","jira_get_fields","jira_get_project","jira_get_transitions","jira_get_users","jira_get_worklogs","jira_list_issue_types","jira_list_projects","jira_remove_watcher","jira_retrieve","jira_search_issues","jira_search_users","jira_transition_issue","jira_update","jira_update_comment","jira_update_worklog","jira_write","jsm_add_comment","jsm_add_customer","jsm_add_organization","jsm_add_participants","jsm_answer_approval","jsm_attach_form","jsm_copy_forms","jsm_create_object","jsm_create_organization","jsm_create_request","jsm_delete_form","jsm_delete_object","jsm_externalise_form","jsm_get_approvals","jsm_get_comments","jsm_get_customers","jsm_get_form","jsm_get_form_answers","jsm_get_form_structure","jsm_get_form_templates","jsm_get_issue_forms","jsm_get_object","jsm_get_object_schema","jsm_get_object_type_attributes","jsm_get_organizations","jsm_get_participants","jsm_get_queues","jsm_get_request","jsm_get_request_type_fields","jsm_get_request_types","jsm_get_requests","jsm_get_service_desks","jsm_get_sla","jsm_get_transitions","jsm_internalise_form","jsm_list_object_schemas","jsm_list_object_types","jsm_reopen_form","jsm_save_form_answers","jsm_search_objects_aql","jsm_submit_form","jsm_transition_request","jsm_update_object","jupyter_copy_content","jupyter_create_file","jupyter_create_session","jupyter_delete_content","jupyter_delete_session","jupyter_get_content","jupyter_interrupt_kernel","jupyter_list_contents","jupyter_list_kernels","jupyter_list_kernelspecs","jupyter_list_sessions","jupyter_rename_content","jupyter_restart_kernel","jupyter_start_kernel","jupyter_stop_kernel","jupyter_upload_file","kalshi_amend_order","kalshi_amend_order_v2","kalshi_cancel_order","kalshi_cancel_order_v2","kalshi_create_order","kalshi_create_order_v2","kalshi_get_balance","kalshi_get_balance_v2","kalshi_get_candlesticks","kalshi_get_candlesticks_v2","kalshi_get_event","kalshi_get_event_candlesticks","kalshi_get_event_candlesticks_v2","kalshi_get_event_v2","kalshi_get_events","kalshi_get_events_v2","kalshi_get_exchange_announcements","kalshi_get_exchange_announcements_v2","kalshi_get_exchange_schedule","kalshi_get_exchange_schedule_v2","kalshi_get_exchange_status","kalshi_get_exchange_status_v2","kalshi_get_fills","kalshi_get_fills_v2","kalshi_get_market","kalshi_get_market_v2","kalshi_get_markets","kalshi_get_markets_v2","kalshi_get_order","kalshi_get_order_v2","kalshi_get_orderbook","kalshi_get_orderbook_v2","kalshi_get_orders","kalshi_get_orders_v2","kalshi_get_positions","kalshi_get_positions_v2","kalshi_get_series_by_ticker","kalshi_get_series_by_ticker_v2","kalshi_get_series_list","kalshi_get_series_list_v2","kalshi_get_settlements","kalshi_get_settlements_v2","kalshi_get_trades","kalshi_get_trades_v2","ketch_get_consent","ketch_get_subscriptions","ketch_invoke_right","ketch_set_consent","ketch_set_subscriptions","knowledge_create_document","knowledge_delete_chunk","knowledge_delete_document","knowledge_get_connector","knowledge_get_document","knowledge_list_chunks","knowledge_list_connectors","knowledge_list_documents","knowledge_list_tags","knowledge_search","knowledge_trigger_sync","knowledge_update_chunk","knowledge_upload_chunk","knowledge_upsert_document","langsmith_create_feedback","langsmith_create_run","langsmith_create_runs_batch","langsmith_get_run","langsmith_update_run","latex_compile","latex_get_package","latex_list_fonts","latex_search_packages","launchdarkly_create_flag","launchdarkly_delete_flag","launchdarkly_get_audit_log","launchdarkly_get_flag","launchdarkly_get_flag_status","launchdarkly_list_environments","launchdarkly_list_flags","launchdarkly_list_members","launchdarkly_list_projects","launchdarkly_list_segments","launchdarkly_toggle_flag","launchdarkly_update_flag","leadmagic_company_search","leadmagic_email_to_profile","leadmagic_find_email","leadmagic_find_mobile","leadmagic_get_credits","leadmagic_profile_search","leadmagic_profile_to_email","leadmagic_role_finder","leadmagic_validate_email","lemlist_get_activities","lemlist_get_lead","lemlist_send_email","linear_add_label_to_issue","linear_add_label_to_project","linear_archive_issue","linear_archive_label","linear_archive_project","linear_create_attachment","linear_create_comment","linear_create_customer","linear_create_customer_request","linear_create_customer_status","linear_create_customer_tier","linear_create_cycle","linear_create_favorite","linear_create_issue","linear_create_issue_relation","linear_create_label","linear_create_project","linear_create_project_label","linear_create_project_milestone","linear_create_project_status","linear_create_project_update","linear_create_workflow_state","linear_delete_attachment","linear_delete_comment","linear_delete_customer","linear_delete_customer_status","linear_delete_customer_tier","linear_delete_issue","linear_delete_issue_relation","linear_delete_project","linear_delete_project_label","linear_delete_project_milestone","linear_delete_project_status","linear_get_active_cycle","linear_get_customer","linear_get_cycle","linear_get_issue","linear_get_project","linear_get_viewer","linear_list_attachments","linear_list_comments","linear_list_customer_requests","linear_list_customer_statuses","linear_list_customer_tiers","linear_list_customers","linear_list_cycles","linear_list_favorites","linear_list_issue_relations","linear_list_labels","linear_list_notifications","linear_list_project_labels","linear_list_project_milestones","linear_list_project_statuses","linear_list_project_updates","linear_list_projects","linear_list_teams","linear_list_users","linear_list_workflow_states","linear_merge_customers","linear_read_issues","linear_remove_label_from_issue","linear_remove_label_from_project","linear_search_issues","linear_unarchive_issue","linear_update_attachment","linear_update_comment","linear_update_customer","linear_update_customer_request","linear_update_customer_status","linear_update_customer_tier","linear_update_issue","linear_update_label","linear_update_notification","linear_update_project","linear_update_project_label","linear_update_project_milestone","linear_update_project_status","linear_update_workflow_state","linkedin_get_profile","linkedin_share_post","linkup_search","linq_add_participant","linq_check_imessage","linq_check_rcs","linq_create_attachment","linq_create_chat","linq_create_contact_card","linq_create_webhook_subscription","linq_delete_attachment","linq_delete_message","linq_delete_webhook_subscription","linq_edit_message","linq_get_attachment","linq_get_chat","linq_get_contact_card","linq_get_message","linq_get_webhook_subscription","linq_leave_chat","linq_list_chats","linq_list_messages","linq_list_phone_numbers","linq_list_thread","linq_list_webhook_events","linq_list_webhook_subscriptions","linq_mark_chat_read","linq_react_to_message","linq_remove_participant","linq_send_message","linq_send_voice_memo","linq_share_contact_card","linq_start_typing","linq_stop_typing","linq_update_chat","linq_update_contact_card","linq_update_webhook_subscription","llm_chat","logfire_get_token_info","logfire_get_trace","logfire_query","logfire_search_records","logs_get","logs_get_execution","logs_get_run_details","logs_query","logs_query_runs","loops_check_contact_suppression","loops_create_contact","loops_create_contact_property","loops_delete_contact","loops_find_contact","loops_get_transactional_email","loops_list_contact_properties","loops_list_mailing_lists","loops_list_transactional_emails","loops_remove_contact_suppression","loops_send_event","loops_send_transactional_email","loops_update_contact","luma_add_guests","luma_cancel_event","luma_create_event","luma_get_event","luma_get_guest","luma_get_guests","luma_list_events","luma_lookup_event","luma_send_invites","luma_update_event","luma_update_guest_status","mailchimp_add_member","mailchimp_add_member_tags","mailchimp_add_or_update_member","mailchimp_add_segment_member","mailchimp_add_subscriber_to_automation","mailchimp_archive_member","mailchimp_create_audience","mailchimp_create_batch_operation","mailchimp_create_campaign","mailchimp_create_interest","mailchimp_create_interest_category","mailchimp_create_landing_page","mailchimp_create_merge_field","mailchimp_create_segment","mailchimp_create_template","mailchimp_delete_audience","mailchimp_delete_batch_operation","mailchimp_delete_campaign","mailchimp_delete_interest","mailchimp_delete_interest_category","mailchimp_delete_landing_page","mailchimp_delete_member","mailchimp_delete_merge_field","mailchimp_delete_segment","mailchimp_delete_template","mailchimp_get_audience","mailchimp_get_audiences","mailchimp_get_automation","mailchimp_get_automations","mailchimp_get_batch_operation","mailchimp_get_batch_operations","mailchimp_get_campaign","mailchimp_get_campaign_content","mailchimp_get_campaign_report","mailchimp_get_campaign_reports","mailchimp_get_campaigns","mailchimp_get_interest","mailchimp_get_interest_categories","mailchimp_get_interest_category","mailchimp_get_interests","mailchimp_get_landing_page","mailchimp_get_landing_pages","mailchimp_get_member","mailchimp_get_member_tags","mailchimp_get_members","mailchimp_get_merge_field","mailchimp_get_merge_fields","mailchimp_get_segment","mailchimp_get_segment_members","mailchimp_get_segments","mailchimp_get_template","mailchimp_get_templates","mailchimp_pause_automation","mailchimp_publish_landing_page","mailchimp_remove_member_tags","mailchimp_remove_segment_member","mailchimp_replicate_campaign","mailchimp_schedule_campaign","mailchimp_send_campaign","mailchimp_set_campaign_content","mailchimp_start_automation","mailchimp_unarchive_member","mailchimp_unpublish_landing_page","mailchimp_unschedule_campaign","mailchimp_update_audience","mailchimp_update_campaign","mailchimp_update_interest","mailchimp_update_interest_category","mailchimp_update_landing_page","mailchimp_update_member","mailchimp_update_merge_field","mailchimp_update_segment","mailchimp_update_template","mailgun_add_list_member","mailgun_create_mailing_list","mailgun_get_domain","mailgun_get_mailing_list","mailgun_get_message","mailgun_list_domains","mailgun_list_messages","mailgun_send_message","managed_agent_archive_session","managed_agent_create_session","managed_agent_delete_session","managed_agent_get_session","managed_agent_interrupt_session","managed_agent_list_events","managed_agent_respond_custom_tool","managed_agent_respond_tool_confirmation","managed_agent_run_session","managed_agent_send_message","managed_agent_update_session","mem0_add_memories","mem0_get_memories","mem0_search_memories","memory_add","memory_delete","memory_get","memory_get_all","microsoft_ad_add_group_member","microsoft_ad_create_group","microsoft_ad_create_user","microsoft_ad_delete_group","microsoft_ad_delete_user","microsoft_ad_get_group","microsoft_ad_get_user","microsoft_ad_list_group_members","microsoft_ad_list_groups","microsoft_ad_list_users","microsoft_ad_remove_group_member","microsoft_ad_update_group","microsoft_ad_update_user","microsoft_dataverse_associate","microsoft_dataverse_create_multiple","microsoft_dataverse_create_record","microsoft_dataverse_delete_record","microsoft_dataverse_disassociate","microsoft_dataverse_download_file","microsoft_dataverse_execute_action","microsoft_dataverse_execute_function","microsoft_dataverse_fetchxml_query","microsoft_dataverse_get_entity_metadata","microsoft_dataverse_get_record","microsoft_dataverse_list_records","microsoft_dataverse_search","microsoft_dataverse_update_multiple","microsoft_dataverse_update_record","microsoft_dataverse_upload_file","microsoft_dataverse_upsert_record","microsoft_dataverse_whoami","microsoft_excel_clear_range","microsoft_excel_create_table","microsoft_excel_delete_worksheet","microsoft_excel_format_range","microsoft_excel_read","microsoft_excel_read_v2","microsoft_excel_sort_range","microsoft_excel_table_add","microsoft_excel_worksheet_add","microsoft_excel_write","microsoft_excel_write_v2","microsoft_planner_create_bucket","microsoft_planner_create_plan","microsoft_planner_create_task","microsoft_planner_delete_bucket","microsoft_planner_delete_plan","microsoft_planner_delete_task","microsoft_planner_get_plan_details","microsoft_planner_get_task_details","microsoft_planner_list_buckets","microsoft_planner_list_plans","microsoft_planner_read_bucket","microsoft_planner_read_plan","microsoft_planner_read_task","microsoft_planner_update_bucket","microsoft_planner_update_plan","microsoft_planner_update_plan_details","microsoft_planner_update_task","microsoft_planner_update_task_details","microsoft_teams_delete_channel_message","microsoft_teams_delete_chat_message","microsoft_teams_get_message","microsoft_teams_list_channel_members","microsoft_teams_list_channels","microsoft_teams_list_chat_members","microsoft_teams_list_chats","microsoft_teams_list_team_members","microsoft_teams_list_teams","microsoft_teams_read_channel","microsoft_teams_read_chat","microsoft_teams_reply_to_message","microsoft_teams_set_reaction","microsoft_teams_unset_reaction","microsoft_teams_update_channel_message","microsoft_teams_update_chat_message","microsoft_teams_write_channel","microsoft_teams_write_chat","millionverifier_get_credits","millionverifier_verify_email","mintlify_create_agent_job","mintlify_create_assistant_message","mintlify_detect_ai_prose","mintlify_get_agent_job","mintlify_get_assistant_caller_stats","mintlify_get_assistant_conversations","mintlify_get_feedback","mintlify_get_feedback_by_page","mintlify_get_page_content","mintlify_get_searches","mintlify_get_update_status","mintlify_get_views","mintlify_get_visitors","mintlify_search","mintlify_send_agent_message","mintlify_trigger_automation","mintlify_trigger_preview","mintlify_trigger_update","mistral_parser","mistral_parser_v2","mistral_parser_v3","monday_archive_item","monday_change_column_value","monday_create_board","monday_create_column","monday_create_group","monday_create_item","monday_create_subitem","monday_create_update","monday_delete_item","monday_duplicate_item","monday_get_board","monday_get_groups","monday_get_item","monday_get_items","monday_list_boards","monday_move_item_to_group","monday_search_items","monday_update_item","mongodb_delete","mongodb_execute","mongodb_insert","mongodb_introspect","mongodb_query","mongodb_update","mysql_delete","mysql_execute","mysql_insert","mysql_introspect","mysql_query","mysql_update","neo4j_create","neo4j_delete","neo4j_execute","neo4j_introspect","neo4j_merge","neo4j_query","neo4j_update","neverbounce_get_credits","neverbounce_verify_email","new_relic_create_deployment_event","new_relic_get_entity","new_relic_nrql_query","new_relic_search_entities","notion_add_database_row","notion_add_database_row_v2","notion_append_blocks","notion_append_blocks_v2","notion_create_comment","notion_create_comment_v2","notion_create_database","notion_create_database_v2","notion_create_page","notion_create_page_v2","notion_delete_block","notion_delete_block_v2","notion_list_comments","notion_list_comments_v2","notion_list_users","notion_list_users_v2","notion_query_database","notion_query_database_v2","notion_read","notion_read_database","notion_read_database_v2","notion_read_v2","notion_retrieve_block","notion_retrieve_block_children","notion_retrieve_block_children_v2","notion_retrieve_block_v2","notion_retrieve_user","notion_retrieve_user_v2","notion_search","notion_search_v2","notion_update_block","notion_update_block_v2","notion_update_page","notion_update_page_v2","notion_write","notion_write_v2","obsidian_append_active","obsidian_append_note","obsidian_append_periodic_note","obsidian_create_note","obsidian_delete_note","obsidian_execute_command","obsidian_get_active","obsidian_get_note","obsidian_get_periodic_note","obsidian_list_commands","obsidian_list_files","obsidian_open_file","obsidian_patch_active","obsidian_patch_note","obsidian_search","okta_activate_user","okta_add_user_to_group","okta_create_group","okta_create_user","okta_deactivate_user","okta_delete_group","okta_delete_user","okta_get_group","okta_get_user","okta_list_group_members","okta_list_groups","okta_list_users","okta_remove_user_from_group","okta_reset_password","okta_suspend_user","okta_unsuspend_user","okta_update_group","okta_update_user","onedrive_copy","onedrive_create_folder","onedrive_create_share_link","onedrive_delete","onedrive_download","onedrive_get_drive_info","onedrive_get_item","onedrive_list","onedrive_move","onedrive_search","onedrive_upload","onepassword_create_item","onepassword_delete_item","onepassword_get_item","onepassword_get_item_file","onepassword_get_vault","onepassword_list_items","onepassword_list_vaults","onepassword_replace_item","onepassword_resolve_secret","onepassword_update_item","openai_embeddings","openai_image","outlook_calendar_create_event","outlook_calendar_delete_event","outlook_calendar_get_event","outlook_calendar_list_events","outlook_calendar_respond","outlook_calendar_update_event","outlook_copy","outlook_create_folder","outlook_delete","outlook_draft","outlook_forward","outlook_get_attachment","outlook_list_attachments","outlook_list_folders","outlook_mark_read","outlook_mark_unread","outlook_move","outlook_read","outlook_reply","outlook_reply_all","outlook_search","outlook_send","outlook_update_message","pagerduty_add_note","pagerduty_create_incident","pagerduty_get_incident","pagerduty_get_service","pagerduty_list_escalation_policies","pagerduty_list_incident_alerts","pagerduty_list_incidents","pagerduty_list_oncalls","pagerduty_list_schedules","pagerduty_list_services","pagerduty_list_users","pagerduty_merge_incidents","pagerduty_send_event","pagerduty_snooze_incident","pagerduty_update_incident","parallel_deep_research","parallel_extract","parallel_search","pdl_autocomplete","pdl_bulk_company_enrich","pdl_bulk_person_enrich","pdl_clean_company","pdl_clean_location","pdl_clean_school","pdl_company_enrich","pdl_company_search","pdl_person_enrich","pdl_person_identify","pdl_person_search","perplexity_chat","perplexity_search","persona_approve_inquiry","persona_create_account","persona_create_inquiry","persona_create_report","persona_decline_inquiry","persona_expire_inquiry","persona_generate_inquiry_link","persona_get_account","persona_get_case","persona_get_document","persona_get_inquiry","persona_get_report","persona_get_verification","persona_import_accounts","persona_list_accounts","persona_list_cases","persona_list_inquiries","persona_list_inquiry_templates","persona_list_reports","persona_mark_inquiry_for_review","persona_print_inquiry_pdf","persona_redact_account","persona_redact_inquiry","persona_resume_inquiry","persona_update_account","persona_update_inquiry","pinecone_delete_vectors","pinecone_describe_index","pinecone_describe_index_stats","pinecone_fetch","pinecone_generate_embeddings","pinecone_list_indexes","pinecone_list_vector_ids","pinecone_search_text","pinecone_search_vector","pinecone_update_vector","pinecone_upsert_text","pipedrive_create_activity","pipedrive_create_deal","pipedrive_create_lead","pipedrive_create_project","pipedrive_delete_lead","pipedrive_get_activities","pipedrive_get_all_deals","pipedrive_get_deal","pipedrive_get_files","pipedrive_get_leads","pipedrive_get_mail_messages","pipedrive_get_mail_thread","pipedrive_get_pipeline_deals","pipedrive_get_pipelines","pipedrive_get_projects","pipedrive_update_activity","pipedrive_update_deal","pipedrive_update_lead","polymarket_get_activity","polymarket_get_event","polymarket_get_events","polymarket_get_holders","polymarket_get_last_trade_price","polymarket_get_leaderboard","polymarket_get_market","polymarket_get_markets","polymarket_get_midpoint","polymarket_get_orderbook","polymarket_get_positions","polymarket_get_price","polymarket_get_price_history","polymarket_get_series","polymarket_get_series_by_id","polymarket_get_spread","polymarket_get_tags","polymarket_get_tick_size","polymarket_get_trades","polymarket_search","postgresql_delete","postgresql_execute","postgresql_insert","postgresql_introspect","postgresql_query","postgresql_update","posthog_batch_events","posthog_capture_event","posthog_create_annotation","posthog_create_cohort","posthog_create_dashboard","posthog_create_experiment","posthog_create_feature_flag","posthog_create_insight","posthog_create_survey","posthog_delete_feature_flag","posthog_delete_person","posthog_delete_survey","posthog_evaluate_flags","posthog_get_cohort","posthog_get_dashboard","posthog_get_event_definition","posthog_get_experiment","posthog_get_feature_flag","posthog_get_insight","posthog_get_organization","posthog_get_person","posthog_get_project","posthog_get_property_definition","posthog_get_session_recording","posthog_get_survey","posthog_list_actions","posthog_list_annotations","posthog_list_cohorts","posthog_list_dashboards","posthog_list_event_definitions","posthog_list_experiments","posthog_list_feature_flags","posthog_list_insights","posthog_list_organizations","posthog_list_persons","posthog_list_projects","posthog_list_property_definitions","posthog_list_recording_playlists","posthog_list_session_recordings","posthog_list_surveys","posthog_query","posthog_update_cohort","posthog_update_event_definition","posthog_update_experiment","posthog_update_feature_flag","posthog_update_insight","posthog_update_property_definition","posthog_update_survey","profound_bot_logs","profound_bots_report","profound_category_assets","profound_category_personas","profound_category_prompts","profound_category_tags","profound_category_topics","profound_citation_prompts","profound_citations_report","profound_list_assets","profound_list_categories","profound_list_domains","profound_list_models","profound_list_optimizations","profound_list_personas","profound_list_regions","profound_optimization_analysis","profound_prompt_answers","profound_prompt_volume","profound_query_fanouts","profound_raw_logs","profound_referrals_report","profound_sentiment_report","profound_visibility_report","prospeo_account_information","prospeo_bulk_enrich_company","prospeo_bulk_enrich_person","prospeo_enrich_company","prospeo_enrich_person","prospeo_search_company","prospeo_search_person","prospeo_search_suggestions","pulse_parser","pulse_parser_v2","qdrant_fetch_points","qdrant_search_vector","qdrant_upsert_points","quartr_get_audio","quartr_get_company","quartr_get_event","quartr_get_event_summary","quartr_get_report","quartr_get_slide_deck","quartr_get_transcript","quartr_list_audio","quartr_list_companies","quartr_list_document_types","quartr_list_documents","quartr_list_event_types","quartr_list_events","quartr_list_live_events","quartr_list_reports","quartr_list_slide_decks","quartr_list_transcripts","quiver_image_to_svg","quiver_list_models","quiver_text_to_svg","railway_create_environment","railway_create_project","railway_create_service","railway_delete_environment","railway_delete_project","railway_delete_service","railway_delete_variable","railway_deploy_service","railway_get_deployment","railway_get_deployment_logs","railway_get_project","railway_list_deployments","railway_list_project_members","railway_list_projects","railway_list_variables","railway_restart_deployment","railway_rollback_deployment","railway_transfer_project","railway_update_project","railway_upsert_variable","rb2b_credit_check","rb2b_email_to_activity","rb2b_hem_to_best_linkedin","rb2b_hem_to_business_profile","rb2b_hem_to_linkedin","rb2b_hem_to_maid","rb2b_ip_to_company","rb2b_ip_to_hem","rb2b_ip_to_maid","rb2b_linkedin_slug_search","rb2b_linkedin_to_best_personal_email","rb2b_linkedin_to_business_profile","rb2b_linkedin_to_hashed_emails","rb2b_linkedin_to_mobile_phone","rb2b_linkedin_to_personal_email","rds_delete","rds_execute","rds_insert","rds_introspect","rds_query","rds_update","reddit_delete","reddit_edit","reddit_get_comments","reddit_get_controversial","reddit_get_info","reddit_get_me","reddit_get_messages","reddit_get_posts","reddit_get_saved","reddit_get_subreddit_info","reddit_get_subreddit_rules","reddit_get_user","reddit_get_user_comments","reddit_get_user_posts","reddit_hide","reddit_hot_posts","reddit_list_my_subreddits","reddit_lock","reddit_mark_all_read","reddit_mark_read","reddit_marknsfw","reddit_mod_approve","reddit_mod_distinguish","reddit_mod_remove","reddit_mod_sticky","reddit_reply","reddit_report","reddit_save","reddit_search","reddit_search_subreddits","reddit_send_message","reddit_submit_post","reddit_subscribe","reddit_unhide","reddit_unlock","reddit_unmarknsfw","reddit_unsave","reddit_vote","redis_command","redis_delete","redis_exists","redis_expire","redis_get","redis_hdel","redis_hget","redis_hgetall","redis_hset","redis_incr","redis_incrby","redis_keys","redis_llen","redis_lpop","redis_lpush","redis_lrange","redis_persist","redis_rpop","redis_rpush","redis_set","redis_setnx","redis_ttl","reducto_parser","reducto_parser_v2","resend_cancel_email","resend_create_audience","resend_create_broadcast","resend_create_contact","resend_delete_audience","resend_delete_contact","resend_get_audience","resend_get_broadcast","resend_get_contact","resend_get_email","resend_list_audiences","resend_list_contacts","resend_list_domains","resend_send","resend_send_broadcast","resend_update_contact","revenuecat_create_purchase","revenuecat_defer_google_subscription","revenuecat_delete_customer","revenuecat_get_customer","revenuecat_grant_entitlement","revenuecat_list_offerings","revenuecat_refund_google_subscription","revenuecat_revoke_entitlement","revenuecat_revoke_google_subscription","revenuecat_update_subscriber_attributes","rippling_bulk_create_custom_object_records","rippling_bulk_delete_custom_object_records","rippling_bulk_update_custom_object_records","rippling_create_business_partner","rippling_create_business_partner_group","rippling_create_custom_app","rippling_create_custom_object","rippling_create_custom_object_field","rippling_create_custom_object_record","rippling_create_custom_page","rippling_create_custom_setting","rippling_create_department","rippling_create_draft_hires","rippling_create_object_category","rippling_create_title","rippling_create_work_location","rippling_delete_business_partner","rippling_delete_business_partner_group","rippling_delete_custom_app","rippling_delete_custom_object","rippling_delete_custom_object_field","rippling_delete_custom_object_record","rippling_delete_custom_page","rippling_delete_custom_setting","rippling_delete_object_category","rippling_delete_title","rippling_delete_work_location","rippling_get_business_partner","rippling_get_business_partner_group","rippling_get_current_user","rippling_get_custom_app","rippling_get_custom_object","rippling_get_custom_object_field","rippling_get_custom_object_record","rippling_get_custom_object_record_by_external_id","rippling_get_custom_page","rippling_get_custom_setting","rippling_get_department","rippling_get_employment_type","rippling_get_job_function","rippling_get_object_category","rippling_get_report_run","rippling_get_supergroup","rippling_get_team","rippling_get_title","rippling_get_user","rippling_get_work_location","rippling_get_worker","rippling_list_business_partner_groups","rippling_list_business_partners","rippling_list_companies","rippling_list_custom_apps","rippling_list_custom_fields","rippling_list_custom_object_fields","rippling_list_custom_object_records","rippling_list_custom_objects","rippling_list_custom_pages","rippling_list_custom_settings","rippling_list_departments","rippling_list_employment_types","rippling_list_entitlements","rippling_list_job_functions","rippling_list_object_categories","rippling_list_supergroup_exclusion_members","rippling_list_supergroup_inclusion_members","rippling_list_supergroup_members","rippling_list_supergroups","rippling_list_teams","rippling_list_titles","rippling_list_users","rippling_list_work_locations","rippling_list_workers","rippling_query_custom_object_records","rippling_trigger_report_run","rippling_update_custom_app","rippling_update_custom_object","rippling_update_custom_object_field","rippling_update_custom_object_record","rippling_update_custom_page","rippling_update_custom_setting","rippling_update_department","rippling_update_object_category","rippling_update_supergroup_exclusion_members","rippling_update_supergroup_inclusion_members","rippling_update_title","rippling_update_work_location","rocketlane_add_field_option","rocketlane_add_project_members","rocketlane_add_task_assignees","rocketlane_add_task_dependencies","rocketlane_add_task_followers","rocketlane_archive_project","rocketlane_assign_placeholders","rocketlane_create_field","rocketlane_create_phase","rocketlane_create_project","rocketlane_create_space","rocketlane_create_space_document","rocketlane_create_task","rocketlane_create_time_entry","rocketlane_create_time_off","rocketlane_delete_field","rocketlane_delete_phase","rocketlane_delete_project","rocketlane_delete_space","rocketlane_delete_space_document","rocketlane_delete_task","rocketlane_delete_time_entry","rocketlane_delete_time_off","rocketlane_get_field","rocketlane_get_invoice","rocketlane_get_invoice_line_items","rocketlane_get_invoice_payments","rocketlane_get_phase","rocketlane_get_project","rocketlane_get_space","rocketlane_get_space_document","rocketlane_get_task","rocketlane_get_time_entry","rocketlane_get_time_off","rocketlane_get_user","rocketlane_import_template","rocketlane_list_fields","rocketlane_list_invoices","rocketlane_list_phases","rocketlane_list_placeholders","rocketlane_list_projects","rocketlane_list_resource_allocations","rocketlane_list_space_documents","rocketlane_list_spaces","rocketlane_list_tasks","rocketlane_list_time_entries","rocketlane_list_time_entry_categories","rocketlane_list_time_offs","rocketlane_list_users","rocketlane_move_task_to_phase","rocketlane_remove_project_members","rocketlane_remove_task_assignees","rocketlane_remove_task_dependencies","rocketlane_remove_task_followers","rocketlane_search_time_entries","rocketlane_unassign_placeholders","rocketlane_update_field","rocketlane_update_field_option","rocketlane_update_phase","rocketlane_update_project","rocketlane_update_space","rocketlane_update_space_document","rocketlane_update_task","rocketlane_update_time_entry","rootly_acknowledge_alert","rootly_add_incident_event","rootly_add_subscribers","rootly_assign_incident_role","rootly_create_action_item","rootly_create_alert","rootly_create_incident","rootly_create_status_page_event","rootly_delete_action_item","rootly_delete_incident","rootly_escalate_alert","rootly_get_alert","rootly_get_incident","rootly_list_action_items","rootly_list_alerts","rootly_list_causes","rootly_list_environments","rootly_list_escalation_policies","rootly_list_functionalities","rootly_list_incident_events","rootly_list_incident_roles","rootly_list_incident_types","rootly_list_incidents","rootly_list_on_calls","rootly_list_playbooks","rootly_list_retrospectives","rootly_list_schedules","rootly_list_services","rootly_list_severities","rootly_list_teams","rootly_list_users","rootly_mitigate_incident","rootly_remove_subscribers","rootly_resolve_alert","rootly_resolve_incident","rootly_run_workflow","rootly_snooze_alert","rootly_unassign_incident_role","rootly_update_action_item","rootly_update_alert","rootly_update_incident","s3_copy_object","s3_create_bucket","s3_delete_bucket","s3_delete_object","s3_delete_objects","s3_get_object","s3_head_object","s3_list_buckets","s3_list_objects","s3_presigned_url","s3_put_object","salesforce_create_account","salesforce_create_case","salesforce_create_contact","salesforce_create_custom_field","salesforce_create_custom_object","salesforce_create_lead","salesforce_create_opportunity","salesforce_create_task","salesforce_delete_account","salesforce_delete_case","salesforce_delete_contact","salesforce_delete_custom_field","salesforce_delete_lead","salesforce_delete_opportunity","salesforce_delete_task","salesforce_describe_object","salesforce_get_accounts","salesforce_get_cases","salesforce_get_contacts","salesforce_get_dashboard","salesforce_get_leads","salesforce_get_opportunities","salesforce_get_report","salesforce_get_tasks","salesforce_list_dashboards","salesforce_list_objects","salesforce_list_report_types","salesforce_list_reports","salesforce_query","salesforce_query_more","salesforce_refresh_dashboard","salesforce_run_report","salesforce_tooling_query","salesforce_update_account","salesforce_update_case","salesforce_update_contact","salesforce_update_custom_field","salesforce_update_lead","salesforce_update_opportunity","salesforce_update_task","sap_concur_approve_expense_report","sap_concur_associate_attendees","sap_concur_create_cash_advance","sap_concur_create_expected_expense","sap_concur_create_expense_report","sap_concur_create_list_item","sap_concur_create_purchase_request","sap_concur_create_quick_expense","sap_concur_create_quick_expense_with_image","sap_concur_create_report_comment","sap_concur_create_travel_request","sap_concur_create_user","sap_concur_delete_expected_expense","sap_concur_delete_expense","sap_concur_delete_expense_report","sap_concur_delete_list_item","sap_concur_delete_travel_request","sap_concur_delete_user","sap_concur_get_allocation","sap_concur_get_budget","sap_concur_get_cash_advance","sap_concur_get_expected_expense","sap_concur_get_expense","sap_concur_get_expense_report","sap_concur_get_itemizations","sap_concur_get_itinerary","sap_concur_get_list","sap_concur_get_list_item","sap_concur_get_purchase_request","sap_concur_get_receipt","sap_concur_get_receipt_status","sap_concur_get_request_cash_advance","sap_concur_get_travel_profile","sap_concur_get_travel_request","sap_concur_get_user","sap_concur_issue_cash_advance","sap_concur_list_allocations","sap_concur_list_attendee_associations","sap_concur_list_budget_categories","sap_concur_list_budgets","sap_concur_list_exceptions","sap_concur_list_expected_expenses","sap_concur_list_expense_reports","sap_concur_list_expenses","sap_concur_list_itineraries","sap_concur_list_list_items","sap_concur_list_lists","sap_concur_list_receipts","sap_concur_list_report_comments","sap_concur_list_reports_to_approve","sap_concur_list_travel_profiles_summary","sap_concur_list_travel_request_comments","sap_concur_list_travel_requests","sap_concur_list_users","sap_concur_move_travel_request","sap_concur_recall_expense_report","sap_concur_remove_all_attendees","sap_concur_search_locations","sap_concur_search_users","sap_concur_send_back_expense_report","sap_concur_submit_expense_report","sap_concur_update_allocation","sap_concur_update_expected_expense","sap_concur_update_expense","sap_concur_update_expense_report","sap_concur_update_list_item","sap_concur_update_travel_request","sap_concur_update_user","sap_concur_upload_exchange_rates","sap_concur_upload_receipt_image","sap_s4hana_create_business_partner","sap_s4hana_create_purchase_order","sap_s4hana_create_purchase_requisition","sap_s4hana_create_sales_order","sap_s4hana_delete_sales_order","sap_s4hana_get_billing_document","sap_s4hana_get_business_partner","sap_s4hana_get_customer","sap_s4hana_get_inbound_delivery","sap_s4hana_get_material_document","sap_s4hana_get_outbound_delivery","sap_s4hana_get_product","sap_s4hana_get_purchase_order","sap_s4hana_get_purchase_requisition","sap_s4hana_get_sales_order","sap_s4hana_get_supplier","sap_s4hana_get_supplier_invoice","sap_s4hana_list_billing_documents","sap_s4hana_list_business_partners","sap_s4hana_list_customers","sap_s4hana_list_inbound_deliveries","sap_s4hana_list_material_documents","sap_s4hana_list_material_stock","sap_s4hana_list_outbound_deliveries","sap_s4hana_list_products","sap_s4hana_list_purchase_orders","sap_s4hana_list_purchase_requisitions","sap_s4hana_list_sales_orders","sap_s4hana_list_supplier_invoices","sap_s4hana_list_suppliers","sap_s4hana_odata_query","sap_s4hana_update_business_partner","sap_s4hana_update_customer","sap_s4hana_update_product","sap_s4hana_update_purchase_order","sap_s4hana_update_purchase_requisition","sap_s4hana_update_sales_order","sap_s4hana_update_supplier","search_tool","secrets_manager_create_secret","secrets_manager_delete_secret","secrets_manager_describe_secret","secrets_manager_get_secret","secrets_manager_list_secrets","secrets_manager_restore_secret","secrets_manager_rotate_secret","secrets_manager_tag_resource","secrets_manager_untag_resource","secrets_manager_update_secret","sendblue_evaluate_service","sendblue_get_message","sendblue_send_group_message","sendblue_send_message","sendblue_send_typing_indicator","sendgrid_add_contact","sendgrid_add_contacts_to_list","sendgrid_create_list","sendgrid_create_template","sendgrid_create_template_version","sendgrid_delete_contacts","sendgrid_delete_list","sendgrid_delete_template","sendgrid_get_contact","sendgrid_get_list","sendgrid_get_template","sendgrid_list_all_lists","sendgrid_list_templates","sendgrid_remove_contacts_from_list","sendgrid_search_contacts","sendgrid_send_mail","sentry_events_get","sentry_events_list","sentry_issues_get","sentry_issues_list","sentry_issues_update","sentry_projects_create","sentry_projects_get","sentry_projects_list","sentry_projects_update","sentry_releases_create","sentry_releases_deploy","sentry_releases_list","sentry_teams_list","serper_search","servicenow_aggregate","servicenow_create_record","servicenow_delete_record","servicenow_download_attachment","servicenow_list_attachments","servicenow_read_record","servicenow_update_record","servicenow_upload_attachment","ses_create_configuration_set","ses_create_email_identity","ses_create_template","ses_delete_email_identity","ses_delete_suppressed_destination","ses_delete_template","ses_get_account","ses_get_email_identity","ses_get_suppressed_destination","ses_get_template","ses_list_identities","ses_list_suppressed_destinations","ses_list_templates","ses_put_suppressed_destination","ses_send_bulk_email","ses_send_custom_verification_email","ses_send_email","ses_send_templated_email","ses_update_template","sftp_delete","sftp_download","sftp_list","sftp_mkdir","sftp_upload","sharepoint_add_list_items","sharepoint_create_list","sharepoint_create_page","sharepoint_delete_file","sharepoint_delete_list_item","sharepoint_delete_page","sharepoint_download_file","sharepoint_get_drive_item","sharepoint_get_list","sharepoint_get_list_item","sharepoint_list_sites","sharepoint_publish_page","sharepoint_read_page","sharepoint_update_list","sharepoint_update_page","sharepoint_upload_file","shopify_adjust_inventory","shopify_cancel_order","shopify_create_customer","shopify_create_fulfillment","shopify_create_product","shopify_delete_customer","shopify_delete_product","shopify_get_collection","shopify_get_customer","shopify_get_inventory_level","shopify_get_order","shopify_get_product","shopify_list_collections","shopify_list_customers","shopify_list_inventory_items","shopify_list_locations","shopify_list_orders","shopify_list_products","shopify_update_customer","shopify_update_order","shopify_update_product","similarweb_bounce_rate","similarweb_page_views","similarweb_pages_per_visit","similarweb_traffic_visits","similarweb_visit_duration","similarweb_website_overview","sixtyfour_enrich_company","sixtyfour_enrich_lead","sixtyfour_find_email","sixtyfour_find_phone","slack_add_reaction","slack_archive_conversation","slack_canvas","slack_create_channel_canvas","slack_create_conversation","slack_delete_canvas","slack_delete_message","slack_delete_scheduled_message","slack_download","slack_edit_canvas","slack_ephemeral_message","slack_get_canvas","slack_get_channel_history","slack_get_channel_info","slack_get_message","slack_get_permalink","slack_get_thread","slack_get_thread_replies","slack_get_user","slack_get_user_presence","slack_invite_to_conversation","slack_list_canvases","slack_list_channels","slack_list_members","slack_list_scheduled_messages","slack_list_users","slack_lookup_canvas_sections","slack_message","slack_message_reader","slack_open_view","slack_publish_view","slack_push_view","slack_remove_reaction","slack_rename_conversation","slack_schedule_message","slack_set_conversation_purpose","slack_set_conversation_topic","slack_set_status","slack_set_suggested_prompts","slack_set_title","slack_update_message","slack_update_view","smartlead_add_email_accounts_to_campaign","smartlead_add_leads_to_campaign","smartlead_create_campaign","smartlead_create_lead_list","smartlead_delete_campaign","smartlead_delete_campaign_webhook","smartlead_delete_lead_from_campaign","smartlead_delete_lead_list","smartlead_duplicate_campaign","smartlead_export_campaign_leads","smartlead_get_campaign","smartlead_get_campaign_analytics","smartlead_get_campaign_analytics_by_date","smartlead_get_campaign_lead_statistics","smartlead_get_campaign_mailbox_statistics","smartlead_get_campaign_sequences","smartlead_get_campaign_statistics","smartlead_get_campaign_top_level_analytics_by_date","smartlead_get_campaign_webhook_summary","smartlead_get_lead_by_email","smartlead_get_lead_by_id","smartlead_get_lead_list","smartlead_get_lead_message_history","smartlead_list_campaign_email_accounts","smartlead_list_campaign_leads","smartlead_list_campaign_webhooks","smartlead_list_campaigns","smartlead_list_clients","smartlead_list_email_accounts","smartlead_list_inbox_replies","smartlead_list_lead_activities","smartlead_list_lead_categories","smartlead_list_lead_lists","smartlead_mark_lead_complete","smartlead_pause_lead","smartlead_remove_email_accounts_from_campaign","smartlead_resume_lead","smartlead_save_campaign_sequences","smartlead_unsubscribe_lead_from_campaign","smartlead_unsubscribe_lead_globally","smartlead_update_campaign_schedule","smartlead_update_campaign_settings","smartlead_update_campaign_status","smartlead_update_lead","smartlead_update_lead_category","smartlead_update_lead_list","smartlead_upsert_campaign_webhook","sms_send","smtp_send_mail","snowflake_alter_warehouse","snowflake_call_procedure","snowflake_cancel_statement","snowflake_cancel_task_run","snowflake_delete_rows","snowflake_execute_sql","snowflake_get_statement","snowflake_get_task","snowflake_get_task_run","snowflake_get_task_run_output","snowflake_get_warehouse","snowflake_insert_rows","snowflake_introspect_schema","snowflake_list_copy_history","snowflake_list_databases","snowflake_list_query_history","snowflake_list_schemas","snowflake_list_tables","snowflake_list_task_runs","snowflake_list_tasks","snowflake_list_warehouses","snowflake_load_data","snowflake_resume_task","snowflake_resume_warehouse","snowflake_run_task","snowflake_suspend_task","snowflake_suspend_warehouse","snowflake_unload_data","snowflake_update_rows","snowflake_upsert_rows","sportmonks_core_get_cities","sportmonks_core_get_city","sportmonks_core_get_continent","sportmonks_core_get_continents","sportmonks_core_get_countries","sportmonks_core_get_country","sportmonks_core_get_entity_filters","sportmonks_core_get_my_usage","sportmonks_core_get_region","sportmonks_core_get_regions","sportmonks_core_get_timezones","sportmonks_core_get_type","sportmonks_core_get_type_by_entity","sportmonks_core_get_types","sportmonks_core_search_cities","sportmonks_core_search_countries","sportmonks_core_search_regions","sportmonks_football_expected_by_player","sportmonks_football_expected_by_team","sportmonks_football_get_all_commentaries","sportmonks_football_get_all_fixtures","sportmonks_football_get_all_players","sportmonks_football_get_all_rivals","sportmonks_football_get_all_teams","sportmonks_football_get_all_transfer_rumours","sportmonks_football_get_all_transfers","sportmonks_football_get_brackets_by_season","sportmonks_football_get_coach","sportmonks_football_get_coaches","sportmonks_football_get_coaches_by_country","sportmonks_football_get_commentaries_by_fixture","sportmonks_football_get_current_leagues_by_team","sportmonks_football_get_expected_lineups_by_player","sportmonks_football_get_expected_lineups_by_team","sportmonks_football_get_extended_team_squad","sportmonks_football_get_fixture","sportmonks_football_get_fixtures_by_date","sportmonks_football_get_fixtures_by_date_range","sportmonks_football_get_fixtures_by_date_range_for_team","sportmonks_football_get_fixtures_by_ids","sportmonks_football_get_grouped_standings_by_round","sportmonks_football_get_head_to_head","sportmonks_football_get_inplay_livescores","sportmonks_football_get_latest_coaches","sportmonks_football_get_latest_fixtures","sportmonks_football_get_latest_livescores","sportmonks_football_get_latest_players","sportmonks_football_get_latest_totw","sportmonks_football_get_latest_transfers","sportmonks_football_get_league","sportmonks_football_get_leagues","sportmonks_football_get_leagues_by_country","sportmonks_football_get_leagues_by_date","sportmonks_football_get_leagues_by_team","sportmonks_football_get_live_leagues","sportmonks_football_get_live_probabilities","sportmonks_football_get_live_probabilities_by_fixture","sportmonks_football_get_live_standings_by_league","sportmonks_football_get_livescores","sportmonks_football_get_match_facts","sportmonks_football_get_match_facts_by_date_range","sportmonks_football_get_match_facts_by_fixture","sportmonks_football_get_match_facts_by_league","sportmonks_football_get_past_fixtures_by_tv_station","sportmonks_football_get_player","sportmonks_football_get_players_by_country","sportmonks_football_get_postmatch_news","sportmonks_football_get_postmatch_news_by_season","sportmonks_football_get_predictability_by_league","sportmonks_football_get_prematch_news","sportmonks_football_get_prematch_news_by_season","sportmonks_football_get_prematch_news_upcoming","sportmonks_football_get_probabilities","sportmonks_football_get_probabilities_by_fixture","sportmonks_football_get_referee","sportmonks_football_get_referees","sportmonks_football_get_referees_by_country","sportmonks_football_get_referees_by_season","sportmonks_football_get_rivals_by_team","sportmonks_football_get_round","sportmonks_football_get_round_statistics","sportmonks_football_get_rounds","sportmonks_football_get_rounds_by_season","sportmonks_football_get_schedules_by_season","sportmonks_football_get_schedules_by_season_and_team","sportmonks_football_get_schedules_by_team","sportmonks_football_get_season","sportmonks_football_get_seasons","sportmonks_football_get_seasons_by_team","sportmonks_football_get_stage","sportmonks_football_get_stage_statistics","sportmonks_football_get_stages","sportmonks_football_get_stages_by_season","sportmonks_football_get_standing_corrections_by_season","sportmonks_football_get_standings","sportmonks_football_get_standings_by_round","sportmonks_football_get_standings_by_season","sportmonks_football_get_state","sportmonks_football_get_states","sportmonks_football_get_team","sportmonks_football_get_team_rankings","sportmonks_football_get_team_rankings_by_date","sportmonks_football_get_team_rankings_by_team","sportmonks_football_get_team_squad","sportmonks_football_get_team_squad_by_season","sportmonks_football_get_teams_by_country","sportmonks_football_get_teams_by_season","sportmonks_football_get_topscorers_by_season","sportmonks_football_get_topscorers_by_stage","sportmonks_football_get_totw","sportmonks_football_get_totw_by_round","sportmonks_football_get_transfer","sportmonks_football_get_transfer_rumour","sportmonks_football_get_transfer_rumours_between_dates","sportmonks_football_get_transfer_rumours_by_player","sportmonks_football_get_transfer_rumours_by_team","sportmonks_football_get_transfers_between_dates","sportmonks_football_get_transfers_by_player","sportmonks_football_get_transfers_by_team","sportmonks_football_get_tv_station","sportmonks_football_get_tv_stations","sportmonks_football_get_tv_stations_by_fixture","sportmonks_football_get_upcoming_fixtures_by_market","sportmonks_football_get_upcoming_fixtures_by_tv_station","sportmonks_football_get_value_bets","sportmonks_football_get_value_bets_by_fixture","sportmonks_football_get_venue","sportmonks_football_get_venues","sportmonks_football_get_venues_by_season","sportmonks_football_search_coaches","sportmonks_football_search_fixtures","sportmonks_football_search_leagues","sportmonks_football_search_players","sportmonks_football_search_referees","sportmonks_football_search_rounds","sportmonks_football_search_seasons","sportmonks_football_search_stages","sportmonks_football_search_teams","sportmonks_football_search_venues","sportmonks_motorsport_get_all_fixtures","sportmonks_motorsport_get_current_leagues_by_team","sportmonks_motorsport_get_driver","sportmonks_motorsport_get_driver_standings","sportmonks_motorsport_get_driver_standings_by_season","sportmonks_motorsport_get_drivers","sportmonks_motorsport_get_drivers_by_country","sportmonks_motorsport_get_drivers_by_season","sportmonks_motorsport_get_fixture","sportmonks_motorsport_get_fixtures_by_date","sportmonks_motorsport_get_fixtures_by_date_range","sportmonks_motorsport_get_fixtures_by_ids","sportmonks_motorsport_get_laps_by_fixture","sportmonks_motorsport_get_laps_by_fixture_and_driver","sportmonks_motorsport_get_laps_by_fixture_and_lap","sportmonks_motorsport_get_latest_laps_by_fixture","sportmonks_motorsport_get_latest_pitstops_by_fixture","sportmonks_motorsport_get_latest_stints_by_fixture","sportmonks_motorsport_get_latest_updated_drivers","sportmonks_motorsport_get_latest_updated_fixtures","sportmonks_motorsport_get_league","sportmonks_motorsport_get_leagues","sportmonks_motorsport_get_leagues_by_country","sportmonks_motorsport_get_leagues_by_date","sportmonks_motorsport_get_leagues_by_live","sportmonks_motorsport_get_leagues_by_team","sportmonks_motorsport_get_livescores","sportmonks_motorsport_get_pitstops_by_fixture","sportmonks_motorsport_get_pitstops_by_fixture_and_driver","sportmonks_motorsport_get_pitstops_by_fixture_and_lap","sportmonks_motorsport_get_race_results_by_season_and_driver","sportmonks_motorsport_get_race_results_by_season_and_team","sportmonks_motorsport_get_schedules_by_season","sportmonks_motorsport_get_season","sportmonks_motorsport_get_seasons","sportmonks_motorsport_get_stage","sportmonks_motorsport_get_stages","sportmonks_motorsport_get_stages_by_season","sportmonks_motorsport_get_state","sportmonks_motorsport_get_states","sportmonks_motorsport_get_stints_by_fixture","sportmonks_motorsport_get_stints_by_fixture_and_driver","sportmonks_motorsport_get_stints_by_fixture_and_stint","sportmonks_motorsport_get_team","sportmonks_motorsport_get_team_standings","sportmonks_motorsport_get_team_standings_by_season","sportmonks_motorsport_get_teams","sportmonks_motorsport_get_teams_by_country","sportmonks_motorsport_get_teams_by_season","sportmonks_motorsport_get_venue","sportmonks_motorsport_get_venues","sportmonks_motorsport_get_venues_by_season","sportmonks_motorsport_search_drivers","sportmonks_motorsport_search_leagues","sportmonks_motorsport_search_stages","sportmonks_motorsport_search_teams","sportmonks_motorsport_search_venues","sportmonks_odds_get_all_historical_odds","sportmonks_odds_get_all_inplay_odds","sportmonks_odds_get_all_pre_match_odds","sportmonks_odds_get_all_premium_odds","sportmonks_odds_get_bookmaker","sportmonks_odds_get_bookmaker_event_ids_by_fixture","sportmonks_odds_get_bookmakers","sportmonks_odds_get_bookmakers_by_fixture","sportmonks_odds_get_inplay_odds_by_fixture","sportmonks_odds_get_inplay_odds_by_fixture_and_bookmaker","sportmonks_odds_get_inplay_odds_by_fixture_and_market","sportmonks_odds_get_last_updated_inplay_odds","sportmonks_odds_get_last_updated_pre_match_odds","sportmonks_odds_get_market","sportmonks_odds_get_markets","sportmonks_odds_get_pre_match_odds_by_fixture","sportmonks_odds_get_pre_match_odds_by_fixture_and_bookmaker","sportmonks_odds_get_pre_match_odds_by_fixture_and_market","sportmonks_odds_get_premium_odds_by_fixture","sportmonks_odds_get_premium_odds_by_fixture_and_bookmaker","sportmonks_odds_get_premium_odds_by_fixture_and_market","sportmonks_odds_get_updated_historical_odds_between","sportmonks_odds_get_updated_premium_odds_between","sportmonks_odds_search_bookmakers","sportmonks_odds_search_markets","spotify_add_playlist_cover","spotify_add_to_queue","spotify_add_tracks_to_playlist","spotify_check_following","spotify_check_playlist_followers","spotify_check_saved_albums","spotify_check_saved_audiobooks","spotify_check_saved_episodes","spotify_check_saved_shows","spotify_check_saved_tracks","spotify_create_playlist","spotify_follow_artists","spotify_follow_playlist","spotify_get_album","spotify_get_album_tracks","spotify_get_albums","spotify_get_artist","spotify_get_artist_albums","spotify_get_artist_top_tracks","spotify_get_artists","spotify_get_audiobook","spotify_get_audiobook_chapters","spotify_get_audiobooks","spotify_get_categories","spotify_get_current_user","spotify_get_currently_playing","spotify_get_devices","spotify_get_episode","spotify_get_episodes","spotify_get_followed_artists","spotify_get_markets","spotify_get_new_releases","spotify_get_playback_state","spotify_get_playlist","spotify_get_playlist_cover","spotify_get_playlist_tracks","spotify_get_queue","spotify_get_recently_played","spotify_get_saved_albums","spotify_get_saved_audiobooks","spotify_get_saved_episodes","spotify_get_saved_shows","spotify_get_saved_tracks","spotify_get_show","spotify_get_show_episodes","spotify_get_shows","spotify_get_top_artists","spotify_get_top_tracks","spotify_get_track","spotify_get_tracks","spotify_get_user_playlists","spotify_get_user_profile","spotify_pause","spotify_play","spotify_remove_saved_albums","spotify_remove_saved_audiobooks","spotify_remove_saved_episodes","spotify_remove_saved_shows","spotify_remove_saved_tracks","spotify_remove_tracks_from_playlist","spotify_reorder_playlist_items","spotify_replace_playlist_items","spotify_save_albums","spotify_save_audiobooks","spotify_save_episodes","spotify_save_shows","spotify_save_tracks","spotify_search","spotify_seek","spotify_set_repeat","spotify_set_shuffle","spotify_set_volume","spotify_skip_next","spotify_skip_previous","spotify_transfer_playback","spotify_unfollow_artists","spotify_unfollow_playlist","spotify_update_playlist","sqs_send","square_batch_retrieve_inventory_counts","square_cancel_invoice","square_cancel_payment","square_complete_payment","square_create_catalog_image","square_create_customer","square_create_invoice","square_create_order","square_create_payment","square_delete_catalog_object","square_delete_customer","square_delete_invoice","square_get_catalog_object","square_get_customer","square_get_invoice","square_get_location","square_get_order","square_get_payment","square_get_refund","square_list_catalog","square_list_customers","square_list_invoices","square_list_locations","square_list_payments","square_list_refunds","square_pay_order","square_publish_invoice","square_refund_payment","square_search_catalog_objects","square_search_customers","square_search_invoices","square_search_orders","square_update_customer","square_upsert_catalog_object","ssh_check_command_exists","ssh_check_file_exists","ssh_create_directory","ssh_delete_file","ssh_download_file","ssh_execute_command","ssh_execute_script","ssh_get_system_info","ssh_list_directory","ssh_move_rename","ssh_read_file_content","ssh_upload_file","ssh_write_file_content","stagehand_agent","stagehand_extract","stripe_cancel_payment_intent","stripe_cancel_subscription","stripe_capture_charge","stripe_capture_payment_intent","stripe_confirm_payment_intent","stripe_create_charge","stripe_create_customer","stripe_create_invoice","stripe_create_payment_intent","stripe_create_price","stripe_create_product","stripe_create_subscription","stripe_delete_customer","stripe_delete_invoice","stripe_delete_product","stripe_finalize_invoice","stripe_list_charges","stripe_list_customers","stripe_list_events","stripe_list_invoices","stripe_list_payment_intents","stripe_list_prices","stripe_list_products","stripe_list_subscriptions","stripe_pay_invoice","stripe_resume_subscription","stripe_retrieve_charge","stripe_retrieve_customer","stripe_retrieve_event","stripe_retrieve_invoice","stripe_retrieve_payment_intent","stripe_retrieve_price","stripe_retrieve_product","stripe_retrieve_subscription","stripe_search_charges","stripe_search_customers","stripe_search_invoices","stripe_search_payment_intents","stripe_search_prices","stripe_search_products","stripe_search_subscriptions","stripe_send_invoice","stripe_update_charge","stripe_update_customer","stripe_update_invoice","stripe_update_payment_intent","stripe_update_price","stripe_update_product","stripe_update_subscription","stripe_void_invoice","sts_assume_role","sts_assume_role_with_saml","sts_assume_role_with_web_identity","sts_get_access_key_info","sts_get_caller_identity","sts_get_session_token","stt_assemblyai","stt_assemblyai_v2","stt_deepgram","stt_deepgram_v2","stt_elevenlabs","stt_elevenlabs_v2","stt_gemini","stt_gemini_v2","stt_whisper","stt_whisper_v2","supabase_count","supabase_delete","supabase_get_row","supabase_insert","supabase_introspect","supabase_invoke_function","supabase_query","supabase_rpc","supabase_storage_copy","supabase_storage_create_bucket","supabase_storage_create_signed_upload_url","supabase_storage_create_signed_url","supabase_storage_delete","supabase_storage_delete_bucket","supabase_storage_download","supabase_storage_empty_bucket","supabase_storage_get_public_url","supabase_storage_list","supabase_storage_list_buckets","supabase_storage_move","supabase_storage_update_bucket","supabase_storage_upload","supabase_text_search","supabase_update","supabase_upsert","supabase_vector_search","table_batch_insert_rows","table_create","table_delete_row","table_delete_rows_by_filter","table_get_row","table_get_schema","table_insert_row","table_list","table_query_rows","table_query_rows_v2","table_update_row","table_update_rows_by_filter","table_upsert_row","tailscale_authorize_device","tailscale_create_auth_key","tailscale_delete_auth_key","tailscale_delete_device","tailscale_delete_user","tailscale_expire_device_key","tailscale_get_acl","tailscale_get_auth_key","tailscale_get_device","tailscale_get_device_routes","tailscale_get_dns_preferences","tailscale_get_dns_searchpaths","tailscale_list_auth_keys","tailscale_list_devices","tailscale_list_dns_nameservers","tailscale_list_users","tailscale_set_acl","tailscale_set_device_routes","tailscale_set_device_tags","tailscale_set_dns_nameservers","tailscale_set_dns_preferences","tailscale_set_dns_searchpaths","tailscale_suspend_user","tailscale_update_device_key","tavily_crawl","tavily_extract","tavily_map","tavily_search","telegram_copy_message","telegram_delete_message","telegram_edit_message_text","telegram_forward_message","telegram_get_chat","telegram_get_chat_member","telegram_message","telegram_pin_message","telegram_send_animation","telegram_send_audio","telegram_send_chat_action","telegram_send_contact","telegram_send_document","telegram_send_location","telegram_send_photo","telegram_send_poll","telegram_send_video","telegram_set_message_reaction","telegram_unpin_message","temporal_cancel_workflow","temporal_count_workflows","temporal_create_schedule","temporal_delete_schedule","temporal_describe_schedule","temporal_describe_task_queue","temporal_describe_workflow","temporal_get_workflow_history","temporal_list_schedules","temporal_list_workflows","temporal_pause_schedule","temporal_query_workflow","temporal_reset_workflow","temporal_signal_with_start","temporal_signal_workflow","temporal_start_workflow","temporal_terminate_workflow","temporal_trigger_schedule","temporal_unpause_schedule","temporal_update_workflow","textract_analyze_expense","textract_analyze_id","textract_parser","textract_parser_v2","thinking_tool","thrive_add_audience_managers","thrive_add_audience_members","thrive_add_user_tags","thrive_create_assignment","thrive_create_audience","thrive_create_completion","thrive_create_user","thrive_delete_assignment","thrive_delete_audience","thrive_delete_user","thrive_get_activity","thrive_get_assignment","thrive_get_audience","thrive_get_completion","thrive_get_content","thrive_get_cpd_category","thrive_get_cpd_entry","thrive_get_cpd_requirement","thrive_get_enrolment","thrive_get_skill_levels","thrive_get_tag","thrive_get_user_by_id","thrive_get_user_by_ref","thrive_list_assignments","thrive_list_audience_managers","thrive_list_audience_members","thrive_list_audiences","thrive_list_completions","thrive_list_enrolments","thrive_list_tags","thrive_query_activities","thrive_query_content","thrive_query_cpd_categories","thrive_query_cpd_entries","thrive_query_cpd_requirements","thrive_query_cpd_user_summaries","thrive_remove_audience_manager","thrive_remove_audience_member","thrive_remove_user_tags","thrive_replace_audience_managers","thrive_replace_audience_members","thrive_search_users","thrive_suspend_user","thrive_update_assignment","thrive_update_audience","thrive_update_user","thrive_update_user_skills","tiktok_get_post_status","tiktok_get_user","tiktok_list_videos","tiktok_query_videos","tiktok_upload_video_draft","tinybird_append_datasource","tinybird_delete_datasource_rows","tinybird_events","tinybird_get_job","tinybird_query","tinybird_query_pipe","tinybird_truncate_datasource","trello_add_checklist","trello_add_checklist_item","trello_add_comment","trello_add_label","trello_add_member","trello_create_board","trello_create_card","trello_create_list","trello_delete_card","trello_get_actions","trello_get_board","trello_get_card","trello_list_cards","trello_list_lists","trello_list_members","trello_remove_label","trello_remove_member","trello_search","trello_update_card","trello_update_checklist_item","trello_update_list","trigger_dev_activate_schedule","trigger_dev_add_run_tags","trigger_dev_batch_trigger_task","trigger_dev_cancel_run","trigger_dev_complete_waitpoint_token","trigger_dev_create_env_var","trigger_dev_create_schedule","trigger_dev_create_waitpoint_token","trigger_dev_deactivate_schedule","trigger_dev_delete_env_var","trigger_dev_delete_schedule","trigger_dev_execute_query","trigger_dev_get_batch","trigger_dev_get_batch_results","trigger_dev_get_deployment","trigger_dev_get_env_var","trigger_dev_get_latest_deployment","trigger_dev_get_query_schema","trigger_dev_get_queue","trigger_dev_get_run","trigger_dev_get_run_events","trigger_dev_get_run_result","trigger_dev_get_run_trace","trigger_dev_get_schedule","trigger_dev_get_waitpoint_token","trigger_dev_import_env_vars","trigger_dev_list_deployments","trigger_dev_list_env_vars","trigger_dev_list_queues","trigger_dev_list_runs","trigger_dev_list_schedules","trigger_dev_list_timezones","trigger_dev_list_waitpoint_tokens","trigger_dev_override_queue_concurrency","trigger_dev_pause_queue","trigger_dev_promote_deployment","trigger_dev_replay_run","trigger_dev_reschedule_run","trigger_dev_reset_queue_concurrency","trigger_dev_resume_queue","trigger_dev_trigger_task","trigger_dev_update_env_var","trigger_dev_update_run_metadata","trigger_dev_update_schedule","tts_azure","tts_cartesia","tts_deepgram","tts_elevenlabs","tts_google","tts_openai","tts_playht","twilio_send_sms","twilio_voice_get_recording","twilio_voice_list_calls","twilio_voice_make_call","typeform_create_form","typeform_delete_form","typeform_files","typeform_get_form","typeform_insights","typeform_list_forms","typeform_responses","typeform_update_form","upstash_redis_command","upstash_redis_delete","upstash_redis_exists","upstash_redis_expire","upstash_redis_get","upstash_redis_hget","upstash_redis_hgetall","upstash_redis_hset","upstash_redis_incr","upstash_redis_incrby","upstash_redis_keys","upstash_redis_lpush","upstash_redis_lrange","upstash_redis_set","upstash_redis_setnx","upstash_redis_ttl","uptimerobot_create_alert_contact","uptimerobot_create_maintenance_window","uptimerobot_create_monitor","uptimerobot_create_psp","uptimerobot_delete_alert_contact","uptimerobot_delete_maintenance_window","uptimerobot_delete_monitor","uptimerobot_delete_psp","uptimerobot_get_account","uptimerobot_get_alert_contact","uptimerobot_get_incident","uptimerobot_get_maintenance_window","uptimerobot_get_monitor","uptimerobot_get_psp","uptimerobot_list_alert_contacts","uptimerobot_list_incidents","uptimerobot_list_maintenance_windows","uptimerobot_list_monitors","uptimerobot_list_psps","uptimerobot_pause_monitor","uptimerobot_start_monitor","uptimerobot_update_maintenance_window","uptimerobot_update_monitor","uptimerobot_update_psp","vanta_download_document_file","vanta_get_control","vanta_get_document","vanta_get_framework","vanta_get_person","vanta_get_policy","vanta_get_risk_scenario","vanta_get_test","vanta_get_vendor","vanta_get_vulnerable_asset","vanta_list_control_documents","vanta_list_control_tests","vanta_list_controls","vanta_list_document_uploads","vanta_list_documents","vanta_list_framework_controls","vanta_list_frameworks","vanta_list_monitored_computers","vanta_list_people","vanta_list_policies","vanta_list_risk_scenarios","vanta_list_test_entities","vanta_list_tests","vanta_list_vendors","vanta_list_vulnerabilities","vanta_list_vulnerability_remediations","vanta_list_vulnerable_assets","vanta_submit_document","vanta_upload_document_file","vercel_add_domain","vercel_add_project_domain","vercel_cancel_deployment","vercel_create_alias","vercel_create_check","vercel_create_deployment","vercel_create_dns_record","vercel_create_edge_config","vercel_create_env_var","vercel_create_project","vercel_create_webhook","vercel_delete_alias","vercel_delete_deployment","vercel_delete_dns_record","vercel_delete_domain","vercel_delete_edge_config","vercel_delete_env_var","vercel_delete_project","vercel_delete_webhook","vercel_get_alias","vercel_get_check","vercel_get_deployment","vercel_get_deployment_events","vercel_get_domain","vercel_get_domain_config","vercel_get_edge_config","vercel_get_edge_config_items","vercel_get_env_vars","vercel_get_project","vercel_get_team","vercel_get_user","vercel_get_webhook","vercel_list_aliases","vercel_list_checks","vercel_list_deployment_files","vercel_list_deployments","vercel_list_dns_records","vercel_list_domains","vercel_list_edge_configs","vercel_list_project_domains","vercel_list_projects","vercel_list_team_members","vercel_list_teams","vercel_list_webhooks","vercel_pause_project","vercel_promote_deployment","vercel_remove_project_domain","vercel_rerequest_check","vercel_unpause_project","vercel_update_check","vercel_update_dns_record","vercel_update_edge_config_items","vercel_update_env_var","vercel_update_project","vercel_update_project_domain","vercel_verify_project_domain","video_falai","video_luma","video_minimax","video_runway","video_veo","vision_tool","vision_tool_v2","wealthbox_read_contact","wealthbox_read_note","wealthbox_read_task","wealthbox_write_contact","wealthbox_write_note","wealthbox_write_task","webflow_create_item","webflow_delete_item","webflow_get_item","webflow_list_items","webflow_update_item","webhook_request","whatsapp_get_media","whatsapp_mark_read","whatsapp_send_interactive","whatsapp_send_media","whatsapp_send_message","whatsapp_send_reaction","whatsapp_send_template","whatsapp_upload_media","wikipedia_content","wikipedia_random","wikipedia_search","wikipedia_summary","wiza_company_enrichment","wiza_get_credits","wiza_individual_reveal","wiza_prospect_search","wordpress_create_category","wordpress_create_comment","wordpress_create_page","wordpress_create_post","wordpress_create_tag","wordpress_delete_category","wordpress_delete_comment","wordpress_delete_media","wordpress_delete_page","wordpress_delete_post","wordpress_delete_tag","wordpress_get_category","wordpress_get_current_user","wordpress_get_media","wordpress_get_page","wordpress_get_post","wordpress_get_tag","wordpress_get_user","wordpress_list_categories","wordpress_list_comments","wordpress_list_media","wordpress_list_pages","wordpress_list_posts","wordpress_list_tags","wordpress_list_users","wordpress_search_content","wordpress_update_category","wordpress_update_comment","wordpress_update_page","wordpress_update_post","wordpress_update_tag","wordpress_upload_media","workday_assign_onboarding","workday_change_job","workday_create_prehire","workday_get_compensation","workday_get_organizations","workday_get_worker","workday_hire_employee","workday_list_workers","workday_terminate_worker","workday_update_worker","workflow_executor","x_create_bookmark","x_create_tweet","x_delete_bookmark","x_delete_tweet","x_get_blocking","x_get_bookmarks","x_get_followers","x_get_following","x_get_liked_tweets","x_get_liking_users","x_get_me","x_get_personalized_trends","x_get_quote_tweets","x_get_retweeted_by","x_get_trends_by_woeid","x_get_tweets_by_ids","x_get_usage","x_get_user_mentions","x_get_user_timeline","x_get_user_tweets","x_hide_reply","x_manage_block","x_manage_follow","x_manage_like","x_manage_mute","x_manage_retweet","x_read","x_search","x_search_tweets","x_search_users","x_user","x_write","youtube_channel_info","youtube_channel_playlists","youtube_channel_videos","youtube_comments","youtube_playlist_items","youtube_search","youtube_trending","youtube_video_categories","youtube_video_details","zendesk_autocomplete_organizations","zendesk_create_organization","zendesk_create_organizations_bulk","zendesk_create_ticket","zendesk_create_tickets_bulk","zendesk_create_user","zendesk_create_users_bulk","zendesk_delete_organization","zendesk_delete_ticket","zendesk_delete_user","zendesk_get_current_user","zendesk_get_organization","zendesk_get_organizations","zendesk_get_ticket","zendesk_get_tickets","zendesk_get_user","zendesk_get_users","zendesk_merge_tickets","zendesk_search","zendesk_search_count","zendesk_search_users","zendesk_update_organization","zendesk_update_ticket","zendesk_update_tickets_bulk","zendesk_update_user","zendesk_update_users_bulk","zep_add_messages","zep_add_user","zep_create_thread","zep_delete_thread","zep_get_context","zep_get_messages","zep_get_threads","zep_get_user","zep_get_user_threads","zerobounce_get_credits","zerobounce_verify_email","zoho_desk_add_comment","zoho_desk_get_attachment","zoho_desk_get_contact","zoho_desk_get_thread","zoho_desk_get_ticket","zoho_desk_list_comments","zoho_desk_list_organizations","zoho_desk_list_threads","zoho_desk_list_tickets","zoho_desk_update_ticket","zoom_create_meeting","zoom_delete_meeting","zoom_delete_recording","zoom_get_meeting","zoom_get_meeting_invitation","zoom_get_meeting_recordings","zoom_list_meetings","zoom_list_past_participants","zoom_list_recordings","zoom_update_meeting","zoominfo_enrich_companies","zoominfo_enrich_contacts","zoominfo_search_companies","zoominfo_search_contacts","zoominfo_search_intent","zoominfo_search_news"]' + '["a2a_cancel_task","a2a_get_agent_card","a2a_get_task","a2a_send_message","agentmail_create_draft","agentmail_create_inbox","agentmail_delete_draft","agentmail_delete_inbox","agentmail_delete_thread","agentmail_forward_message","agentmail_get_draft","agentmail_get_inbox","agentmail_get_message","agentmail_get_thread","agentmail_list_drafts","agentmail_list_inboxes","agentmail_list_messages","agentmail_list_threads","agentmail_reply_message","agentmail_send_draft","agentmail_send_message","agentmail_update_draft","agentmail_update_inbox","agentmail_update_message","agentmail_update_thread","agentphone_create_call","agentphone_create_contact","agentphone_create_number","agentphone_delete_contact","agentphone_get_call","agentphone_get_call_transcript","agentphone_get_contact","agentphone_get_conversation","agentphone_get_conversation_messages","agentphone_get_number_messages","agentphone_get_usage","agentphone_get_usage_daily","agentphone_get_usage_monthly","agentphone_list_calls","agentphone_list_contacts","agentphone_list_conversations","agentphone_list_numbers","agentphone_react_to_message","agentphone_release_number","agentphone_send_message","agentphone_update_contact","agentphone_update_conversation","agiloft_async_status","agiloft_attach_file","agiloft_attachment_info","agiloft_create_record","agiloft_delete_record","agiloft_get_choice_line_id","agiloft_list_tables","agiloft_lock_record","agiloft_nlp_search","agiloft_read_record","agiloft_remove_attachment","agiloft_retrieve_attachment","agiloft_run_action_button","agiloft_saved_search","agiloft_search_records","agiloft_select_records","agiloft_update_record","agiloft_upsert_record","ahrefs_anchors","ahrefs_backlinks","ahrefs_backlinks_stats","ahrefs_batch_analysis","ahrefs_broken_backlinks","ahrefs_domain_rating","ahrefs_domain_rating_history","ahrefs_keyword_overview","ahrefs_keywords_history","ahrefs_metrics","ahrefs_metrics_history","ahrefs_organic_competitors","ahrefs_organic_keywords","ahrefs_paid_pages","ahrefs_rank_tracker_competitors_overview","ahrefs_rank_tracker_competitors_stats","ahrefs_rank_tracker_overview","ahrefs_rank_tracker_serp_overview","ahrefs_refdomains_history","ahrefs_referring_domains","ahrefs_related_terms","ahrefs_site_audit_page_explorer","ahrefs_top_pages","airtable_create_records","airtable_delete_records","airtable_get_base_schema","airtable_get_record","airtable_list_bases","airtable_list_records","airtable_list_tables","airtable_update_multiple_records","airtable_update_record","airtable_upsert_records","airweave_search","algolia_add_record","algolia_batch_operations","algolia_browse_records","algolia_clear_records","algolia_copy_move_index","algolia_delete_by_filter","algolia_delete_index","algolia_delete_record","algolia_get_record","algolia_get_records","algolia_get_settings","algolia_get_task_status","algolia_list_indices","algolia_partial_update_record","algolia_search","algolia_update_settings","amplitude_event_segmentation","amplitude_funnels","amplitude_get_active_users","amplitude_get_revenue","amplitude_group_identify","amplitude_identify_user","amplitude_list_events","amplitude_realtime_active_users","amplitude_retention","amplitude_send_event","amplitude_user_activity","amplitude_user_profile","amplitude_user_search","apify_get_dataset_items","apify_get_run","apify_run_actor_async","apify_run_actor_sync","apify_run_task","apollo_account_bulk_create","apollo_account_bulk_update","apollo_account_create","apollo_account_search","apollo_account_update","apollo_contact_bulk_create","apollo_contact_bulk_update","apollo_contact_create","apollo_contact_search","apollo_contact_update","apollo_email_accounts","apollo_opportunity_create","apollo_opportunity_get","apollo_opportunity_search","apollo_opportunity_update","apollo_organization_bulk_enrich","apollo_organization_enrich","apollo_organization_search","apollo_people_bulk_enrich","apollo_people_enrich","apollo_people_search","apollo_sequence_add_contacts","apollo_sequence_search","apollo_task_create","apollo_task_search","appconfig_create_application","appconfig_create_configuration_profile","appconfig_create_environment","appconfig_create_hosted_configuration_version","appconfig_delete_application","appconfig_delete_configuration_profile","appconfig_delete_environment","appconfig_delete_hosted_configuration_version","appconfig_get_application","appconfig_get_configuration","appconfig_get_configuration_profile","appconfig_get_deployment","appconfig_get_environment","appconfig_get_hosted_configuration_version","appconfig_list_applications","appconfig_list_configuration_profiles","appconfig_list_deployment_strategies","appconfig_list_deployments","appconfig_list_environments","appconfig_list_hosted_configuration_versions","appconfig_start_deployment","appconfig_stop_deployment","appconfig_update_application","appconfig_update_configuration_profile","appconfig_update_environment","arxiv_get_author_papers","arxiv_get_paper","arxiv_search","asana_add_comment","asana_add_followers","asana_create_project","asana_create_section","asana_create_subtask","asana_create_task","asana_delete_task","asana_get_project","asana_get_projects","asana_get_task","asana_list_sections","asana_list_workspaces","asana_search_tasks","asana_update_task","ashby_add_candidate_tag","ashby_change_application_stage","ashby_create_application","ashby_create_candidate","ashby_create_note","ashby_get_application","ashby_get_candidate","ashby_get_job","ashby_get_job_posting","ashby_get_offer","ashby_list_applications","ashby_list_archive_reasons","ashby_list_candidate_tags","ashby_list_candidates","ashby_list_custom_fields","ashby_list_departments","ashby_list_interviews","ashby_list_job_postings","ashby_list_jobs","ashby_list_locations","ashby_list_notes","ashby_list_offers","ashby_list_openings","ashby_list_sources","ashby_list_users","ashby_remove_candidate_tag","ashby_search_candidates","ashby_update_candidate","athena_batch_get_query_execution","athena_create_named_query","athena_delete_named_query","athena_get_named_query","athena_get_query_execution","athena_get_query_results","athena_list_databases","athena_list_named_queries","athena_list_query_executions","athena_list_table_metadata","athena_start_query","athena_stop_query","attio_assert_record","attio_create_attribute","attio_create_comment","attio_create_list","attio_create_list_entry","attio_create_note","attio_create_object","attio_create_record","attio_create_task","attio_create_webhook","attio_delete_comment","attio_delete_list_entry","attio_delete_note","attio_delete_record","attio_delete_task","attio_delete_webhook","attio_get_attribute","attio_get_comment","attio_get_list","attio_get_list_entry","attio_get_member","attio_get_note","attio_get_object","attio_get_record","attio_get_task","attio_get_thread","attio_get_webhook","attio_list_attributes","attio_list_lists","attio_list_members","attio_list_notes","attio_list_objects","attio_list_records","attio_list_tasks","attio_list_threads","attio_list_webhooks","attio_query_list_entries","attio_search_records","attio_update_attribute","attio_update_list","attio_update_list_entry","attio_update_object","attio_update_record","attio_update_task","attio_update_webhook","azure_devops_add_comment","azure_devops_create_work_item","azure_devops_get_build_log","azure_devops_get_build_timeline","azure_devops_get_comments","azure_devops_get_pipeline","azure_devops_get_pipeline_run","azure_devops_get_work_item","azure_devops_get_work_items_batch","azure_devops_get_work_items_between_builds","azure_devops_list_build_logs","azure_devops_list_builds","azure_devops_list_pipeline_runs","azure_devops_list_pipelines","azure_devops_query_work_items","azure_devops_update_work_item","box_copy_file","box_create_folder","box_delete_file","box_delete_folder","box_download_file","box_get_file_info","box_list_folder_items","box_search","box_sign_cancel_request","box_sign_create_request","box_sign_get_request","box_sign_list_requests","box_sign_resend_request","box_update_file","box_upload_file","brandfetch_get_brand","brandfetch_search","brex_archive_budget","brex_create_budget","brex_create_spend_limit","brex_create_transfer","brex_create_vendor","brex_get_budget","brex_get_cash_account","brex_get_company","brex_get_current_user","brex_get_expense","brex_get_spend_limit","brex_get_transfer","brex_get_user","brex_get_vendor","brex_list_budgets","brex_list_card_accounts","brex_list_card_statements","brex_list_card_transactions","brex_list_cards","brex_list_cash_accounts","brex_list_cash_statements","brex_list_cash_transactions","brex_list_departments","brex_list_expenses","brex_list_locations","brex_list_spend_limits","brex_list_titles","brex_list_transfers","brex_list_users","brex_list_vendors","brex_match_receipt","brex_update_expense","brex_update_vendor","brex_upload_receipt","brightdata_cancel_snapshot","brightdata_discover","brightdata_download_snapshot","brightdata_scrape_dataset","brightdata_scrape_url","brightdata_serp_search","brightdata_snapshot_status","brightdata_sync_scrape","browser_use_run_task","buffer_create_idea","buffer_create_post","buffer_delete_post","buffer_edit_post","buffer_get_account","buffer_get_channels","buffer_get_idea_groups","buffer_get_ideas","buffer_get_post","buffer_get_posts","calcom_cancel_booking","calcom_confirm_booking","calcom_create_booking","calcom_create_event_type","calcom_create_schedule","calcom_decline_booking","calcom_delete_event_type","calcom_delete_schedule","calcom_get_booking","calcom_get_default_schedule","calcom_get_event_type","calcom_get_schedule","calcom_get_slots","calcom_list_bookings","calcom_list_event_types","calcom_list_schedules","calcom_reschedule_booking","calcom_update_event_type","calcom_update_schedule","calendly_cancel_event","calendly_create_event_invitee","calendly_create_invitee_no_show","calendly_create_scheduling_link","calendly_create_webhook","calendly_delete_invitee_no_show","calendly_delete_webhook","calendly_get_current_user","calendly_get_event_invitee","calendly_get_event_type","calendly_get_scheduled_event","calendly_get_user","calendly_list_event_invitees","calendly_list_event_type_available_times","calendly_list_event_types","calendly_list_organization_memberships","calendly_list_routing_form_submissions","calendly_list_routing_forms","calendly_list_scheduled_events","calendly_list_user_availability_schedules","calendly_list_user_busy_times","calendly_list_webhooks","clay_populate","clerk_add_organization_member","clerk_ban_user","clerk_create_actor_token","clerk_create_allowlist_identifier","clerk_create_blocklist_identifier","clerk_create_organization","clerk_create_organization_invitation","clerk_create_user","clerk_delete_allowlist_identifier","clerk_delete_blocklist_identifier","clerk_delete_organization","clerk_delete_user","clerk_get_jwt_template","clerk_get_organization","clerk_get_session","clerk_get_user","clerk_get_user_oauth_token","clerk_list_allowlist_identifiers","clerk_list_blocklist_identifiers","clerk_list_jwt_templates","clerk_list_organization_invitations","clerk_list_organization_memberships","clerk_list_organizations","clerk_list_sessions","clerk_list_users","clerk_lock_user","clerk_remove_organization_member","clerk_revoke_actor_token","clerk_revoke_session","clerk_unban_user","clerk_unlock_user","clerk_update_organization","clerk_update_organization_membership","clerk_update_user","clickhouse_count_rows","clickhouse_create_database","clickhouse_create_table","clickhouse_delete","clickhouse_describe_table","clickhouse_drop_database","clickhouse_drop_partition","clickhouse_drop_table","clickhouse_execute","clickhouse_insert","clickhouse_insert_rows","clickhouse_introspect","clickhouse_kill_query","clickhouse_list_clusters","clickhouse_list_databases","clickhouse_list_mutations","clickhouse_list_partitions","clickhouse_list_running_queries","clickhouse_list_tables","clickhouse_optimize_table","clickhouse_query","clickhouse_rename_table","clickhouse_show_create_table","clickhouse_table_stats","clickhouse_truncate_table","clickhouse_update","clickup_add_tag_to_task","clickup_create_checklist","clickup_create_checklist_item","clickup_create_comment","clickup_create_folder","clickup_create_list","clickup_create_task","clickup_create_time_entry","clickup_delete_checklist","clickup_delete_checklist_item","clickup_delete_comment","clickup_delete_task","clickup_delete_time_entry","clickup_get_comments","clickup_get_custom_fields","clickup_get_folders","clickup_get_list_members","clickup_get_lists","clickup_get_running_timer","clickup_get_space_tags","clickup_get_spaces","clickup_get_task","clickup_get_task_members","clickup_get_tasks","clickup_get_time_entries","clickup_get_workspaces","clickup_remove_custom_field_value","clickup_remove_tag_from_task","clickup_search_tasks","clickup_set_custom_field_value","clickup_start_timer","clickup_stop_timer","clickup_update_checklist","clickup_update_checklist_item","clickup_update_comment","clickup_update_task","clickup_update_time_entry","clickup_upload_attachment","cloudflare_create_dns_record","cloudflare_create_zone","cloudflare_delete_dns_record","cloudflare_delete_zone","cloudflare_dns_analytics","cloudflare_get_zone","cloudflare_get_zone_settings","cloudflare_list_certificates","cloudflare_list_dns_records","cloudflare_list_zones","cloudflare_purge_cache","cloudflare_update_dns_record","cloudflare_update_zone_setting","cloudformation_cancel_update_stack","cloudformation_create_change_set","cloudformation_create_stack","cloudformation_delete_stack","cloudformation_describe_change_set","cloudformation_describe_stack_drift_detection_status","cloudformation_describe_stack_events","cloudformation_describe_stacks","cloudformation_detect_stack_drift","cloudformation_execute_change_set","cloudformation_get_template","cloudformation_get_template_summary","cloudformation_list_stack_resources","cloudformation_update_stack","cloudformation_validate_template","cloudwatch_describe_alarm_history","cloudwatch_describe_alarms","cloudwatch_describe_log_groups","cloudwatch_describe_log_streams","cloudwatch_filter_log_events","cloudwatch_get_log_events","cloudwatch_get_metric_statistics","cloudwatch_list_metrics","cloudwatch_mute_alarm","cloudwatch_put_log_group_retention","cloudwatch_put_metric_data","cloudwatch_query_logs","cloudwatch_unmute_alarm","codepipeline_disable_stage_transition","codepipeline_enable_stage_transition","codepipeline_get_pipeline","codepipeline_get_pipeline_execution","codepipeline_get_pipeline_state","codepipeline_list_action_executions","codepipeline_list_pipeline_executions","codepipeline_list_pipelines","codepipeline_put_approval_result","codepipeline_retry_stage_execution","codepipeline_start_execution","codepipeline_stop_execution","confluence_add_label","confluence_create_blogpost","confluence_create_comment","confluence_create_page","confluence_create_page_property","confluence_create_space","confluence_create_space_property","confluence_delete_attachment","confluence_delete_blogpost","confluence_delete_comment","confluence_delete_label","confluence_delete_page","confluence_delete_page_property","confluence_delete_space","confluence_delete_space_property","confluence_get_blogpost","confluence_get_page_ancestors","confluence_get_page_children","confluence_get_page_descendants","confluence_get_page_version","confluence_get_pages_by_label","confluence_get_space","confluence_get_task","confluence_get_user","confluence_list_attachments","confluence_list_blogposts","confluence_list_blogposts_in_space","confluence_list_comments","confluence_list_labels","confluence_list_page_properties","confluence_list_page_versions","confluence_list_pages_in_space","confluence_list_space_labels","confluence_list_space_permissions","confluence_list_space_properties","confluence_list_spaces","confluence_list_tasks","confluence_retrieve","confluence_search","confluence_search_in_space","confluence_update","confluence_update_blogpost","confluence_update_comment","confluence_update_space","confluence_update_task","confluence_upload_attachment","context_dev_classify_naics","context_dev_classify_sic","context_dev_crawl","context_dev_extract","context_dev_extract_product","context_dev_extract_products","context_dev_get_brand","context_dev_get_brand_by_email","context_dev_get_brand_by_name","context_dev_get_brand_by_ticker","context_dev_identify_transaction","context_dev_map","context_dev_scrape_fonts","context_dev_scrape_html","context_dev_scrape_images","context_dev_scrape_markdown","context_dev_scrape_styleguide","context_dev_screenshot","context_dev_search","convex_action","convex_document_deltas","convex_list_documents","convex_list_tables","convex_mutation","convex_query","convex_run_function","crowdstrike_get_sensor_aggregates","crowdstrike_get_sensor_details","crowdstrike_query_sensors","cursor_add_followup","cursor_add_followup_v2","cursor_delete_agent","cursor_delete_agent_v2","cursor_download_artifact","cursor_download_artifact_v2","cursor_get_agent","cursor_get_agent_v2","cursor_get_api_key_info","cursor_get_api_key_info_v2","cursor_get_conversation","cursor_get_conversation_v2","cursor_launch_agent","cursor_launch_agent_v2","cursor_list_agents","cursor_list_agents_v2","cursor_list_artifacts","cursor_list_artifacts_v2","cursor_list_models","cursor_list_models_v2","cursor_list_repositories","cursor_list_repositories_v2","cursor_stop_agent","cursor_stop_agent_v2","dagster_delete_run","dagster_get_asset","dagster_get_run","dagster_get_run_logs","dagster_launch_run","dagster_list_assets","dagster_list_jobs","dagster_list_runs","dagster_list_schedules","dagster_list_sensors","dagster_materialize_assets","dagster_reexecute_run","dagster_report_asset_materialization","dagster_start_schedule","dagster_start_sensor","dagster_stop_schedule","dagster_stop_sensor","dagster_terminate_run","dagster_wipe_asset","databricks_cancel_run","databricks_execute_sql","databricks_get_cluster","databricks_get_job","databricks_get_run","databricks_get_run_output","databricks_get_statement","databricks_list_clusters","databricks_list_jobs","databricks_list_runs","databricks_list_warehouses","databricks_run_job","datadog_cancel_downtime","datadog_create_downtime","datadog_create_event","datadog_create_monitor","datadog_get_monitor","datadog_list_downtimes","datadog_list_monitors","datadog_mute_monitor","datadog_query_logs","datadog_query_timeseries","datadog_send_logs","datadog_submit_metrics","datagma_enrich_company","datagma_enrich_person","datagma_find_email","datagma_find_phone","datagma_get_credits","daytona_create_sandbox","daytona_delete_sandbox","daytona_download_file","daytona_execute_command","daytona_get_sandbox","daytona_git_clone","daytona_list_files","daytona_list_sandboxes","daytona_run_code","daytona_start_sandbox","daytona_stop_sandbox","daytona_upload_file","deployed_block_executor","deployments_deploy","deployments_get_version","deployments_list_versions","deployments_promote","deployments_undeploy","devin_append_session_tags","devin_archive_session","devin_create_session","devin_get_session","devin_get_session_tags","devin_list_session_attachments","devin_list_session_messages","devin_list_sessions","devin_replace_session_tags","devin_send_message","devin_terminate_session","discord_add_reaction","discord_archive_thread","discord_assign_role","discord_ban_member","discord_bulk_delete_messages","discord_create_channel","discord_create_invite","discord_create_role","discord_create_thread","discord_create_webhook","discord_delete_channel","discord_delete_invite","discord_delete_message","discord_delete_role","discord_delete_webhook","discord_edit_message","discord_execute_webhook","discord_get_channel","discord_get_invite","discord_get_member","discord_get_messages","discord_get_pinned_messages","discord_get_server","discord_get_user","discord_get_webhook","discord_join_thread","discord_kick_member","discord_leave_thread","discord_list_channels","discord_list_roles","discord_pin_message","discord_remove_reaction","discord_remove_role","discord_send_message","discord_unban_member","discord_unpin_message","discord_update_channel","discord_update_member","discord_update_role","docusign_create_from_template","docusign_download_document","docusign_get_envelope","docusign_list_envelopes","docusign_list_recipients","docusign_list_templates","docusign_send_envelope","docusign_void_envelope","downdetector_get_company","downdetector_get_company_attribution","downdetector_get_company_baseline","downdetector_get_company_events","downdetector_get_company_incidents","downdetector_get_company_indicators","downdetector_get_company_last_15","downdetector_get_company_status","downdetector_get_provider","downdetector_get_reports","downdetector_get_site_companies","downdetector_list_categories","downdetector_list_incidents","downdetector_list_sites","downdetector_search_companies","dropbox_copy","dropbox_create_folder","dropbox_create_shared_link","dropbox_delete","dropbox_download","dropbox_get_metadata","dropbox_list_folder","dropbox_list_revisions","dropbox_list_shared_links","dropbox_move","dropbox_restore","dropbox_search","dropbox_upload","dropcontact_enrich_contact","dspy_chain_of_thought","dspy_predict","dspy_react","dub_bulk_create_links","dub_bulk_delete_links","dub_bulk_update_links","dub_create_link","dub_create_tag","dub_delete_link","dub_get_analytics","dub_get_events","dub_get_link","dub_get_links_count","dub_get_qr_code","dub_list_domains","dub_list_folders","dub_list_links","dub_list_tags","dub_update_link","dub_upsert_link","duckduckgo_search","dynamodb_delete","dynamodb_get","dynamodb_introspect","dynamodb_put","dynamodb_query","dynamodb_scan","dynamodb_update","dynatrace_add_problem_comment","dynatrace_add_tags","dynatrace_close_problem","dynatrace_create_settings_object","dynatrace_create_slo","dynatrace_delete_problem_comment","dynatrace_delete_settings_object","dynatrace_delete_slo","dynatrace_delete_tag","dynatrace_execute_synthetic_monitors","dynatrace_get_attack","dynatrace_get_audit_logs","dynatrace_get_entity","dynatrace_get_event","dynatrace_get_metric","dynatrace_get_problem","dynatrace_get_problem_comment","dynatrace_get_security_problem","dynatrace_get_settings_object","dynatrace_get_slo","dynatrace_get_synthetic_batch","dynatrace_ingest_event","dynatrace_ingest_logs","dynatrace_ingest_metrics","dynatrace_list_attacks","dynatrace_list_entities","dynatrace_list_entity_types","dynatrace_list_events","dynatrace_list_metrics","dynatrace_list_problem_comments","dynatrace_list_problems","dynatrace_list_remediation_items","dynatrace_list_security_problems","dynatrace_list_settings_objects","dynatrace_list_settings_schemas","dynatrace_list_slos","dynatrace_list_synthetic_monitors","dynatrace_list_tags","dynatrace_mute_security_problem","dynatrace_mute_security_problems","dynatrace_query_metrics","dynatrace_search_logs","dynatrace_unmute_security_problem","dynatrace_unmute_security_problems","dynatrace_update_problem_comment","dynatrace_update_settings_object","dynatrace_update_slo","elasticsearch_bulk","elasticsearch_cluster_health","elasticsearch_cluster_stats","elasticsearch_count","elasticsearch_create_index","elasticsearch_delete_document","elasticsearch_delete_index","elasticsearch_get_document","elasticsearch_get_index","elasticsearch_index_document","elasticsearch_list_indices","elasticsearch_search","elasticsearch_update_document","elevenlabs_audio_isolation","elevenlabs_edit_voice_settings","elevenlabs_get_user","elevenlabs_get_voice","elevenlabs_get_voice_settings","elevenlabs_list_models","elevenlabs_list_voices","elevenlabs_sound_effects","elevenlabs_speech_to_speech","elevenlabs_tts","emailbison_attach_leads_to_campaign","emailbison_attach_tags_to_leads","emailbison_create_campaign","emailbison_create_lead","emailbison_create_tag","emailbison_get_lead","emailbison_list_campaigns","emailbison_list_leads","emailbison_list_replies","emailbison_list_tags","emailbison_update_campaign","emailbison_update_campaign_status","emailbison_update_lead","embeddings_cohere","embeddings_gemini","embeddings_mistral","embeddings_openai","embeddings_openrouter","enrich_check_credits","enrich_company_funding","enrich_company_lookup","enrich_company_revenue","enrich_disposable_email_check","enrich_email_to_ip","enrich_email_to_person_lite","enrich_email_to_phone","enrich_email_to_profile","enrich_find_email","enrich_get_post_details","enrich_ip_to_company","enrich_linkedin_profile","enrich_linkedin_to_personal_email","enrich_linkedin_to_work_email","enrich_phone_finder","enrich_reverse_hash_lookup","enrich_sales_pointer_people","enrich_search_company","enrich_search_company_activities","enrich_search_company_employees","enrich_search_jobs","enrich_search_logo","enrich_search_people","enrich_search_people_activities","enrich_search_post_comments","enrich_search_post_comments_by_url","enrich_search_post_reactions","enrich_search_post_reactions_by_url","enrich_search_posts","enrich_search_similar_companies","enrich_verify_email","enrichment_run","enrow_find_email","enrow_verify_email","evernote_copy_note","evernote_create_note","evernote_create_notebook","evernote_create_tag","evernote_delete_note","evernote_get_note","evernote_get_notebook","evernote_list_notebooks","evernote_list_tags","evernote_search_notes","evernote_update_note","exa_agent","exa_answer","exa_find_similar_links","exa_get_contents","exa_search","extend_parser","extend_parser_v2","fathom_get_summary","fathom_get_transcript","fathom_list_meeting_types","fathom_list_meetings","fathom_list_team_members","fathom_list_teams","file_append","file_compress","file_decompress","file_fetch","file_get","file_get_content","file_manage_sharing","file_parser","file_parser_v2","file_parser_v3","file_read","file_write","findymail_find_email_from_linkedin","findymail_find_email_from_name","findymail_find_emails_by_domain","findymail_find_employees","findymail_find_phone","findymail_get_company","findymail_get_credits","findymail_lookup_technologies","findymail_reverse_email_lookup","findymail_search_technologies","findymail_verify_email","firecrawl_agent","firecrawl_batch_scrape","firecrawl_batch_scrape_status","firecrawl_cancel_crawl","firecrawl_crawl","firecrawl_crawl_status","firecrawl_credit_usage","firecrawl_extract","firecrawl_extract_status","firecrawl_map","firecrawl_parse","firecrawl_scrape","firecrawl_search","fireflies_add_to_live_meeting","fireflies_create_bite","fireflies_delete_transcript","fireflies_get_transcript","fireflies_get_user","fireflies_list_bites","fireflies_list_contacts","fireflies_list_transcripts","fireflies_list_users","fireflies_upload_audio","flint_create_task","flint_generate_pages","flint_get_task","function_execute","gamma_check_status","gamma_generate","gamma_generate_from_template","gamma_list_folders","gamma_list_themes","github_add_assignees","github_add_assignees_v2","github_add_labels","github_add_labels_v2","github_cancel_workflow_run","github_cancel_workflow_run_v2","github_check_star","github_check_star_v2","github_close_issue","github_close_issue_v2","github_close_pr","github_close_pr_v2","github_comment","github_comment_v2","github_compare_commits","github_compare_commits_v2","github_create_branch","github_create_branch_v2","github_create_comment_reaction","github_create_comment_reaction_v2","github_create_file","github_create_file_v2","github_create_gist","github_create_gist_v2","github_create_issue","github_create_issue_reaction","github_create_issue_reaction_v2","github_create_issue_v2","github_create_milestone","github_create_milestone_v2","github_create_pr","github_create_pr_review","github_create_pr_review_v2","github_create_pr_v2","github_create_project","github_create_project_v2","github_create_release","github_create_release_v2","github_delete_branch","github_delete_branch_v2","github_delete_comment","github_delete_comment_reaction","github_delete_comment_reaction_v2","github_delete_comment_v2","github_delete_file","github_delete_file_v2","github_delete_gist","github_delete_gist_v2","github_delete_issue_reaction","github_delete_issue_reaction_v2","github_delete_milestone","github_delete_milestone_v2","github_delete_project","github_delete_project_v2","github_delete_release","github_delete_release_v2","github_fork_gist","github_fork_gist_v2","github_fork_repo","github_fork_repo_v2","github_get_branch","github_get_branch_protection","github_get_branch_protection_v2","github_get_branch_v2","github_get_commit","github_get_commit_v2","github_get_file_content","github_get_file_content_v2","github_get_gist","github_get_gist_v2","github_get_issue","github_get_issue_v2","github_get_latest_release","github_get_latest_release_v2","github_get_milestone","github_get_milestone_v2","github_get_pr_files","github_get_pr_files_v2","github_get_project","github_get_project_v2","github_get_readme","github_get_readme_v2","github_get_release","github_get_release_v2","github_get_tree","github_get_tree_v2","github_get_workflow","github_get_workflow_run","github_get_workflow_run_v2","github_get_workflow_v2","github_issue_comment","github_issue_comment_v2","github_job_logs","github_latest_commit","github_latest_commit_v2","github_list_branches","github_list_branches_v2","github_list_commits","github_list_commits_v2","github_list_forks","github_list_forks_v2","github_list_gists","github_list_gists_v2","github_list_issue_comments","github_list_issue_comments_v2","github_list_issues","github_list_issues_v2","github_list_milestones","github_list_milestones_v2","github_list_pr_comments","github_list_pr_comments_v2","github_list_projects","github_list_projects_v2","github_list_prs","github_list_prs_v2","github_list_releases","github_list_releases_v2","github_list_review_threads","github_list_stargazers","github_list_stargazers_v2","github_list_tags","github_list_tags_v2","github_list_workflow_runs","github_list_workflow_runs_v2","github_list_workflows","github_list_workflows_v2","github_merge_pr","github_merge_pr_v2","github_pr","github_pr_v2","github_remove_label","github_remove_label_v2","github_reply_review_thread","github_repo_info","github_repo_info_v2","github_request_reviewers","github_request_reviewers_v2","github_rerun_workflow","github_rerun_workflow_v2","github_resolve_review_thread","github_search_code","github_search_code_v2","github_search_commits","github_search_commits_v2","github_search_issues","github_search_issues_v2","github_search_repos","github_search_repos_v2","github_search_users","github_search_users_v2","github_star_gist","github_star_gist_v2","github_star_repo","github_star_repo_v2","github_status_check_rollup","github_trigger_workflow","github_trigger_workflow_v2","github_unstar_gist","github_unstar_gist_v2","github_unstar_repo","github_unstar_repo_v2","github_update_branch_protection","github_update_branch_protection_v2","github_update_comment","github_update_comment_v2","github_update_file","github_update_file_v2","github_update_gist","github_update_gist_v2","github_update_issue","github_update_issue_v2","github_update_milestone","github_update_milestone_v2","github_update_pr","github_update_pr_v2","github_update_project","github_update_project_v2","github_update_release","github_update_release_v2","gitlab_activate_user","gitlab_add_member","gitlab_add_saml_group_link","gitlab_approve_access_request","gitlab_approve_merge_request","gitlab_approve_user","gitlab_ban_user","gitlab_block_user","gitlab_cancel_pipeline","gitlab_compare_branches","gitlab_create_branch","gitlab_create_file","gitlab_create_issue","gitlab_create_issue_note","gitlab_create_merge_request","gitlab_create_merge_request_note","gitlab_create_pipeline","gitlab_create_release","gitlab_create_user","gitlab_deactivate_user","gitlab_delete_branch","gitlab_delete_issue","gitlab_delete_saml_group_link","gitlab_delete_user","gitlab_delete_user_identity","gitlab_deny_access_request","gitlab_get_file","gitlab_get_group","gitlab_get_issue","gitlab_get_job_log","gitlab_get_merge_request","gitlab_get_merge_request_changes","gitlab_get_pipeline","gitlab_get_project","gitlab_invite_member","gitlab_list_access_requests","gitlab_list_branches","gitlab_list_commits","gitlab_list_groups","gitlab_list_invitations","gitlab_list_issues","gitlab_list_members","gitlab_list_merge_requests","gitlab_list_pipeline_jobs","gitlab_list_pipelines","gitlab_list_projects","gitlab_list_releases","gitlab_list_repository_tree","gitlab_list_saml_group_links","gitlab_list_user_memberships","gitlab_merge_merge_request","gitlab_play_job","gitlab_reject_user","gitlab_remove_member","gitlab_retry_pipeline","gitlab_revoke_invitation","gitlab_search_users","gitlab_unban_user","gitlab_unblock_user","gitlab_update_file","gitlab_update_invitation","gitlab_update_issue","gitlab_update_member","gitlab_update_merge_request","gitlab_update_user","gmail_add_label","gmail_add_label_v2","gmail_archive","gmail_archive_v2","gmail_create_label_v2","gmail_delete","gmail_delete_draft_v2","gmail_delete_label_v2","gmail_delete_v2","gmail_draft","gmail_draft_v2","gmail_edit_draft_v2","gmail_get_draft_v2","gmail_get_thread_v2","gmail_list_drafts_v2","gmail_list_labels_v2","gmail_list_threads_v2","gmail_mark_read","gmail_mark_read_v2","gmail_mark_unread","gmail_mark_unread_v2","gmail_move","gmail_move_v2","gmail_read","gmail_read_v2","gmail_remove_label","gmail_remove_label_v2","gmail_search","gmail_search_v2","gmail_send","gmail_send_v2","gmail_trash_thread_v2","gmail_unarchive","gmail_unarchive_v2","gmail_untrash_thread_v2","gmail_update_label_v2","gong_aggregate_activity","gong_aggregate_by_period","gong_answered_scorecards","gong_ask_anything","gong_assign_flow_prospects","gong_create_call","gong_day_by_day_activity","gong_get_brief","gong_get_call","gong_get_call_transcript","gong_get_coaching","gong_get_extensive_calls","gong_get_folder_content","gong_get_logs","gong_get_prospect_flows","gong_get_user","gong_interaction_stats","gong_list_calls","gong_list_flows","gong_list_library_folders","gong_list_scorecards","gong_list_trackers","gong_list_users","gong_list_workspaces","gong_lookup_email","gong_lookup_phone","gong_purge_email_address","gong_purge_phone_number","gong_unassign_flow_prospects","google_ads_ad_performance","google_ads_campaign_performance","google_ads_list_ad_groups","google_ads_list_campaigns","google_ads_list_customers","google_ads_search","google_appsheet_add_rows","google_appsheet_delete_rows","google_appsheet_edit_rows","google_appsheet_find_rows","google_bigquery_create_dataset","google_bigquery_create_table","google_bigquery_delete_dataset","google_bigquery_delete_table","google_bigquery_get_query_results","google_bigquery_get_table","google_bigquery_insert_rows","google_bigquery_list_datasets","google_bigquery_list_table_data","google_bigquery_list_tables","google_bigquery_query","google_books_volume_details","google_books_volume_search","google_calendar_create","google_calendar_create_calendar","google_calendar_create_calendar_v2","google_calendar_create_v2","google_calendar_delete","google_calendar_delete_calendar","google_calendar_delete_calendar_v2","google_calendar_delete_v2","google_calendar_freebusy","google_calendar_freebusy_v2","google_calendar_get","google_calendar_get_v2","google_calendar_instances","google_calendar_instances_v2","google_calendar_invite","google_calendar_invite_v2","google_calendar_list","google_calendar_list_acl","google_calendar_list_acl_v2","google_calendar_list_calendars","google_calendar_list_calendars_v2","google_calendar_list_v2","google_calendar_move","google_calendar_move_v2","google_calendar_quick_add","google_calendar_quick_add_v2","google_calendar_share_calendar","google_calendar_share_calendar_v2","google_calendar_unshare_calendar","google_calendar_unshare_calendar_v2","google_calendar_update","google_calendar_update_acl","google_calendar_update_acl_v2","google_calendar_update_calendar","google_calendar_update_calendar_v2","google_calendar_update_v2","google_contacts_create","google_contacts_delete","google_contacts_get","google_contacts_list","google_contacts_search","google_contacts_update","google_docs_create","google_docs_create_named_range","google_docs_create_paragraph_bullets","google_docs_delete_content_range","google_docs_delete_named_range","google_docs_delete_paragraph_bullets","google_docs_insert_image","google_docs_insert_page_break","google_docs_insert_table","google_docs_insert_text","google_docs_read","google_docs_replace_text","google_docs_update_paragraph_style","google_docs_update_text_style","google_docs_write","google_drive_copy","google_drive_create_comment","google_drive_create_folder","google_drive_delete","google_drive_delete_comment","google_drive_download","google_drive_export","google_drive_get_about","google_drive_get_content","google_drive_get_file","google_drive_get_revision","google_drive_list","google_drive_list_comments","google_drive_list_permissions","google_drive_list_revisions","google_drive_move","google_drive_search","google_drive_share","google_drive_trash","google_drive_unshare","google_drive_untrash","google_drive_update","google_drive_upload","google_forms_batch_update","google_forms_create_form","google_forms_create_watch","google_forms_delete_watch","google_forms_get_form","google_forms_get_responses","google_forms_list_watches","google_forms_renew_watch","google_forms_set_publish_settings","google_groups_add_alias","google_groups_add_member","google_groups_create_group","google_groups_delete_group","google_groups_get_group","google_groups_get_member","google_groups_get_settings","google_groups_has_member","google_groups_list_aliases","google_groups_list_groups","google_groups_list_members","google_groups_remove_alias","google_groups_remove_member","google_groups_update_group","google_groups_update_member","google_groups_update_settings","google_maps_air_quality","google_maps_directions","google_maps_distance_matrix","google_maps_elevation","google_maps_geocode","google_maps_geolocate","google_maps_place_details","google_maps_places_nearby","google_maps_places_search","google_maps_pollen","google_maps_reverse_geocode","google_maps_snap_to_roads","google_maps_solar","google_maps_speed_limits","google_maps_timezone","google_maps_validate_address","google_meet_create_space","google_meet_end_conference","google_meet_get_conference_record","google_meet_get_space","google_meet_list_conference_records","google_meet_list_participants","google_pagespeed_analyze","google_search","google_sheets_append","google_sheets_append_v2","google_sheets_batch_clear_v2","google_sheets_batch_get_v2","google_sheets_batch_update_v2","google_sheets_clear_v2","google_sheets_copy_sheet_v2","google_sheets_create_spreadsheet_v2","google_sheets_delete_rows_v2","google_sheets_delete_sheet_v2","google_sheets_delete_spreadsheet_v2","google_sheets_get_spreadsheet_v2","google_sheets_read","google_sheets_read_v2","google_sheets_update","google_sheets_update_v2","google_sheets_write","google_sheets_write_v2","google_slides_add_image","google_slides_add_slide","google_slides_batch_update","google_slides_copy_presentation","google_slides_create","google_slides_create_line","google_slides_create_paragraph_bullets","google_slides_create_shape","google_slides_create_sheets_chart","google_slides_create_table","google_slides_create_video","google_slides_delete_object","google_slides_delete_paragraph_bullets","google_slides_delete_table_column","google_slides_delete_table_row","google_slides_delete_text","google_slides_duplicate_object","google_slides_export_presentation","google_slides_get_page","google_slides_get_thumbnail","google_slides_group_objects","google_slides_insert_table_columns","google_slides_insert_table_rows","google_slides_insert_text","google_slides_merge_table_cells","google_slides_read","google_slides_refresh_sheets_chart","google_slides_replace_all_shapes_with_image","google_slides_replace_all_shapes_with_sheets_chart","google_slides_replace_all_text","google_slides_replace_image","google_slides_reroute_line","google_slides_ungroup_objects","google_slides_unmerge_table_cells","google_slides_update_image_properties","google_slides_update_line_category","google_slides_update_line_properties","google_slides_update_page_element_alt_text","google_slides_update_page_element_transform","google_slides_update_page_elements_z_order","google_slides_update_page_properties","google_slides_update_paragraph_style","google_slides_update_shape_properties","google_slides_update_slide_properties","google_slides_update_slides_position","google_slides_update_table_border_properties","google_slides_update_table_cell_properties","google_slides_update_table_column_properties","google_slides_update_table_row_properties","google_slides_update_text_style","google_slides_update_video_properties","google_slides_write","google_tasks_create","google_tasks_delete","google_tasks_get","google_tasks_list","google_tasks_list_task_lists","google_tasks_update","google_translate_detect","google_translate_text","google_vault_add_held_accounts","google_vault_add_matters_permissions","google_vault_close_matters","google_vault_create_matters","google_vault_create_matters_export","google_vault_create_matters_holds","google_vault_create_saved_query","google_vault_delete_matters","google_vault_delete_matters_export","google_vault_delete_matters_holds","google_vault_delete_saved_query","google_vault_download_export_file","google_vault_list_matters","google_vault_list_matters_export","google_vault_list_matters_holds","google_vault_list_saved_queries","google_vault_remove_held_accounts","google_vault_remove_matters_permissions","google_vault_reopen_matters","google_vault_undelete_matters","google_vault_update_matters","google_vault_update_matters_holds","grafana_check_data_source_health","grafana_create_alert_rule","grafana_create_annotation","grafana_create_contact_point","grafana_create_dashboard","grafana_create_folder","grafana_delete_alert_rule","grafana_delete_annotation","grafana_delete_dashboard","grafana_delete_folder","grafana_get_alert_rule","grafana_get_dashboard","grafana_get_data_source","grafana_get_folder","grafana_get_health","grafana_list_alert_rules","grafana_list_annotations","grafana_list_contact_points","grafana_list_dashboards","grafana_list_data_sources","grafana_list_folders","grafana_update_alert_rule","grafana_update_annotation","grafana_update_dashboard","grafana_update_folder","grain_create_hook","grain_create_hook_v2","grain_delete_hook","grain_delete_hook_v2","grain_get_recording","grain_get_transcript","grain_list_hooks","grain_list_hooks_v2","grain_list_meeting_types","grain_list_recordings","grain_list_teams","grain_list_views","granola_get_note","granola_list_folders","granola_list_notes","greenhouse_get_application","greenhouse_get_candidate","greenhouse_get_job","greenhouse_get_user","greenhouse_list_applications","greenhouse_list_candidates","greenhouse_list_departments","greenhouse_list_job_stages","greenhouse_list_jobs","greenhouse_list_offices","greenhouse_list_users","greptile_index_repo","greptile_query","greptile_search","greptile_status","guardrails_validate","hex_cancel_run","hex_create_collection","hex_create_group","hex_deactivate_user","hex_delete_group","hex_get_collection","hex_get_data_connection","hex_get_group","hex_get_project","hex_get_project_runs","hex_get_queried_tables","hex_get_run_status","hex_list_collections","hex_list_data_connections","hex_list_groups","hex_list_projects","hex_list_users","hex_run_project","hex_update_collection","hex_update_group","hex_update_project","http_request","hubspot_add_list_memberships","hubspot_create_appointment","hubspot_create_association","hubspot_create_company","hubspot_create_contact","hubspot_create_deal","hubspot_create_email","hubspot_create_line_item","hubspot_create_list","hubspot_create_note","hubspot_create_ticket","hubspot_delete_association","hubspot_delete_company","hubspot_delete_contact","hubspot_delete_deal","hubspot_delete_line_item","hubspot_delete_ticket","hubspot_get_appointment","hubspot_get_association_labels","hubspot_get_cart","hubspot_get_company","hubspot_get_contact","hubspot_get_deal","hubspot_get_email","hubspot_get_line_item","hubspot_get_list","hubspot_get_list_memberships","hubspot_get_marketing_event","hubspot_get_note","hubspot_get_properties","hubspot_get_quote","hubspot_get_ticket","hubspot_get_users","hubspot_list_appointments","hubspot_list_associations","hubspot_list_carts","hubspot_list_companies","hubspot_list_contacts","hubspot_list_deals","hubspot_list_emails","hubspot_list_line_items","hubspot_list_lists","hubspot_list_marketing_events","hubspot_list_notes","hubspot_list_owners","hubspot_list_quotes","hubspot_list_tickets","hubspot_remove_list_memberships","hubspot_search_companies","hubspot_search_contacts","hubspot_search_deals","hubspot_search_emails","hubspot_search_line_items","hubspot_search_notes","hubspot_search_quotes","hubspot_search_tickets","hubspot_update_appointment","hubspot_update_company","hubspot_update_contact","hubspot_update_deal","hubspot_update_line_item","hubspot_update_ticket","huggingface_chat","hunter_companies_find","hunter_discover","hunter_domain_search","hunter_email_count","hunter_email_finder","hunter_email_verifier","iam_add_user_to_group","iam_attach_role_policy","iam_attach_user_policy","iam_create_access_key","iam_create_role","iam_create_user","iam_delete_access_key","iam_delete_role","iam_delete_user","iam_detach_role_policy","iam_detach_user_policy","iam_get_role","iam_get_user","iam_list_attached_role_policies","iam_list_attached_user_policies","iam_list_groups","iam_list_policies","iam_list_roles","iam_list_users","iam_remove_user_from_group","iam_simulate_principal_policy","icypeas_find_email","icypeas_verify_email","identity_center_check_assignment_deletion_status","identity_center_check_assignment_status","identity_center_create_account_assignment","identity_center_delete_account_assignment","identity_center_describe_account","identity_center_get_group","identity_center_get_user","identity_center_list_account_assignments","identity_center_list_accounts","identity_center_list_groups","identity_center_list_instances","identity_center_list_permission_sets","image_generate","incidentio_actions_create","incidentio_actions_list","incidentio_actions_show","incidentio_actions_update","incidentio_alert_events_create","incidentio_alerts_list","incidentio_alerts_resolve","incidentio_alerts_show","incidentio_catalog_entries_list","incidentio_catalog_types_list","incidentio_custom_fields_create","incidentio_custom_fields_delete","incidentio_custom_fields_list","incidentio_custom_fields_show","incidentio_custom_fields_update","incidentio_escalation_paths_create","incidentio_escalation_paths_delete","incidentio_escalation_paths_list","incidentio_escalation_paths_show","incidentio_escalation_paths_update","incidentio_escalations_cancel","incidentio_escalations_create","incidentio_escalations_list","incidentio_escalations_show","incidentio_follow_ups_create","incidentio_follow_ups_list","incidentio_follow_ups_show","incidentio_follow_ups_update","incidentio_incident_alerts_list","incidentio_incident_memberships_create","incidentio_incident_memberships_revoke","incidentio_incident_participants_list","incidentio_incident_roles_create","incidentio_incident_roles_delete","incidentio_incident_roles_list","incidentio_incident_roles_show","incidentio_incident_roles_update","incidentio_incident_statuses_list","incidentio_incident_timestamps_list","incidentio_incident_timestamps_show","incidentio_incident_types_list","incidentio_incident_updates_list","incidentio_incidents_create","incidentio_incidents_list","incidentio_incidents_show","incidentio_incidents_update","incidentio_on_call_now","incidentio_schedule_entries_list","incidentio_schedule_overrides_create","incidentio_schedule_overrides_list","incidentio_schedules_create","incidentio_schedules_delete","incidentio_schedules_list","incidentio_schedules_show","incidentio_schedules_update","incidentio_severities_list","incidentio_teams_list","incidentio_teams_show","incidentio_users_list","incidentio_users_show","incidentio_workflows_create","incidentio_workflows_delete","incidentio_workflows_list","incidentio_workflows_show","incidentio_workflows_update","infisical_create_secret","infisical_delete_secret","infisical_get_secret","infisical_list_secrets","infisical_update_secret","instagram_delete_comment","instagram_download_media","instagram_get_account_insights","instagram_get_container_status","instagram_get_conversation_messages","instagram_get_media","instagram_get_media_insights","instagram_get_message","instagram_get_profile","instagram_get_publishing_limit","instagram_hide_comment","instagram_list_comments","instagram_list_conversations","instagram_list_media","instagram_list_stories","instagram_private_reply","instagram_publish_carousel","instagram_publish_image","instagram_publish_reel","instagram_publish_story","instagram_publish_video","instagram_reply_to_comment","instagram_send_text_message","instagram_set_comments_enabled","instantly_activate_campaign","instantly_create_campaign","instantly_create_lead","instantly_create_lead_list","instantly_delete_campaign","instantly_delete_leads","instantly_get_lead","instantly_list_campaigns","instantly_list_emails","instantly_list_lead_lists","instantly_list_leads","instantly_patch_campaign","instantly_patch_lead","instantly_pause_campaign","instantly_reply_to_email","instantly_update_lead_interest_status","intercom_assign_conversation_v2","intercom_attach_contact_to_company_v2","intercom_close_conversation_v2","intercom_create_company","intercom_create_company_v2","intercom_create_contact","intercom_create_contact_v2","intercom_create_event_v2","intercom_create_message","intercom_create_message_v2","intercom_create_note_v2","intercom_create_tag_v2","intercom_create_ticket","intercom_create_ticket_v2","intercom_delete_contact","intercom_delete_contact_v2","intercom_detach_contact_from_company_v2","intercom_get_company","intercom_get_company_v2","intercom_get_contact","intercom_get_contact_v2","intercom_get_conversation","intercom_get_conversation_v2","intercom_get_ticket","intercom_get_ticket_v2","intercom_list_admins_v2","intercom_list_companies","intercom_list_companies_v2","intercom_list_contacts","intercom_list_contacts_v2","intercom_list_conversations","intercom_list_conversations_v2","intercom_list_tags_v2","intercom_open_conversation_v2","intercom_reply_conversation","intercom_reply_conversation_v2","intercom_search_contacts","intercom_search_contacts_v2","intercom_search_conversations","intercom_search_conversations_v2","intercom_snooze_conversation_v2","intercom_tag_contact_v2","intercom_tag_conversation_v2","intercom_untag_contact_v2","intercom_update_contact","intercom_update_contact_v2","intercom_update_ticket_v2","jina_read_url","jina_search","jira_add_attachment","jira_add_comment","jira_add_watcher","jira_add_worklog","jira_assign_issue","jira_bulk_read","jira_create_issue_link","jira_delete_attachment","jira_delete_comment","jira_delete_issue","jira_delete_issue_link","jira_delete_worklog","jira_get_attachments","jira_get_comments","jira_get_fields","jira_get_project","jira_get_transitions","jira_get_users","jira_get_worklogs","jira_list_issue_types","jira_list_projects","jira_remove_watcher","jira_retrieve","jira_search_issues","jira_search_users","jira_transition_issue","jira_update","jira_update_comment","jira_update_worklog","jira_write","jsm_add_comment","jsm_add_customer","jsm_add_organization","jsm_add_participants","jsm_answer_approval","jsm_attach_form","jsm_copy_forms","jsm_create_object","jsm_create_organization","jsm_create_request","jsm_delete_form","jsm_delete_object","jsm_externalise_form","jsm_get_approvals","jsm_get_comments","jsm_get_customers","jsm_get_form","jsm_get_form_answers","jsm_get_form_structure","jsm_get_form_templates","jsm_get_issue_forms","jsm_get_object","jsm_get_object_schema","jsm_get_object_type_attributes","jsm_get_organizations","jsm_get_participants","jsm_get_queues","jsm_get_request","jsm_get_request_type_fields","jsm_get_request_types","jsm_get_requests","jsm_get_service_desks","jsm_get_sla","jsm_get_transitions","jsm_internalise_form","jsm_list_object_schemas","jsm_list_object_types","jsm_reopen_form","jsm_save_form_answers","jsm_search_objects_aql","jsm_submit_form","jsm_transition_request","jsm_update_object","jupyter_copy_content","jupyter_create_file","jupyter_create_session","jupyter_delete_content","jupyter_delete_session","jupyter_get_content","jupyter_interrupt_kernel","jupyter_list_contents","jupyter_list_kernels","jupyter_list_kernelspecs","jupyter_list_sessions","jupyter_rename_content","jupyter_restart_kernel","jupyter_start_kernel","jupyter_stop_kernel","jupyter_upload_file","kalshi_amend_order","kalshi_amend_order_v2","kalshi_cancel_order","kalshi_cancel_order_v2","kalshi_create_order","kalshi_create_order_v2","kalshi_get_balance","kalshi_get_balance_v2","kalshi_get_candlesticks","kalshi_get_candlesticks_v2","kalshi_get_event","kalshi_get_event_candlesticks","kalshi_get_event_candlesticks_v2","kalshi_get_event_v2","kalshi_get_events","kalshi_get_events_v2","kalshi_get_exchange_announcements","kalshi_get_exchange_announcements_v2","kalshi_get_exchange_schedule","kalshi_get_exchange_schedule_v2","kalshi_get_exchange_status","kalshi_get_exchange_status_v2","kalshi_get_fills","kalshi_get_fills_v2","kalshi_get_market","kalshi_get_market_v2","kalshi_get_markets","kalshi_get_markets_v2","kalshi_get_order","kalshi_get_order_v2","kalshi_get_orderbook","kalshi_get_orderbook_v2","kalshi_get_orders","kalshi_get_orders_v2","kalshi_get_positions","kalshi_get_positions_v2","kalshi_get_series_by_ticker","kalshi_get_series_by_ticker_v2","kalshi_get_series_list","kalshi_get_series_list_v2","kalshi_get_settlements","kalshi_get_settlements_v2","kalshi_get_trades","kalshi_get_trades_v2","ketch_get_consent","ketch_get_subscriptions","ketch_invoke_right","ketch_set_consent","ketch_set_subscriptions","knowledge_create_document","knowledge_delete_chunk","knowledge_delete_document","knowledge_get_connector","knowledge_get_document","knowledge_list_chunks","knowledge_list_connectors","knowledge_list_documents","knowledge_list_tags","knowledge_search","knowledge_trigger_sync","knowledge_update_chunk","knowledge_upload_chunk","knowledge_upsert_document","langsmith_create_feedback","langsmith_create_run","langsmith_create_runs_batch","langsmith_get_run","langsmith_update_run","latex_compile","latex_get_package","latex_list_fonts","latex_search_packages","launchdarkly_create_flag","launchdarkly_delete_flag","launchdarkly_get_audit_log","launchdarkly_get_flag","launchdarkly_get_flag_status","launchdarkly_list_environments","launchdarkly_list_flags","launchdarkly_list_members","launchdarkly_list_projects","launchdarkly_list_segments","launchdarkly_toggle_flag","launchdarkly_update_flag","leadmagic_company_search","leadmagic_email_to_profile","leadmagic_find_email","leadmagic_find_mobile","leadmagic_get_credits","leadmagic_profile_search","leadmagic_profile_to_email","leadmagic_role_finder","leadmagic_validate_email","lemlist_get_activities","lemlist_get_lead","lemlist_send_email","linear_add_label_to_issue","linear_add_label_to_project","linear_archive_issue","linear_archive_label","linear_archive_project","linear_create_attachment","linear_create_comment","linear_create_customer","linear_create_customer_request","linear_create_customer_status","linear_create_customer_tier","linear_create_cycle","linear_create_favorite","linear_create_issue","linear_create_issue_relation","linear_create_label","linear_create_project","linear_create_project_label","linear_create_project_milestone","linear_create_project_status","linear_create_project_update","linear_create_workflow_state","linear_delete_attachment","linear_delete_comment","linear_delete_customer","linear_delete_customer_status","linear_delete_customer_tier","linear_delete_issue","linear_delete_issue_relation","linear_delete_project","linear_delete_project_label","linear_delete_project_milestone","linear_delete_project_status","linear_get_active_cycle","linear_get_customer","linear_get_cycle","linear_get_issue","linear_get_project","linear_get_viewer","linear_list_attachments","linear_list_comments","linear_list_customer_requests","linear_list_customer_statuses","linear_list_customer_tiers","linear_list_customers","linear_list_cycles","linear_list_favorites","linear_list_issue_relations","linear_list_labels","linear_list_notifications","linear_list_project_labels","linear_list_project_milestones","linear_list_project_statuses","linear_list_project_updates","linear_list_projects","linear_list_teams","linear_list_users","linear_list_workflow_states","linear_merge_customers","linear_read_issues","linear_remove_label_from_issue","linear_remove_label_from_project","linear_search_issues","linear_unarchive_issue","linear_update_attachment","linear_update_comment","linear_update_customer","linear_update_customer_request","linear_update_customer_status","linear_update_customer_tier","linear_update_issue","linear_update_label","linear_update_notification","linear_update_project","linear_update_project_label","linear_update_project_milestone","linear_update_project_status","linear_update_workflow_state","linkedin_get_profile","linkedin_share_post","linkup_search","linq_add_participant","linq_check_imessage","linq_check_rcs","linq_create_attachment","linq_create_chat","linq_create_contact_card","linq_create_webhook_subscription","linq_delete_attachment","linq_delete_message","linq_delete_webhook_subscription","linq_edit_message","linq_get_attachment","linq_get_chat","linq_get_contact_card","linq_get_message","linq_get_webhook_subscription","linq_leave_chat","linq_list_chats","linq_list_messages","linq_list_phone_numbers","linq_list_thread","linq_list_webhook_events","linq_list_webhook_subscriptions","linq_mark_chat_read","linq_react_to_message","linq_remove_participant","linq_send_message","linq_send_voice_memo","linq_share_contact_card","linq_start_typing","linq_stop_typing","linq_update_chat","linq_update_contact_card","linq_update_webhook_subscription","llm_chat","logfire_get_token_info","logfire_get_trace","logfire_query","logfire_search_records","logs_get","logs_get_execution","logs_get_run_details","logs_query","logs_query_runs","loops_check_contact_suppression","loops_create_contact","loops_create_contact_property","loops_delete_contact","loops_find_contact","loops_get_transactional_email","loops_list_contact_properties","loops_list_mailing_lists","loops_list_transactional_emails","loops_remove_contact_suppression","loops_send_event","loops_send_transactional_email","loops_update_contact","luma_add_guests","luma_cancel_event","luma_create_event","luma_get_event","luma_get_guest","luma_get_guests","luma_list_events","luma_lookup_event","luma_send_invites","luma_update_event","luma_update_guest_status","mailchimp_add_member","mailchimp_add_member_tags","mailchimp_add_or_update_member","mailchimp_add_segment_member","mailchimp_add_subscriber_to_automation","mailchimp_archive_member","mailchimp_create_audience","mailchimp_create_batch_operation","mailchimp_create_campaign","mailchimp_create_interest","mailchimp_create_interest_category","mailchimp_create_landing_page","mailchimp_create_merge_field","mailchimp_create_segment","mailchimp_create_template","mailchimp_delete_audience","mailchimp_delete_batch_operation","mailchimp_delete_campaign","mailchimp_delete_interest","mailchimp_delete_interest_category","mailchimp_delete_landing_page","mailchimp_delete_member","mailchimp_delete_merge_field","mailchimp_delete_segment","mailchimp_delete_template","mailchimp_get_audience","mailchimp_get_audiences","mailchimp_get_automation","mailchimp_get_automations","mailchimp_get_batch_operation","mailchimp_get_batch_operations","mailchimp_get_campaign","mailchimp_get_campaign_content","mailchimp_get_campaign_report","mailchimp_get_campaign_reports","mailchimp_get_campaigns","mailchimp_get_interest","mailchimp_get_interest_categories","mailchimp_get_interest_category","mailchimp_get_interests","mailchimp_get_landing_page","mailchimp_get_landing_pages","mailchimp_get_member","mailchimp_get_member_tags","mailchimp_get_members","mailchimp_get_merge_field","mailchimp_get_merge_fields","mailchimp_get_segment","mailchimp_get_segment_members","mailchimp_get_segments","mailchimp_get_template","mailchimp_get_templates","mailchimp_pause_automation","mailchimp_publish_landing_page","mailchimp_remove_member_tags","mailchimp_remove_segment_member","mailchimp_replicate_campaign","mailchimp_schedule_campaign","mailchimp_send_campaign","mailchimp_set_campaign_content","mailchimp_start_automation","mailchimp_unarchive_member","mailchimp_unpublish_landing_page","mailchimp_unschedule_campaign","mailchimp_update_audience","mailchimp_update_campaign","mailchimp_update_interest","mailchimp_update_interest_category","mailchimp_update_landing_page","mailchimp_update_member","mailchimp_update_merge_field","mailchimp_update_segment","mailchimp_update_template","mailgun_add_list_member","mailgun_create_mailing_list","mailgun_get_domain","mailgun_get_mailing_list","mailgun_get_message","mailgun_list_domains","mailgun_list_messages","mailgun_send_message","managed_agent_archive_session","managed_agent_create_session","managed_agent_delete_session","managed_agent_get_session","managed_agent_interrupt_session","managed_agent_list_events","managed_agent_respond_custom_tool","managed_agent_respond_tool_confirmation","managed_agent_run_session","managed_agent_send_message","managed_agent_update_session","mem0_add_memories","mem0_get_memories","mem0_search_memories","memory_add","memory_delete","memory_get","memory_get_all","microsoft_ad_add_group_member","microsoft_ad_create_group","microsoft_ad_create_user","microsoft_ad_delete_group","microsoft_ad_delete_user","microsoft_ad_get_group","microsoft_ad_get_user","microsoft_ad_list_group_members","microsoft_ad_list_groups","microsoft_ad_list_users","microsoft_ad_remove_group_member","microsoft_ad_update_group","microsoft_ad_update_user","microsoft_dataverse_associate","microsoft_dataverse_create_multiple","microsoft_dataverse_create_record","microsoft_dataverse_delete_record","microsoft_dataverse_disassociate","microsoft_dataverse_download_file","microsoft_dataverse_execute_action","microsoft_dataverse_execute_function","microsoft_dataverse_fetchxml_query","microsoft_dataverse_get_entity_metadata","microsoft_dataverse_get_record","microsoft_dataverse_list_records","microsoft_dataverse_search","microsoft_dataverse_update_multiple","microsoft_dataverse_update_record","microsoft_dataverse_upload_file","microsoft_dataverse_upsert_record","microsoft_dataverse_whoami","microsoft_excel_clear_range","microsoft_excel_create_table","microsoft_excel_delete_worksheet","microsoft_excel_format_range","microsoft_excel_read","microsoft_excel_read_v2","microsoft_excel_sort_range","microsoft_excel_table_add","microsoft_excel_worksheet_add","microsoft_excel_write","microsoft_excel_write_v2","microsoft_planner_create_bucket","microsoft_planner_create_plan","microsoft_planner_create_task","microsoft_planner_delete_bucket","microsoft_planner_delete_plan","microsoft_planner_delete_task","microsoft_planner_get_plan_details","microsoft_planner_get_task_details","microsoft_planner_list_buckets","microsoft_planner_list_plans","microsoft_planner_read_bucket","microsoft_planner_read_plan","microsoft_planner_read_task","microsoft_planner_update_bucket","microsoft_planner_update_plan","microsoft_planner_update_plan_details","microsoft_planner_update_task","microsoft_planner_update_task_details","microsoft_teams_delete_channel_message","microsoft_teams_delete_chat_message","microsoft_teams_get_message","microsoft_teams_list_channel_members","microsoft_teams_list_channels","microsoft_teams_list_chat_members","microsoft_teams_list_chats","microsoft_teams_list_team_members","microsoft_teams_list_teams","microsoft_teams_read_channel","microsoft_teams_read_chat","microsoft_teams_reply_to_message","microsoft_teams_set_reaction","microsoft_teams_unset_reaction","microsoft_teams_update_channel_message","microsoft_teams_update_chat_message","microsoft_teams_write_channel","microsoft_teams_write_chat","millionverifier_get_credits","millionverifier_verify_email","mintlify_create_agent_job","mintlify_create_assistant_message","mintlify_detect_ai_prose","mintlify_get_agent_job","mintlify_get_assistant_caller_stats","mintlify_get_assistant_conversations","mintlify_get_feedback","mintlify_get_feedback_by_page","mintlify_get_page_content","mintlify_get_searches","mintlify_get_update_status","mintlify_get_views","mintlify_get_visitors","mintlify_search","mintlify_send_agent_message","mintlify_trigger_automation","mintlify_trigger_preview","mintlify_trigger_update","mistral_parser","mistral_parser_v2","mistral_parser_v3","monday_archive_item","monday_change_column_value","monday_create_board","monday_create_column","monday_create_group","monday_create_item","monday_create_subitem","monday_create_update","monday_delete_item","monday_duplicate_item","monday_get_board","monday_get_groups","monday_get_item","monday_get_items","monday_list_boards","monday_move_item_to_group","monday_search_items","monday_update_item","mongodb_delete","mongodb_execute","mongodb_insert","mongodb_introspect","mongodb_query","mongodb_update","mysql_delete","mysql_execute","mysql_insert","mysql_introspect","mysql_query","mysql_update","neo4j_create","neo4j_delete","neo4j_execute","neo4j_introspect","neo4j_merge","neo4j_query","neo4j_update","neverbounce_get_credits","neverbounce_verify_email","new_relic_create_deployment_event","new_relic_get_entity","new_relic_nrql_query","new_relic_search_entities","notion_add_database_row","notion_add_database_row_v2","notion_append_blocks","notion_append_blocks_v2","notion_create_comment","notion_create_comment_v2","notion_create_database","notion_create_database_v2","notion_create_page","notion_create_page_v2","notion_delete_block","notion_delete_block_v2","notion_list_comments","notion_list_comments_v2","notion_list_users","notion_list_users_v2","notion_query_database","notion_query_database_v2","notion_read","notion_read_database","notion_read_database_v2","notion_read_v2","notion_retrieve_block","notion_retrieve_block_children","notion_retrieve_block_children_v2","notion_retrieve_block_v2","notion_retrieve_user","notion_retrieve_user_v2","notion_search","notion_search_v2","notion_update_block","notion_update_block_v2","notion_update_page","notion_update_page_v2","notion_write","notion_write_v2","obsidian_append_active","obsidian_append_note","obsidian_append_periodic_note","obsidian_create_note","obsidian_delete_note","obsidian_execute_command","obsidian_get_active","obsidian_get_note","obsidian_get_periodic_note","obsidian_list_commands","obsidian_list_files","obsidian_open_file","obsidian_patch_active","obsidian_patch_note","obsidian_search","okta_activate_user","okta_add_user_to_group","okta_create_group","okta_create_user","okta_deactivate_user","okta_delete_group","okta_delete_user","okta_get_group","okta_get_user","okta_list_group_members","okta_list_groups","okta_list_users","okta_remove_user_from_group","okta_reset_password","okta_suspend_user","okta_unsuspend_user","okta_update_group","okta_update_user","onedrive_copy","onedrive_create_folder","onedrive_create_share_link","onedrive_delete","onedrive_download","onedrive_get_drive_info","onedrive_get_item","onedrive_list","onedrive_move","onedrive_search","onedrive_upload","onepassword_create_item","onepassword_delete_item","onepassword_get_item","onepassword_get_item_file","onepassword_get_vault","onepassword_list_items","onepassword_list_vaults","onepassword_replace_item","onepassword_resolve_secret","onepassword_update_item","openai_embeddings","openai_image","outlook_calendar_create_event","outlook_calendar_delete_event","outlook_calendar_get_event","outlook_calendar_list_events","outlook_calendar_respond","outlook_calendar_update_event","outlook_copy","outlook_create_folder","outlook_delete","outlook_draft","outlook_forward","outlook_get_attachment","outlook_list_attachments","outlook_list_folders","outlook_mark_read","outlook_mark_unread","outlook_move","outlook_read","outlook_reply","outlook_reply_all","outlook_search","outlook_send","outlook_update_message","pagerduty_add_note","pagerduty_create_incident","pagerduty_get_incident","pagerduty_get_service","pagerduty_list_escalation_policies","pagerduty_list_incident_alerts","pagerduty_list_incidents","pagerduty_list_oncalls","pagerduty_list_schedules","pagerduty_list_services","pagerduty_list_users","pagerduty_merge_incidents","pagerduty_send_event","pagerduty_snooze_incident","pagerduty_update_incident","parallel_deep_research","parallel_extract","parallel_search","pdl_autocomplete","pdl_bulk_company_enrich","pdl_bulk_person_enrich","pdl_clean_company","pdl_clean_location","pdl_clean_school","pdl_company_enrich","pdl_company_search","pdl_person_enrich","pdl_person_identify","pdl_person_search","perplexity_chat","perplexity_search","persona_approve_inquiry","persona_create_account","persona_create_inquiry","persona_create_report","persona_decline_inquiry","persona_expire_inquiry","persona_generate_inquiry_link","persona_get_account","persona_get_case","persona_get_document","persona_get_inquiry","persona_get_report","persona_get_verification","persona_import_accounts","persona_list_accounts","persona_list_cases","persona_list_inquiries","persona_list_inquiry_templates","persona_list_reports","persona_mark_inquiry_for_review","persona_print_inquiry_pdf","persona_redact_account","persona_redact_inquiry","persona_resume_inquiry","persona_update_account","persona_update_inquiry","pinecone_delete_vectors","pinecone_describe_index","pinecone_describe_index_stats","pinecone_fetch","pinecone_generate_embeddings","pinecone_list_indexes","pinecone_list_vector_ids","pinecone_search_text","pinecone_search_vector","pinecone_update_vector","pinecone_upsert_text","pipedrive_create_activity","pipedrive_create_deal","pipedrive_create_lead","pipedrive_create_project","pipedrive_delete_lead","pipedrive_get_activities","pipedrive_get_all_deals","pipedrive_get_deal","pipedrive_get_files","pipedrive_get_leads","pipedrive_get_mail_messages","pipedrive_get_mail_thread","pipedrive_get_pipeline_deals","pipedrive_get_pipelines","pipedrive_get_projects","pipedrive_update_activity","pipedrive_update_deal","pipedrive_update_lead","polymarket_get_activity","polymarket_get_event","polymarket_get_events","polymarket_get_holders","polymarket_get_last_trade_price","polymarket_get_leaderboard","polymarket_get_market","polymarket_get_markets","polymarket_get_midpoint","polymarket_get_orderbook","polymarket_get_positions","polymarket_get_price","polymarket_get_price_history","polymarket_get_series","polymarket_get_series_by_id","polymarket_get_spread","polymarket_get_tags","polymarket_get_tick_size","polymarket_get_trades","polymarket_search","postgresql_delete","postgresql_execute","postgresql_insert","postgresql_introspect","postgresql_query","postgresql_update","posthog_batch_events","posthog_capture_event","posthog_create_annotation","posthog_create_cohort","posthog_create_dashboard","posthog_create_experiment","posthog_create_feature_flag","posthog_create_insight","posthog_create_survey","posthog_delete_feature_flag","posthog_delete_person","posthog_delete_survey","posthog_evaluate_flags","posthog_get_cohort","posthog_get_dashboard","posthog_get_event_definition","posthog_get_experiment","posthog_get_feature_flag","posthog_get_insight","posthog_get_organization","posthog_get_person","posthog_get_project","posthog_get_property_definition","posthog_get_session_recording","posthog_get_survey","posthog_list_actions","posthog_list_annotations","posthog_list_cohorts","posthog_list_dashboards","posthog_list_event_definitions","posthog_list_experiments","posthog_list_feature_flags","posthog_list_insights","posthog_list_organizations","posthog_list_persons","posthog_list_projects","posthog_list_property_definitions","posthog_list_recording_playlists","posthog_list_session_recordings","posthog_list_surveys","posthog_query","posthog_update_cohort","posthog_update_event_definition","posthog_update_experiment","posthog_update_feature_flag","posthog_update_insight","posthog_update_property_definition","posthog_update_survey","profound_bot_logs","profound_bots_report","profound_category_assets","profound_category_personas","profound_category_prompts","profound_category_tags","profound_category_topics","profound_citation_prompts","profound_citations_report","profound_list_assets","profound_list_categories","profound_list_domains","profound_list_models","profound_list_optimizations","profound_list_personas","profound_list_regions","profound_optimization_analysis","profound_prompt_answers","profound_prompt_volume","profound_query_fanouts","profound_raw_logs","profound_referrals_report","profound_sentiment_report","profound_visibility_report","prospeo_account_information","prospeo_bulk_enrich_company","prospeo_bulk_enrich_person","prospeo_enrich_company","prospeo_enrich_person","prospeo_search_company","prospeo_search_person","prospeo_search_suggestions","pulse_parser","pulse_parser_v2","qdrant_fetch_points","qdrant_search_vector","qdrant_upsert_points","quartr_get_audio","quartr_get_company","quartr_get_event","quartr_get_event_summary","quartr_get_report","quartr_get_slide_deck","quartr_get_transcript","quartr_list_audio","quartr_list_companies","quartr_list_document_types","quartr_list_documents","quartr_list_event_types","quartr_list_events","quartr_list_live_events","quartr_list_reports","quartr_list_slide_decks","quartr_list_transcripts","quiver_image_to_svg","quiver_list_models","quiver_text_to_svg","railway_create_environment","railway_create_project","railway_create_service","railway_delete_environment","railway_delete_project","railway_delete_service","railway_delete_variable","railway_deploy_service","railway_get_deployment","railway_get_deployment_logs","railway_get_project","railway_list_deployments","railway_list_project_members","railway_list_projects","railway_list_variables","railway_restart_deployment","railway_rollback_deployment","railway_transfer_project","railway_update_project","railway_upsert_variable","rb2b_credit_check","rb2b_email_to_activity","rb2b_hem_to_best_linkedin","rb2b_hem_to_business_profile","rb2b_hem_to_linkedin","rb2b_hem_to_maid","rb2b_ip_to_company","rb2b_ip_to_hem","rb2b_ip_to_maid","rb2b_linkedin_slug_search","rb2b_linkedin_to_best_personal_email","rb2b_linkedin_to_business_profile","rb2b_linkedin_to_hashed_emails","rb2b_linkedin_to_mobile_phone","rb2b_linkedin_to_personal_email","rds_delete","rds_execute","rds_insert","rds_introspect","rds_query","rds_update","reddit_delete","reddit_edit","reddit_get_comments","reddit_get_controversial","reddit_get_info","reddit_get_me","reddit_get_messages","reddit_get_posts","reddit_get_saved","reddit_get_subreddit_info","reddit_get_subreddit_rules","reddit_get_user","reddit_get_user_comments","reddit_get_user_posts","reddit_hide","reddit_hot_posts","reddit_list_my_subreddits","reddit_lock","reddit_mark_all_read","reddit_mark_read","reddit_marknsfw","reddit_mod_approve","reddit_mod_distinguish","reddit_mod_remove","reddit_mod_sticky","reddit_reply","reddit_report","reddit_save","reddit_search","reddit_search_subreddits","reddit_send_message","reddit_submit_post","reddit_subscribe","reddit_unhide","reddit_unlock","reddit_unmarknsfw","reddit_unsave","reddit_vote","redis_command","redis_delete","redis_exists","redis_expire","redis_get","redis_hdel","redis_hget","redis_hgetall","redis_hset","redis_incr","redis_incrby","redis_keys","redis_llen","redis_lpop","redis_lpush","redis_lrange","redis_persist","redis_rpop","redis_rpush","redis_set","redis_setnx","redis_ttl","reducto_parser","reducto_parser_v2","resend_cancel_email","resend_create_audience","resend_create_broadcast","resend_create_contact","resend_delete_audience","resend_delete_contact","resend_get_audience","resend_get_broadcast","resend_get_contact","resend_get_email","resend_list_audiences","resend_list_contacts","resend_list_domains","resend_send","resend_send_broadcast","resend_update_contact","revenuecat_create_purchase","revenuecat_defer_google_subscription","revenuecat_delete_customer","revenuecat_get_customer","revenuecat_grant_entitlement","revenuecat_list_offerings","revenuecat_refund_google_subscription","revenuecat_revoke_entitlement","revenuecat_revoke_google_subscription","revenuecat_update_subscriber_attributes","rippling_bulk_create_custom_object_records","rippling_bulk_delete_custom_object_records","rippling_bulk_update_custom_object_records","rippling_create_business_partner","rippling_create_business_partner_group","rippling_create_custom_app","rippling_create_custom_object","rippling_create_custom_object_field","rippling_create_custom_object_record","rippling_create_custom_page","rippling_create_custom_setting","rippling_create_department","rippling_create_draft_hires","rippling_create_object_category","rippling_create_title","rippling_create_work_location","rippling_delete_business_partner","rippling_delete_business_partner_group","rippling_delete_custom_app","rippling_delete_custom_object","rippling_delete_custom_object_field","rippling_delete_custom_object_record","rippling_delete_custom_page","rippling_delete_custom_setting","rippling_delete_object_category","rippling_delete_title","rippling_delete_work_location","rippling_get_business_partner","rippling_get_business_partner_group","rippling_get_current_user","rippling_get_custom_app","rippling_get_custom_object","rippling_get_custom_object_field","rippling_get_custom_object_record","rippling_get_custom_object_record_by_external_id","rippling_get_custom_page","rippling_get_custom_setting","rippling_get_department","rippling_get_employment_type","rippling_get_job_function","rippling_get_object_category","rippling_get_report_run","rippling_get_supergroup","rippling_get_team","rippling_get_title","rippling_get_user","rippling_get_work_location","rippling_get_worker","rippling_list_business_partner_groups","rippling_list_business_partners","rippling_list_companies","rippling_list_custom_apps","rippling_list_custom_fields","rippling_list_custom_object_fields","rippling_list_custom_object_records","rippling_list_custom_objects","rippling_list_custom_pages","rippling_list_custom_settings","rippling_list_departments","rippling_list_employment_types","rippling_list_entitlements","rippling_list_job_functions","rippling_list_object_categories","rippling_list_supergroup_exclusion_members","rippling_list_supergroup_inclusion_members","rippling_list_supergroup_members","rippling_list_supergroups","rippling_list_teams","rippling_list_titles","rippling_list_users","rippling_list_work_locations","rippling_list_workers","rippling_query_custom_object_records","rippling_trigger_report_run","rippling_update_custom_app","rippling_update_custom_object","rippling_update_custom_object_field","rippling_update_custom_object_record","rippling_update_custom_page","rippling_update_custom_setting","rippling_update_department","rippling_update_object_category","rippling_update_supergroup_exclusion_members","rippling_update_supergroup_inclusion_members","rippling_update_title","rippling_update_work_location","rocketlane_add_field_option","rocketlane_add_project_members","rocketlane_add_task_assignees","rocketlane_add_task_dependencies","rocketlane_add_task_followers","rocketlane_archive_project","rocketlane_assign_placeholders","rocketlane_create_field","rocketlane_create_phase","rocketlane_create_project","rocketlane_create_space","rocketlane_create_space_document","rocketlane_create_task","rocketlane_create_time_entry","rocketlane_create_time_off","rocketlane_delete_field","rocketlane_delete_phase","rocketlane_delete_project","rocketlane_delete_space","rocketlane_delete_space_document","rocketlane_delete_task","rocketlane_delete_time_entry","rocketlane_delete_time_off","rocketlane_get_field","rocketlane_get_invoice","rocketlane_get_invoice_line_items","rocketlane_get_invoice_payments","rocketlane_get_phase","rocketlane_get_project","rocketlane_get_space","rocketlane_get_space_document","rocketlane_get_task","rocketlane_get_time_entry","rocketlane_get_time_off","rocketlane_get_user","rocketlane_import_template","rocketlane_list_fields","rocketlane_list_invoices","rocketlane_list_phases","rocketlane_list_placeholders","rocketlane_list_projects","rocketlane_list_resource_allocations","rocketlane_list_space_documents","rocketlane_list_spaces","rocketlane_list_tasks","rocketlane_list_time_entries","rocketlane_list_time_entry_categories","rocketlane_list_time_offs","rocketlane_list_users","rocketlane_move_task_to_phase","rocketlane_remove_project_members","rocketlane_remove_task_assignees","rocketlane_remove_task_dependencies","rocketlane_remove_task_followers","rocketlane_search_time_entries","rocketlane_unassign_placeholders","rocketlane_update_field","rocketlane_update_field_option","rocketlane_update_phase","rocketlane_update_project","rocketlane_update_space","rocketlane_update_space_document","rocketlane_update_task","rocketlane_update_time_entry","rootly_acknowledge_alert","rootly_add_incident_event","rootly_add_subscribers","rootly_assign_incident_role","rootly_create_action_item","rootly_create_alert","rootly_create_incident","rootly_create_status_page_event","rootly_delete_action_item","rootly_delete_incident","rootly_escalate_alert","rootly_get_alert","rootly_get_incident","rootly_list_action_items","rootly_list_alerts","rootly_list_causes","rootly_list_environments","rootly_list_escalation_policies","rootly_list_functionalities","rootly_list_incident_events","rootly_list_incident_roles","rootly_list_incident_types","rootly_list_incidents","rootly_list_on_calls","rootly_list_playbooks","rootly_list_retrospectives","rootly_list_schedules","rootly_list_services","rootly_list_severities","rootly_list_teams","rootly_list_users","rootly_mitigate_incident","rootly_remove_subscribers","rootly_resolve_alert","rootly_resolve_incident","rootly_run_workflow","rootly_snooze_alert","rootly_unassign_incident_role","rootly_update_action_item","rootly_update_alert","rootly_update_incident","s3_copy_object","s3_create_bucket","s3_delete_bucket","s3_delete_object","s3_delete_objects","s3_get_object","s3_head_object","s3_list_buckets","s3_list_objects","s3_presigned_url","s3_put_object","salesforce_create_account","salesforce_create_case","salesforce_create_contact","salesforce_create_custom_field","salesforce_create_custom_object","salesforce_create_lead","salesforce_create_opportunity","salesforce_create_task","salesforce_delete_account","salesforce_delete_case","salesforce_delete_contact","salesforce_delete_custom_field","salesforce_delete_lead","salesforce_delete_opportunity","salesforce_delete_task","salesforce_describe_object","salesforce_get_accounts","salesforce_get_cases","salesforce_get_contacts","salesforce_get_dashboard","salesforce_get_leads","salesforce_get_opportunities","salesforce_get_report","salesforce_get_tasks","salesforce_list_dashboards","salesforce_list_objects","salesforce_list_report_types","salesforce_list_reports","salesforce_query","salesforce_query_more","salesforce_refresh_dashboard","salesforce_run_report","salesforce_tooling_query","salesforce_update_account","salesforce_update_case","salesforce_update_contact","salesforce_update_custom_field","salesforce_update_lead","salesforce_update_opportunity","salesforce_update_task","sap_concur_approve_expense_report","sap_concur_associate_attendees","sap_concur_create_cash_advance","sap_concur_create_expected_expense","sap_concur_create_expense_report","sap_concur_create_list_item","sap_concur_create_purchase_request","sap_concur_create_quick_expense","sap_concur_create_quick_expense_with_image","sap_concur_create_report_comment","sap_concur_create_travel_request","sap_concur_create_user","sap_concur_delete_expected_expense","sap_concur_delete_expense","sap_concur_delete_expense_report","sap_concur_delete_list_item","sap_concur_delete_travel_request","sap_concur_delete_user","sap_concur_get_allocation","sap_concur_get_budget","sap_concur_get_cash_advance","sap_concur_get_expected_expense","sap_concur_get_expense","sap_concur_get_expense_report","sap_concur_get_itemizations","sap_concur_get_itinerary","sap_concur_get_list","sap_concur_get_list_item","sap_concur_get_purchase_request","sap_concur_get_receipt","sap_concur_get_receipt_status","sap_concur_get_request_cash_advance","sap_concur_get_travel_profile","sap_concur_get_travel_request","sap_concur_get_user","sap_concur_issue_cash_advance","sap_concur_list_allocations","sap_concur_list_attendee_associations","sap_concur_list_budget_categories","sap_concur_list_budgets","sap_concur_list_exceptions","sap_concur_list_expected_expenses","sap_concur_list_expense_reports","sap_concur_list_expenses","sap_concur_list_itineraries","sap_concur_list_list_items","sap_concur_list_lists","sap_concur_list_receipts","sap_concur_list_report_comments","sap_concur_list_reports_to_approve","sap_concur_list_travel_profiles_summary","sap_concur_list_travel_request_comments","sap_concur_list_travel_requests","sap_concur_list_users","sap_concur_move_travel_request","sap_concur_recall_expense_report","sap_concur_remove_all_attendees","sap_concur_search_locations","sap_concur_search_users","sap_concur_send_back_expense_report","sap_concur_submit_expense_report","sap_concur_update_allocation","sap_concur_update_expected_expense","sap_concur_update_expense","sap_concur_update_expense_report","sap_concur_update_list_item","sap_concur_update_travel_request","sap_concur_update_user","sap_concur_upload_exchange_rates","sap_concur_upload_receipt_image","sap_s4hana_create_business_partner","sap_s4hana_create_purchase_order","sap_s4hana_create_purchase_requisition","sap_s4hana_create_sales_order","sap_s4hana_delete_sales_order","sap_s4hana_get_billing_document","sap_s4hana_get_business_partner","sap_s4hana_get_customer","sap_s4hana_get_inbound_delivery","sap_s4hana_get_material_document","sap_s4hana_get_outbound_delivery","sap_s4hana_get_product","sap_s4hana_get_purchase_order","sap_s4hana_get_purchase_requisition","sap_s4hana_get_sales_order","sap_s4hana_get_supplier","sap_s4hana_get_supplier_invoice","sap_s4hana_list_billing_documents","sap_s4hana_list_business_partners","sap_s4hana_list_customers","sap_s4hana_list_inbound_deliveries","sap_s4hana_list_material_documents","sap_s4hana_list_material_stock","sap_s4hana_list_outbound_deliveries","sap_s4hana_list_products","sap_s4hana_list_purchase_orders","sap_s4hana_list_purchase_requisitions","sap_s4hana_list_sales_orders","sap_s4hana_list_supplier_invoices","sap_s4hana_list_suppliers","sap_s4hana_odata_query","sap_s4hana_update_business_partner","sap_s4hana_update_customer","sap_s4hana_update_product","sap_s4hana_update_purchase_order","sap_s4hana_update_purchase_requisition","sap_s4hana_update_sales_order","sap_s4hana_update_supplier","search_tool","secrets_manager_create_secret","secrets_manager_delete_secret","secrets_manager_describe_secret","secrets_manager_get_secret","secrets_manager_list_secrets","secrets_manager_restore_secret","secrets_manager_rotate_secret","secrets_manager_tag_resource","secrets_manager_untag_resource","secrets_manager_update_secret","sendblue_evaluate_service","sendblue_get_message","sendblue_send_group_message","sendblue_send_message","sendblue_send_typing_indicator","sendgrid_add_contact","sendgrid_add_contacts_to_list","sendgrid_create_list","sendgrid_create_template","sendgrid_create_template_version","sendgrid_delete_contacts","sendgrid_delete_list","sendgrid_delete_template","sendgrid_get_contact","sendgrid_get_list","sendgrid_get_template","sendgrid_list_all_lists","sendgrid_list_templates","sendgrid_remove_contacts_from_list","sendgrid_search_contacts","sendgrid_send_mail","sentry_events_get","sentry_events_list","sentry_issues_get","sentry_issues_list","sentry_issues_update","sentry_projects_create","sentry_projects_get","sentry_projects_list","sentry_projects_update","sentry_releases_create","sentry_releases_deploy","sentry_releases_list","sentry_teams_list","serper_search","servicenow_aggregate","servicenow_create_record","servicenow_delete_record","servicenow_download_attachment","servicenow_list_attachments","servicenow_read_record","servicenow_update_record","servicenow_upload_attachment","ses_create_configuration_set","ses_create_email_identity","ses_create_template","ses_delete_email_identity","ses_delete_suppressed_destination","ses_delete_template","ses_get_account","ses_get_email_identity","ses_get_suppressed_destination","ses_get_template","ses_list_identities","ses_list_suppressed_destinations","ses_list_templates","ses_put_suppressed_destination","ses_send_bulk_email","ses_send_custom_verification_email","ses_send_email","ses_send_templated_email","ses_update_template","sftp_delete","sftp_download","sftp_list","sftp_mkdir","sftp_upload","sharepoint_add_list_items","sharepoint_create_list","sharepoint_create_page","sharepoint_delete_file","sharepoint_delete_list_item","sharepoint_delete_page","sharepoint_download_file","sharepoint_get_drive_item","sharepoint_get_list","sharepoint_get_list_item","sharepoint_list_sites","sharepoint_publish_page","sharepoint_read_page","sharepoint_update_list","sharepoint_update_page","sharepoint_upload_file","shopify_adjust_inventory","shopify_cancel_order","shopify_create_customer","shopify_create_fulfillment","shopify_create_product","shopify_delete_customer","shopify_delete_product","shopify_get_collection","shopify_get_customer","shopify_get_inventory_level","shopify_get_order","shopify_get_product","shopify_list_collections","shopify_list_customers","shopify_list_inventory_items","shopify_list_locations","shopify_list_orders","shopify_list_products","shopify_update_customer","shopify_update_order","shopify_update_product","similarweb_bounce_rate","similarweb_page_views","similarweb_pages_per_visit","similarweb_traffic_visits","similarweb_visit_duration","similarweb_website_overview","sixtyfour_enrich_company","sixtyfour_enrich_lead","sixtyfour_find_email","sixtyfour_find_phone","slack_add_reaction","slack_archive_conversation","slack_canvas","slack_create_channel_canvas","slack_create_conversation","slack_delete_canvas","slack_delete_message","slack_delete_scheduled_message","slack_download","slack_edit_canvas","slack_ephemeral_message","slack_get_canvas","slack_get_channel_history","slack_get_channel_info","slack_get_message","slack_get_permalink","slack_get_thread","slack_get_thread_replies","slack_get_user","slack_get_user_presence","slack_invite_to_conversation","slack_list_canvases","slack_list_channels","slack_list_members","slack_list_scheduled_messages","slack_list_users","slack_lookup_canvas_sections","slack_message","slack_message_reader","slack_open_view","slack_publish_view","slack_push_view","slack_remove_reaction","slack_rename_conversation","slack_schedule_message","slack_set_conversation_purpose","slack_set_conversation_topic","slack_set_status","slack_set_suggested_prompts","slack_set_title","slack_update_message","slack_update_view","smartlead_add_email_accounts_to_campaign","smartlead_add_leads_to_campaign","smartlead_create_campaign","smartlead_create_lead_list","smartlead_delete_campaign","smartlead_delete_campaign_webhook","smartlead_delete_lead_from_campaign","smartlead_delete_lead_list","smartlead_duplicate_campaign","smartlead_export_campaign_leads","smartlead_get_campaign","smartlead_get_campaign_analytics","smartlead_get_campaign_analytics_by_date","smartlead_get_campaign_lead_statistics","smartlead_get_campaign_mailbox_statistics","smartlead_get_campaign_sequences","smartlead_get_campaign_statistics","smartlead_get_campaign_top_level_analytics_by_date","smartlead_get_campaign_webhook_summary","smartlead_get_lead_by_email","smartlead_get_lead_by_id","smartlead_get_lead_list","smartlead_get_lead_message_history","smartlead_list_campaign_email_accounts","smartlead_list_campaign_leads","smartlead_list_campaign_webhooks","smartlead_list_campaigns","smartlead_list_clients","smartlead_list_email_accounts","smartlead_list_inbox_replies","smartlead_list_lead_activities","smartlead_list_lead_categories","smartlead_list_lead_lists","smartlead_mark_lead_complete","smartlead_pause_lead","smartlead_remove_email_accounts_from_campaign","smartlead_resume_lead","smartlead_save_campaign_sequences","smartlead_unsubscribe_lead_from_campaign","smartlead_unsubscribe_lead_globally","smartlead_update_campaign_schedule","smartlead_update_campaign_settings","smartlead_update_campaign_status","smartlead_update_lead","smartlead_update_lead_category","smartlead_update_lead_list","smartlead_upsert_campaign_webhook","sms_send","smtp_send_mail","snowflake_alter_warehouse","snowflake_call_procedure","snowflake_cancel_statement","snowflake_cancel_task_run","snowflake_delete_rows","snowflake_execute_sql","snowflake_get_statement","snowflake_get_task","snowflake_get_task_run","snowflake_get_task_run_output","snowflake_get_warehouse","snowflake_insert_rows","snowflake_introspect_schema","snowflake_list_copy_history","snowflake_list_databases","snowflake_list_query_history","snowflake_list_schemas","snowflake_list_tables","snowflake_list_task_runs","snowflake_list_tasks","snowflake_list_warehouses","snowflake_load_data","snowflake_resume_task","snowflake_resume_warehouse","snowflake_run_task","snowflake_suspend_task","snowflake_suspend_warehouse","snowflake_unload_data","snowflake_update_rows","snowflake_upsert_rows","sportmonks_core_get_cities","sportmonks_core_get_city","sportmonks_core_get_continent","sportmonks_core_get_continents","sportmonks_core_get_countries","sportmonks_core_get_country","sportmonks_core_get_entity_filters","sportmonks_core_get_my_usage","sportmonks_core_get_region","sportmonks_core_get_regions","sportmonks_core_get_timezones","sportmonks_core_get_type","sportmonks_core_get_type_by_entity","sportmonks_core_get_types","sportmonks_core_search_cities","sportmonks_core_search_countries","sportmonks_core_search_regions","sportmonks_football_expected_by_player","sportmonks_football_expected_by_team","sportmonks_football_get_all_commentaries","sportmonks_football_get_all_fixtures","sportmonks_football_get_all_players","sportmonks_football_get_all_rivals","sportmonks_football_get_all_teams","sportmonks_football_get_all_transfer_rumours","sportmonks_football_get_all_transfers","sportmonks_football_get_brackets_by_season","sportmonks_football_get_coach","sportmonks_football_get_coaches","sportmonks_football_get_coaches_by_country","sportmonks_football_get_commentaries_by_fixture","sportmonks_football_get_current_leagues_by_team","sportmonks_football_get_expected_lineups_by_player","sportmonks_football_get_expected_lineups_by_team","sportmonks_football_get_extended_team_squad","sportmonks_football_get_fixture","sportmonks_football_get_fixtures_by_date","sportmonks_football_get_fixtures_by_date_range","sportmonks_football_get_fixtures_by_date_range_for_team","sportmonks_football_get_fixtures_by_ids","sportmonks_football_get_grouped_standings_by_round","sportmonks_football_get_head_to_head","sportmonks_football_get_inplay_livescores","sportmonks_football_get_latest_coaches","sportmonks_football_get_latest_fixtures","sportmonks_football_get_latest_livescores","sportmonks_football_get_latest_players","sportmonks_football_get_latest_totw","sportmonks_football_get_latest_transfers","sportmonks_football_get_league","sportmonks_football_get_leagues","sportmonks_football_get_leagues_by_country","sportmonks_football_get_leagues_by_date","sportmonks_football_get_leagues_by_team","sportmonks_football_get_live_leagues","sportmonks_football_get_live_probabilities","sportmonks_football_get_live_probabilities_by_fixture","sportmonks_football_get_live_standings_by_league","sportmonks_football_get_livescores","sportmonks_football_get_match_facts","sportmonks_football_get_match_facts_by_date_range","sportmonks_football_get_match_facts_by_fixture","sportmonks_football_get_match_facts_by_league","sportmonks_football_get_past_fixtures_by_tv_station","sportmonks_football_get_player","sportmonks_football_get_players_by_country","sportmonks_football_get_postmatch_news","sportmonks_football_get_postmatch_news_by_season","sportmonks_football_get_predictability_by_league","sportmonks_football_get_prematch_news","sportmonks_football_get_prematch_news_by_season","sportmonks_football_get_prematch_news_upcoming","sportmonks_football_get_probabilities","sportmonks_football_get_probabilities_by_fixture","sportmonks_football_get_referee","sportmonks_football_get_referees","sportmonks_football_get_referees_by_country","sportmonks_football_get_referees_by_season","sportmonks_football_get_rivals_by_team","sportmonks_football_get_round","sportmonks_football_get_round_statistics","sportmonks_football_get_rounds","sportmonks_football_get_rounds_by_season","sportmonks_football_get_schedules_by_season","sportmonks_football_get_schedules_by_season_and_team","sportmonks_football_get_schedules_by_team","sportmonks_football_get_season","sportmonks_football_get_seasons","sportmonks_football_get_seasons_by_team","sportmonks_football_get_stage","sportmonks_football_get_stage_statistics","sportmonks_football_get_stages","sportmonks_football_get_stages_by_season","sportmonks_football_get_standing_corrections_by_season","sportmonks_football_get_standings","sportmonks_football_get_standings_by_round","sportmonks_football_get_standings_by_season","sportmonks_football_get_state","sportmonks_football_get_states","sportmonks_football_get_team","sportmonks_football_get_team_rankings","sportmonks_football_get_team_rankings_by_date","sportmonks_football_get_team_rankings_by_team","sportmonks_football_get_team_squad","sportmonks_football_get_team_squad_by_season","sportmonks_football_get_teams_by_country","sportmonks_football_get_teams_by_season","sportmonks_football_get_topscorers_by_season","sportmonks_football_get_topscorers_by_stage","sportmonks_football_get_totw","sportmonks_football_get_totw_by_round","sportmonks_football_get_transfer","sportmonks_football_get_transfer_rumour","sportmonks_football_get_transfer_rumours_between_dates","sportmonks_football_get_transfer_rumours_by_player","sportmonks_football_get_transfer_rumours_by_team","sportmonks_football_get_transfers_between_dates","sportmonks_football_get_transfers_by_player","sportmonks_football_get_transfers_by_team","sportmonks_football_get_tv_station","sportmonks_football_get_tv_stations","sportmonks_football_get_tv_stations_by_fixture","sportmonks_football_get_upcoming_fixtures_by_market","sportmonks_football_get_upcoming_fixtures_by_tv_station","sportmonks_football_get_value_bets","sportmonks_football_get_value_bets_by_fixture","sportmonks_football_get_venue","sportmonks_football_get_venues","sportmonks_football_get_venues_by_season","sportmonks_football_search_coaches","sportmonks_football_search_fixtures","sportmonks_football_search_leagues","sportmonks_football_search_players","sportmonks_football_search_referees","sportmonks_football_search_rounds","sportmonks_football_search_seasons","sportmonks_football_search_stages","sportmonks_football_search_teams","sportmonks_football_search_venues","sportmonks_motorsport_get_all_fixtures","sportmonks_motorsport_get_current_leagues_by_team","sportmonks_motorsport_get_driver","sportmonks_motorsport_get_driver_standings","sportmonks_motorsport_get_driver_standings_by_season","sportmonks_motorsport_get_drivers","sportmonks_motorsport_get_drivers_by_country","sportmonks_motorsport_get_drivers_by_season","sportmonks_motorsport_get_fixture","sportmonks_motorsport_get_fixtures_by_date","sportmonks_motorsport_get_fixtures_by_date_range","sportmonks_motorsport_get_fixtures_by_ids","sportmonks_motorsport_get_laps_by_fixture","sportmonks_motorsport_get_laps_by_fixture_and_driver","sportmonks_motorsport_get_laps_by_fixture_and_lap","sportmonks_motorsport_get_latest_laps_by_fixture","sportmonks_motorsport_get_latest_pitstops_by_fixture","sportmonks_motorsport_get_latest_stints_by_fixture","sportmonks_motorsport_get_latest_updated_drivers","sportmonks_motorsport_get_latest_updated_fixtures","sportmonks_motorsport_get_league","sportmonks_motorsport_get_leagues","sportmonks_motorsport_get_leagues_by_country","sportmonks_motorsport_get_leagues_by_date","sportmonks_motorsport_get_leagues_by_live","sportmonks_motorsport_get_leagues_by_team","sportmonks_motorsport_get_livescores","sportmonks_motorsport_get_pitstops_by_fixture","sportmonks_motorsport_get_pitstops_by_fixture_and_driver","sportmonks_motorsport_get_pitstops_by_fixture_and_lap","sportmonks_motorsport_get_race_results_by_season_and_driver","sportmonks_motorsport_get_race_results_by_season_and_team","sportmonks_motorsport_get_schedules_by_season","sportmonks_motorsport_get_season","sportmonks_motorsport_get_seasons","sportmonks_motorsport_get_stage","sportmonks_motorsport_get_stages","sportmonks_motorsport_get_stages_by_season","sportmonks_motorsport_get_state","sportmonks_motorsport_get_states","sportmonks_motorsport_get_stints_by_fixture","sportmonks_motorsport_get_stints_by_fixture_and_driver","sportmonks_motorsport_get_stints_by_fixture_and_stint","sportmonks_motorsport_get_team","sportmonks_motorsport_get_team_standings","sportmonks_motorsport_get_team_standings_by_season","sportmonks_motorsport_get_teams","sportmonks_motorsport_get_teams_by_country","sportmonks_motorsport_get_teams_by_season","sportmonks_motorsport_get_venue","sportmonks_motorsport_get_venues","sportmonks_motorsport_get_venues_by_season","sportmonks_motorsport_search_drivers","sportmonks_motorsport_search_leagues","sportmonks_motorsport_search_stages","sportmonks_motorsport_search_teams","sportmonks_motorsport_search_venues","sportmonks_odds_get_all_historical_odds","sportmonks_odds_get_all_inplay_odds","sportmonks_odds_get_all_pre_match_odds","sportmonks_odds_get_all_premium_odds","sportmonks_odds_get_bookmaker","sportmonks_odds_get_bookmaker_event_ids_by_fixture","sportmonks_odds_get_bookmakers","sportmonks_odds_get_bookmakers_by_fixture","sportmonks_odds_get_inplay_odds_by_fixture","sportmonks_odds_get_inplay_odds_by_fixture_and_bookmaker","sportmonks_odds_get_inplay_odds_by_fixture_and_market","sportmonks_odds_get_last_updated_inplay_odds","sportmonks_odds_get_last_updated_pre_match_odds","sportmonks_odds_get_market","sportmonks_odds_get_markets","sportmonks_odds_get_pre_match_odds_by_fixture","sportmonks_odds_get_pre_match_odds_by_fixture_and_bookmaker","sportmonks_odds_get_pre_match_odds_by_fixture_and_market","sportmonks_odds_get_premium_odds_by_fixture","sportmonks_odds_get_premium_odds_by_fixture_and_bookmaker","sportmonks_odds_get_premium_odds_by_fixture_and_market","sportmonks_odds_get_updated_historical_odds_between","sportmonks_odds_get_updated_premium_odds_between","sportmonks_odds_search_bookmakers","sportmonks_odds_search_markets","spotify_add_playlist_cover","spotify_add_to_queue","spotify_add_tracks_to_playlist","spotify_check_following","spotify_check_playlist_followers","spotify_check_saved_albums","spotify_check_saved_audiobooks","spotify_check_saved_episodes","spotify_check_saved_shows","spotify_check_saved_tracks","spotify_create_playlist","spotify_follow_artists","spotify_follow_playlist","spotify_get_album","spotify_get_album_tracks","spotify_get_albums","spotify_get_artist","spotify_get_artist_albums","spotify_get_artist_top_tracks","spotify_get_artists","spotify_get_audiobook","spotify_get_audiobook_chapters","spotify_get_audiobooks","spotify_get_categories","spotify_get_current_user","spotify_get_currently_playing","spotify_get_devices","spotify_get_episode","spotify_get_episodes","spotify_get_followed_artists","spotify_get_markets","spotify_get_new_releases","spotify_get_playback_state","spotify_get_playlist","spotify_get_playlist_cover","spotify_get_playlist_tracks","spotify_get_queue","spotify_get_recently_played","spotify_get_saved_albums","spotify_get_saved_audiobooks","spotify_get_saved_episodes","spotify_get_saved_shows","spotify_get_saved_tracks","spotify_get_show","spotify_get_show_episodes","spotify_get_shows","spotify_get_top_artists","spotify_get_top_tracks","spotify_get_track","spotify_get_tracks","spotify_get_user_playlists","spotify_get_user_profile","spotify_pause","spotify_play","spotify_remove_saved_albums","spotify_remove_saved_audiobooks","spotify_remove_saved_episodes","spotify_remove_saved_shows","spotify_remove_saved_tracks","spotify_remove_tracks_from_playlist","spotify_reorder_playlist_items","spotify_replace_playlist_items","spotify_save_albums","spotify_save_audiobooks","spotify_save_episodes","spotify_save_shows","spotify_save_tracks","spotify_search","spotify_seek","spotify_set_repeat","spotify_set_shuffle","spotify_set_volume","spotify_skip_next","spotify_skip_previous","spotify_transfer_playback","spotify_unfollow_artists","spotify_unfollow_playlist","spotify_update_playlist","sqs_send","square_batch_retrieve_inventory_counts","square_cancel_invoice","square_cancel_payment","square_complete_payment","square_create_catalog_image","square_create_customer","square_create_invoice","square_create_order","square_create_payment","square_delete_catalog_object","square_delete_customer","square_delete_invoice","square_get_catalog_object","square_get_customer","square_get_invoice","square_get_location","square_get_order","square_get_payment","square_get_refund","square_list_catalog","square_list_customers","square_list_invoices","square_list_locations","square_list_payments","square_list_refunds","square_pay_order","square_publish_invoice","square_refund_payment","square_search_catalog_objects","square_search_customers","square_search_invoices","square_search_orders","square_update_customer","square_upsert_catalog_object","ssh_check_command_exists","ssh_check_file_exists","ssh_create_directory","ssh_delete_file","ssh_download_file","ssh_execute_command","ssh_execute_script","ssh_get_system_info","ssh_list_directory","ssh_move_rename","ssh_read_file_content","ssh_upload_file","ssh_write_file_content","stagehand_agent","stagehand_extract","stripe_cancel_payment_intent","stripe_cancel_subscription","stripe_capture_charge","stripe_capture_payment_intent","stripe_confirm_payment_intent","stripe_create_charge","stripe_create_customer","stripe_create_invoice","stripe_create_payment_intent","stripe_create_price","stripe_create_product","stripe_create_subscription","stripe_delete_customer","stripe_delete_invoice","stripe_delete_product","stripe_finalize_invoice","stripe_list_charges","stripe_list_customers","stripe_list_events","stripe_list_invoices","stripe_list_payment_intents","stripe_list_prices","stripe_list_products","stripe_list_subscriptions","stripe_pay_invoice","stripe_resume_subscription","stripe_retrieve_charge","stripe_retrieve_customer","stripe_retrieve_event","stripe_retrieve_invoice","stripe_retrieve_payment_intent","stripe_retrieve_price","stripe_retrieve_product","stripe_retrieve_subscription","stripe_search_charges","stripe_search_customers","stripe_search_invoices","stripe_search_payment_intents","stripe_search_prices","stripe_search_products","stripe_search_subscriptions","stripe_send_invoice","stripe_update_charge","stripe_update_customer","stripe_update_invoice","stripe_update_payment_intent","stripe_update_price","stripe_update_product","stripe_update_subscription","stripe_void_invoice","sts_assume_role","sts_assume_role_with_saml","sts_assume_role_with_web_identity","sts_get_access_key_info","sts_get_caller_identity","sts_get_session_token","stt_assemblyai","stt_assemblyai_v2","stt_deepgram","stt_deepgram_v2","stt_elevenlabs","stt_elevenlabs_v2","stt_gemini","stt_gemini_v2","stt_whisper","stt_whisper_v2","supabase_count","supabase_delete","supabase_get_row","supabase_insert","supabase_introspect","supabase_invoke_function","supabase_query","supabase_rpc","supabase_storage_copy","supabase_storage_create_bucket","supabase_storage_create_signed_upload_url","supabase_storage_create_signed_url","supabase_storage_delete","supabase_storage_delete_bucket","supabase_storage_download","supabase_storage_empty_bucket","supabase_storage_get_public_url","supabase_storage_list","supabase_storage_list_buckets","supabase_storage_move","supabase_storage_update_bucket","supabase_storage_upload","supabase_text_search","supabase_update","supabase_upsert","supabase_vector_search","table_batch_insert_rows","table_create","table_delete_row","table_delete_rows_by_filter","table_get_row","table_get_schema","table_insert_row","table_list","table_query_rows","table_query_rows_v2","table_update_row","table_update_rows_by_filter","table_upsert_row","tailscale_authorize_device","tailscale_create_auth_key","tailscale_delete_auth_key","tailscale_delete_device","tailscale_delete_user","tailscale_expire_device_key","tailscale_get_acl","tailscale_get_auth_key","tailscale_get_device","tailscale_get_device_routes","tailscale_get_dns_preferences","tailscale_get_dns_searchpaths","tailscale_list_auth_keys","tailscale_list_devices","tailscale_list_dns_nameservers","tailscale_list_users","tailscale_set_acl","tailscale_set_device_routes","tailscale_set_device_tags","tailscale_set_dns_nameservers","tailscale_set_dns_preferences","tailscale_set_dns_searchpaths","tailscale_suspend_user","tailscale_update_device_key","tavily_crawl","tavily_extract","tavily_map","tavily_search","telegram_copy_message","telegram_delete_message","telegram_edit_message_text","telegram_forward_message","telegram_get_chat","telegram_get_chat_member","telegram_message","telegram_pin_message","telegram_send_animation","telegram_send_audio","telegram_send_chat_action","telegram_send_contact","telegram_send_document","telegram_send_location","telegram_send_photo","telegram_send_poll","telegram_send_video","telegram_set_message_reaction","telegram_unpin_message","temporal_cancel_workflow","temporal_count_workflows","temporal_create_schedule","temporal_delete_schedule","temporal_describe_schedule","temporal_describe_task_queue","temporal_describe_workflow","temporal_get_workflow_history","temporal_list_schedules","temporal_list_workflows","temporal_pause_schedule","temporal_query_workflow","temporal_reset_workflow","temporal_signal_with_start","temporal_signal_workflow","temporal_start_workflow","temporal_terminate_workflow","temporal_trigger_schedule","temporal_unpause_schedule","temporal_update_workflow","textract_analyze_expense","textract_analyze_id","textract_parser","textract_parser_v2","thinking_tool","thrive_add_audience_managers","thrive_add_audience_members","thrive_add_user_tags","thrive_create_assignment","thrive_create_audience","thrive_create_completion","thrive_create_user","thrive_delete_assignment","thrive_delete_audience","thrive_delete_user","thrive_get_activity","thrive_get_assignment","thrive_get_audience","thrive_get_completion","thrive_get_content","thrive_get_cpd_category","thrive_get_cpd_entry","thrive_get_cpd_requirement","thrive_get_enrolment","thrive_get_skill_levels","thrive_get_tag","thrive_get_user_by_id","thrive_get_user_by_ref","thrive_list_assignments","thrive_list_audience_managers","thrive_list_audience_members","thrive_list_audiences","thrive_list_completions","thrive_list_enrolments","thrive_list_tags","thrive_query_activities","thrive_query_content","thrive_query_cpd_categories","thrive_query_cpd_entries","thrive_query_cpd_requirements","thrive_query_cpd_user_summaries","thrive_remove_audience_manager","thrive_remove_audience_member","thrive_remove_user_tags","thrive_replace_audience_managers","thrive_replace_audience_members","thrive_search_users","thrive_suspend_user","thrive_update_assignment","thrive_update_audience","thrive_update_user","thrive_update_user_skills","tiktok_get_post_status","tiktok_get_user","tiktok_list_videos","tiktok_query_videos","tiktok_upload_video_draft","tinybird_append_datasource","tinybird_delete_datasource_rows","tinybird_events","tinybird_get_job","tinybird_query","tinybird_query_pipe","tinybird_truncate_datasource","trello_add_checklist","trello_add_checklist_item","trello_add_comment","trello_add_label","trello_add_member","trello_create_board","trello_create_card","trello_create_list","trello_delete_card","trello_get_actions","trello_get_board","trello_get_card","trello_list_cards","trello_list_lists","trello_list_members","trello_remove_label","trello_remove_member","trello_search","trello_update_card","trello_update_checklist_item","trello_update_list","trigger_dev_activate_schedule","trigger_dev_add_run_tags","trigger_dev_batch_trigger_task","trigger_dev_cancel_run","trigger_dev_complete_waitpoint_token","trigger_dev_create_env_var","trigger_dev_create_schedule","trigger_dev_create_waitpoint_token","trigger_dev_deactivate_schedule","trigger_dev_delete_env_var","trigger_dev_delete_schedule","trigger_dev_execute_query","trigger_dev_get_batch","trigger_dev_get_batch_results","trigger_dev_get_deployment","trigger_dev_get_env_var","trigger_dev_get_latest_deployment","trigger_dev_get_query_schema","trigger_dev_get_queue","trigger_dev_get_run","trigger_dev_get_run_events","trigger_dev_get_run_result","trigger_dev_get_run_trace","trigger_dev_get_schedule","trigger_dev_get_waitpoint_token","trigger_dev_import_env_vars","trigger_dev_list_deployments","trigger_dev_list_env_vars","trigger_dev_list_queues","trigger_dev_list_runs","trigger_dev_list_schedules","trigger_dev_list_timezones","trigger_dev_list_waitpoint_tokens","trigger_dev_override_queue_concurrency","trigger_dev_pause_queue","trigger_dev_promote_deployment","trigger_dev_replay_run","trigger_dev_reschedule_run","trigger_dev_reset_queue_concurrency","trigger_dev_resume_queue","trigger_dev_trigger_task","trigger_dev_update_env_var","trigger_dev_update_run_metadata","trigger_dev_update_schedule","tts_azure","tts_cartesia","tts_deepgram","tts_elevenlabs","tts_google","tts_openai","tts_playht","twilio_send_sms","twilio_voice_get_recording","twilio_voice_list_calls","twilio_voice_make_call","typeform_create_form","typeform_delete_form","typeform_files","typeform_get_form","typeform_insights","typeform_list_forms","typeform_responses","typeform_update_form","upstash_redis_command","upstash_redis_delete","upstash_redis_exists","upstash_redis_expire","upstash_redis_get","upstash_redis_hget","upstash_redis_hgetall","upstash_redis_hset","upstash_redis_incr","upstash_redis_incrby","upstash_redis_keys","upstash_redis_lpush","upstash_redis_lrange","upstash_redis_set","upstash_redis_setnx","upstash_redis_ttl","uptimerobot_create_alert_contact","uptimerobot_create_maintenance_window","uptimerobot_create_monitor","uptimerobot_create_psp","uptimerobot_delete_alert_contact","uptimerobot_delete_maintenance_window","uptimerobot_delete_monitor","uptimerobot_delete_psp","uptimerobot_get_account","uptimerobot_get_alert_contact","uptimerobot_get_incident","uptimerobot_get_maintenance_window","uptimerobot_get_monitor","uptimerobot_get_psp","uptimerobot_list_alert_contacts","uptimerobot_list_incidents","uptimerobot_list_maintenance_windows","uptimerobot_list_monitors","uptimerobot_list_psps","uptimerobot_pause_monitor","uptimerobot_start_monitor","uptimerobot_update_maintenance_window","uptimerobot_update_monitor","uptimerobot_update_psp","vanta_download_document_file","vanta_get_control","vanta_get_document","vanta_get_framework","vanta_get_person","vanta_get_policy","vanta_get_risk_scenario","vanta_get_test","vanta_get_vendor","vanta_get_vulnerable_asset","vanta_list_control_documents","vanta_list_control_tests","vanta_list_controls","vanta_list_document_uploads","vanta_list_documents","vanta_list_framework_controls","vanta_list_frameworks","vanta_list_monitored_computers","vanta_list_people","vanta_list_policies","vanta_list_risk_scenarios","vanta_list_test_entities","vanta_list_tests","vanta_list_vendors","vanta_list_vulnerabilities","vanta_list_vulnerability_remediations","vanta_list_vulnerable_assets","vanta_submit_document","vanta_upload_document_file","vercel_add_domain","vercel_add_project_domain","vercel_cancel_deployment","vercel_create_alias","vercel_create_check","vercel_create_deployment","vercel_create_dns_record","vercel_create_edge_config","vercel_create_env_var","vercel_create_project","vercel_create_webhook","vercel_delete_alias","vercel_delete_deployment","vercel_delete_dns_record","vercel_delete_domain","vercel_delete_edge_config","vercel_delete_env_var","vercel_delete_project","vercel_delete_webhook","vercel_get_alias","vercel_get_check","vercel_get_deployment","vercel_get_deployment_events","vercel_get_domain","vercel_get_domain_config","vercel_get_edge_config","vercel_get_edge_config_items","vercel_get_env_vars","vercel_get_project","vercel_get_team","vercel_get_user","vercel_get_webhook","vercel_list_aliases","vercel_list_checks","vercel_list_deployment_files","vercel_list_deployments","vercel_list_dns_records","vercel_list_domains","vercel_list_edge_configs","vercel_list_project_domains","vercel_list_projects","vercel_list_team_members","vercel_list_teams","vercel_list_webhooks","vercel_pause_project","vercel_promote_deployment","vercel_remove_project_domain","vercel_rerequest_check","vercel_unpause_project","vercel_update_check","vercel_update_dns_record","vercel_update_edge_config_items","vercel_update_env_var","vercel_update_project","vercel_update_project_domain","vercel_verify_project_domain","video_falai","video_luma","video_minimax","video_runway","video_veo","vision_tool","vision_tool_v2","wealthbox_read_contact","wealthbox_read_note","wealthbox_read_task","wealthbox_write_contact","wealthbox_write_note","wealthbox_write_task","webflow_create_item","webflow_delete_item","webflow_get_item","webflow_list_items","webflow_update_item","webhook_request","whatsapp_get_media","whatsapp_mark_read","whatsapp_send_interactive","whatsapp_send_media","whatsapp_send_message","whatsapp_send_reaction","whatsapp_send_template","whatsapp_upload_media","wikipedia_content","wikipedia_random","wikipedia_search","wikipedia_summary","wiza_company_enrichment","wiza_get_credits","wiza_individual_reveal","wiza_prospect_search","wordpress_create_category","wordpress_create_comment","wordpress_create_page","wordpress_create_post","wordpress_create_tag","wordpress_delete_category","wordpress_delete_comment","wordpress_delete_media","wordpress_delete_page","wordpress_delete_post","wordpress_delete_tag","wordpress_get_category","wordpress_get_current_user","wordpress_get_media","wordpress_get_page","wordpress_get_post","wordpress_get_tag","wordpress_get_user","wordpress_list_categories","wordpress_list_comments","wordpress_list_media","wordpress_list_pages","wordpress_list_posts","wordpress_list_tags","wordpress_list_users","wordpress_search_content","wordpress_update_category","wordpress_update_comment","wordpress_update_page","wordpress_update_post","wordpress_update_tag","wordpress_upload_media","workday_assign_onboarding","workday_change_job","workday_create_prehire","workday_get_compensation","workday_get_organizations","workday_get_worker","workday_hire_employee","workday_list_workers","workday_terminate_worker","workday_update_worker","workflow_executor","x_create_bookmark","x_create_tweet","x_delete_bookmark","x_delete_tweet","x_get_blocking","x_get_bookmarks","x_get_followers","x_get_following","x_get_liked_tweets","x_get_liking_users","x_get_me","x_get_personalized_trends","x_get_quote_tweets","x_get_retweeted_by","x_get_trends_by_woeid","x_get_tweets_by_ids","x_get_usage","x_get_user_mentions","x_get_user_timeline","x_get_user_tweets","x_hide_reply","x_manage_block","x_manage_follow","x_manage_like","x_manage_mute","x_manage_retweet","x_read","x_search","x_search_tweets","x_search_users","x_user","x_write","youtube_channel_info","youtube_channel_playlists","youtube_channel_videos","youtube_comments","youtube_playlist_items","youtube_search","youtube_trending","youtube_video_categories","youtube_video_details","zendesk_autocomplete_organizations","zendesk_create_organization","zendesk_create_organizations_bulk","zendesk_create_ticket","zendesk_create_tickets_bulk","zendesk_create_user","zendesk_create_users_bulk","zendesk_delete_organization","zendesk_delete_ticket","zendesk_delete_user","zendesk_get_current_user","zendesk_get_organization","zendesk_get_organizations","zendesk_get_ticket","zendesk_get_tickets","zendesk_get_user","zendesk_get_users","zendesk_merge_tickets","zendesk_search","zendesk_search_count","zendesk_search_users","zendesk_update_organization","zendesk_update_ticket","zendesk_update_tickets_bulk","zendesk_update_user","zendesk_update_users_bulk","zep_add_messages","zep_add_user","zep_create_thread","zep_delete_thread","zep_get_context","zep_get_messages","zep_get_threads","zep_get_user","zep_get_user_threads","zerobounce_get_credits","zerobounce_verify_email","zoho_desk_add_comment","zoho_desk_get_attachment","zoho_desk_get_contact","zoho_desk_get_thread","zoho_desk_get_ticket","zoho_desk_list_comments","zoho_desk_list_organizations","zoho_desk_list_threads","zoho_desk_list_tickets","zoho_desk_update_ticket","zoom_create_meeting","zoom_delete_meeting","zoom_delete_recording","zoom_get_meeting","zoom_get_meeting_invitation","zoom_get_meeting_recordings","zoom_list_meetings","zoom_list_past_participants","zoom_list_recordings","zoom_update_meeting","zoominfo_enrich_companies","zoominfo_enrich_contacts","zoominfo_search_companies","zoominfo_search_contacts","zoominfo_search_intent","zoominfo_search_news"]' ) export default toolIds diff --git a/apps/sim/tools/generated/tool-metadata.ts b/apps/sim/tools/generated/tool-metadata.ts index 71377b8236a..76dabf25d15 100644 --- a/apps/sim/tools/generated/tool-metadata.ts +++ b/apps/sim/tools/generated/tool-metadata.ts @@ -3,7 +3,7 @@ /** Serializable metadata for every built-in tool, keyed by tool id. */ const toolMetadata: Record = JSON.parse( - '{"a2a_cancel_task":{"id":"a2a_cancel_task","name":"A2A Cancel Task","description":"Request cancellation of an in-progress A2A task.","version":"1.0.0","params":{"agentUrl":{"type":"string","required":true,"visibility":"user-only","description":"The A2A agent endpoint URL"},"taskId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The task ID to cancel"},"apiKey":{"type":"string","required":false,"visibility":"user-only","description":"API key for authentication (if required)"}}},"a2a_get_agent_card":{"id":"a2a_get_agent_card","name":"A2A Get Agent Card","description":"Fetch the Agent Card (discovery document) for an external A2A agent.","version":"1.0.0","params":{"agentUrl":{"type":"string","required":true,"visibility":"user-only","description":"The A2A agent endpoint URL"},"apiKey":{"type":"string","required":false,"visibility":"user-only","description":"API key for authentication (if required)"}}},"a2a_get_task":{"id":"a2a_get_task","name":"A2A Get Task","description":"Retrieve the current state and result of an A2A task.","version":"1.0.0","params":{"agentUrl":{"type":"string","required":true,"visibility":"user-only","description":"The A2A agent endpoint URL"},"taskId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The task ID to retrieve"},"historyLength":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of history messages to include"},"apiKey":{"type":"string","required":false,"visibility":"user-only","description":"API key for authentication (if required)"}}},"a2a_send_message":{"id":"a2a_send_message","name":"A2A Send Message","description":"Send a message to an external A2A agent and return its response.","version":"1.0.0","params":{"agentUrl":{"type":"string","required":true,"visibility":"user-only","description":"The A2A agent endpoint URL"},"message":{"type":"string","required":true,"visibility":"user-or-llm","description":"The message text to send"},"data":{"type":"json","required":false,"visibility":"user-or-llm","description":"Optional structured JSON data to attach"},"files":{"type":"json","required":false,"visibility":"user-or-llm","description":"Optional files to attach"},"taskId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Existing task ID to continue"},"contextId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Conversation context ID to continue"},"apiKey":{"type":"string","required":false,"visibility":"user-only","description":"API key for authentication (if required)"}}},"agentmail_create_draft":{"id":"agentmail_create_draft","name":"Create Draft","description":"Create a new email draft in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox to create the draft in"},"to":{"type":"string","required":false,"visibility":"user-or-llm","description":"Recipient email addresses (comma-separated)"},"subject":{"type":"string","required":false,"visibility":"user-or-llm","description":"Draft subject line"},"text":{"type":"string","required":false,"visibility":"user-or-llm","description":"Plain text draft body"},"html":{"type":"string","required":false,"visibility":"user-or-llm","description":"HTML draft body"},"cc":{"type":"string","required":false,"visibility":"user-or-llm","description":"CC recipient email addresses (comma-separated)"},"bcc":{"type":"string","required":false,"visibility":"user-or-llm","description":"BCC recipient email addresses (comma-separated)"},"inReplyTo":{"type":"string","required":false,"visibility":"user-or-llm","description":"ID of message being replied to"},"sendAt":{"type":"string","required":false,"visibility":"user-or-llm","description":"ISO 8601 timestamp to schedule sending"}}},"agentmail_create_inbox":{"id":"agentmail_create_inbox","name":"Create Inbox","description":"Create a new email inbox with AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"username":{"type":"string","required":false,"visibility":"user-or-llm","description":"Username for the inbox email address"},"domain":{"type":"string","required":false,"visibility":"user-or-llm","description":"Domain for the inbox email address"},"displayName":{"type":"string","required":false,"visibility":"user-or-llm","description":"Display name for the inbox"}}},"agentmail_delete_draft":{"id":"agentmail_delete_draft","name":"Delete Draft","description":"Delete an email draft in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox containing the draft"},"draftId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the draft to delete"}}},"agentmail_delete_inbox":{"id":"agentmail_delete_inbox","name":"Delete Inbox","description":"Delete an email inbox in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox to delete"}}},"agentmail_delete_thread":{"id":"agentmail_delete_thread","name":"Delete Thread","description":"Delete an email thread in AgentMail (moves to trash, or permanently deletes if already in trash)","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox containing the thread"},"threadId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the thread to delete"},"permanent":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Force permanent deletion instead of moving to trash"}}},"agentmail_forward_message":{"id":"agentmail_forward_message","name":"Forward Message","description":"Forward an email message to new recipients in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox containing the message"},"messageId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the message to forward"},"to":{"type":"string","required":true,"visibility":"user-or-llm","description":"Recipient email addresses (comma-separated)"},"subject":{"type":"string","required":false,"visibility":"user-or-llm","description":"Override subject line"},"text":{"type":"string","required":false,"visibility":"user-or-llm","description":"Additional plain text to prepend"},"html":{"type":"string","required":false,"visibility":"user-or-llm","description":"Additional HTML to prepend"},"cc":{"type":"string","required":false,"visibility":"user-or-llm","description":"CC recipient email addresses (comma-separated)"},"bcc":{"type":"string","required":false,"visibility":"user-or-llm","description":"BCC recipient email addresses (comma-separated)"}}},"agentmail_get_draft":{"id":"agentmail_get_draft","name":"Get Draft","description":"Get details of a specific email draft in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox the draft belongs to"},"draftId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the draft to retrieve"}}},"agentmail_get_inbox":{"id":"agentmail_get_inbox","name":"Get Inbox","description":"Get details of a specific email inbox in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox to retrieve"}}},"agentmail_get_message":{"id":"agentmail_get_message","name":"Get Message","description":"Get details of a specific email message in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox containing the message"},"messageId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the message to retrieve"}}},"agentmail_get_thread":{"id":"agentmail_get_thread","name":"Get Thread","description":"Get details of a specific email thread including messages in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox containing the thread"},"threadId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the thread to retrieve"}}},"agentmail_list_drafts":{"id":"agentmail_list_drafts","name":"List Drafts","description":"List email drafts in an inbox in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox to list drafts from"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of drafts to return"},"pageToken":{"type":"string","required":false,"visibility":"user-or-llm","description":"Pagination token for next page of results"}}},"agentmail_list_inboxes":{"id":"agentmail_list_inboxes","name":"List Inboxes","description":"List all email inboxes in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of inboxes to return"},"pageToken":{"type":"string","required":false,"visibility":"user-or-llm","description":"Pagination token for next page of results"}}},"agentmail_list_messages":{"id":"agentmail_list_messages","name":"List Messages","description":"List messages in an inbox in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox to list messages from"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of messages to return"},"pageToken":{"type":"string","required":false,"visibility":"user-or-llm","description":"Pagination token for next page of results"}}},"agentmail_list_threads":{"id":"agentmail_list_threads","name":"List Threads","description":"List email threads in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox to list threads from"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of threads to return"},"pageToken":{"type":"string","required":false,"visibility":"user-or-llm","description":"Pagination token for next page of results"},"labels":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated labels to filter threads by"},"before":{"type":"string","required":false,"visibility":"user-or-llm","description":"Filter threads before this ISO 8601 timestamp"},"after":{"type":"string","required":false,"visibility":"user-or-llm","description":"Filter threads after this ISO 8601 timestamp"}}},"agentmail_reply_message":{"id":"agentmail_reply_message","name":"Reply to Message","description":"Reply to an existing email message in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox to reply from"},"messageId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the message to reply to"},"text":{"type":"string","required":false,"visibility":"user-or-llm","description":"Plain text reply body"},"html":{"type":"string","required":false,"visibility":"user-or-llm","description":"HTML reply body"},"to":{"type":"string","required":false,"visibility":"user-or-llm","description":"Override recipient email addresses (comma-separated)"},"cc":{"type":"string","required":false,"visibility":"user-or-llm","description":"CC email addresses (comma-separated)"},"bcc":{"type":"string","required":false,"visibility":"user-or-llm","description":"BCC email addresses (comma-separated)"},"replyAll":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Reply to all recipients of the original message"}}},"agentmail_send_draft":{"id":"agentmail_send_draft","name":"Send Draft","description":"Send an existing email draft in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox containing the draft"},"draftId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the draft to send"}}},"agentmail_send_message":{"id":"agentmail_send_message","name":"Send Message","description":"Send an email message from an AgentMail inbox","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox to send from"},"to":{"type":"string","required":true,"visibility":"user-or-llm","description":"Recipient email address (comma-separated for multiple)"},"subject":{"type":"string","required":true,"visibility":"user-or-llm","description":"Email subject line"},"text":{"type":"string","required":false,"visibility":"user-or-llm","description":"Plain text email body"},"html":{"type":"string","required":false,"visibility":"user-or-llm","description":"HTML email body"},"cc":{"type":"string","required":false,"visibility":"user-or-llm","description":"CC recipient email addresses (comma-separated)"},"bcc":{"type":"string","required":false,"visibility":"user-or-llm","description":"BCC recipient email addresses (comma-separated)"}}},"agentmail_update_draft":{"id":"agentmail_update_draft","name":"Update Draft","description":"Update an existing email draft in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox containing the draft"},"draftId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the draft to update"},"to":{"type":"string","required":false,"visibility":"user-or-llm","description":"Recipient email addresses (comma-separated)"},"subject":{"type":"string","required":false,"visibility":"user-or-llm","description":"Draft subject line"},"text":{"type":"string","required":false,"visibility":"user-or-llm","description":"Plain text draft body"},"html":{"type":"string","required":false,"visibility":"user-or-llm","description":"HTML draft body"},"cc":{"type":"string","required":false,"visibility":"user-or-llm","description":"CC recipient email addresses (comma-separated)"},"bcc":{"type":"string","required":false,"visibility":"user-or-llm","description":"BCC recipient email addresses (comma-separated)"},"sendAt":{"type":"string","required":false,"visibility":"user-or-llm","description":"ISO 8601 timestamp to schedule sending"}}},"agentmail_update_inbox":{"id":"agentmail_update_inbox","name":"Update Inbox","description":"Update the display name of an email inbox in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox to update"},"displayName":{"type":"string","required":true,"visibility":"user-or-llm","description":"New display name for the inbox"}}},"agentmail_update_message":{"id":"agentmail_update_message","name":"Update Message","description":"Add or remove labels on an email message in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox containing the message"},"messageId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the message to update"},"addLabels":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated labels to add to the message"},"removeLabels":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated labels to remove from the message"}}},"agentmail_update_thread":{"id":"agentmail_update_thread","name":"Update Thread Labels","description":"Add or remove labels on an email thread in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox containing the thread"},"threadId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the thread to update"},"addLabels":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated labels to add to the thread"},"removeLabels":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated labels to remove from the thread"}}},"agentphone_create_call":{"id":"agentphone_create_call","name":"Create Outbound Call","description":"Initiate an outbound voice call from an AgentPhone agent","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"agentId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Agent that will handle the call"},"toNumber":{"type":"string","required":true,"visibility":"user-or-llm","description":"Phone number to call in E.164 format (e.g. +14155551234)"},"fromNumberId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Phone number ID to use as caller ID. Must belong to the agent. If omitted, the agent\'s first assigned number is used."},"initialGreeting":{"type":"string","required":false,"visibility":"user-or-llm","description":"Optional greeting spoken when the recipient answers"},"voice":{"type":"string","required":false,"visibility":"user-or-llm","description":"Voice ID override for this call (defaults to the agent\'s configured voice)"},"systemPrompt":{"type":"string","required":false,"visibility":"user-or-llm","description":"When provided, uses a built-in LLM for the conversation instead of forwarding to your webhook"}}},"agentphone_create_contact":{"id":"agentphone_create_contact","name":"Create Contact","description":"Create a new contact in AgentPhone","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"phoneNumber":{"type":"string","required":true,"visibility":"user-or-llm","description":"Phone number in E.164 format (e.g. +14155551234)"},"name":{"type":"string","required":true,"visibility":"user-or-llm","description":"Contact\'s full name"},"email":{"type":"string","required":false,"visibility":"user-or-llm","description":"Contact\'s email address"},"notes":{"type":"string","required":false,"visibility":"user-or-llm","description":"Freeform notes stored on the contact"}}},"agentphone_create_number":{"id":"agentphone_create_number","name":"Create Phone Number","description":"Provision a new SMS- and voice-enabled phone number","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"country":{"type":"string","required":false,"visibility":"user-or-llm","description":"Two-letter country code (e.g. US, CA). Defaults to US."},"areaCode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Preferred area code (US/CA only, e.g. \\"415\\"). Best-effort — may be ignored if unavailable."},"agentId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Optionally attach the number to an agent immediately"}}},"agentphone_delete_contact":{"id":"agentphone_delete_contact","name":"Delete Contact","description":"Delete a contact by ID","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"contactId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Contact ID"}}},"agentphone_get_call":{"id":"agentphone_get_call","name":"Get Call","description":"Fetch a call and its full transcript","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"callId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the call to retrieve"}}},"agentphone_get_call_transcript":{"id":"agentphone_get_call_transcript","name":"Get Call Transcript","description":"Get the full ordered transcript for a call","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"callId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the call to retrieve the transcript for"}}},"agentphone_get_contact":{"id":"agentphone_get_contact","name":"Get Contact","description":"Fetch a single contact by ID","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"contactId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Contact ID"}}},"agentphone_get_conversation":{"id":"agentphone_get_conversation","name":"Get Conversation","description":"Get a conversation along with its recent messages","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"conversationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Conversation ID"},"messageLimit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of recent messages to include (default 50, max 100)"}}},"agentphone_get_conversation_messages":{"id":"agentphone_get_conversation_messages","name":"Get Conversation Messages","description":"Get paginated messages for a conversation","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"conversationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Conversation ID"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of messages to return (default 50, max 200)"},"before":{"type":"string","required":false,"visibility":"user-or-llm","description":"Return messages received before this ISO 8601 timestamp"},"after":{"type":"string","required":false,"visibility":"user-or-llm","description":"Return messages received after this ISO 8601 timestamp"}}},"agentphone_get_number_messages":{"id":"agentphone_get_number_messages","name":"Get Phone Number Messages","description":"Fetch messages received on a specific phone number","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"numberId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the phone number"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of messages to return (default 50, max 200)"},"before":{"type":"string","required":false,"visibility":"user-or-llm","description":"Return messages received before this ISO 8601 timestamp"},"after":{"type":"string","required":false,"visibility":"user-or-llm","description":"Return messages received after this ISO 8601 timestamp"}}},"agentphone_get_usage":{"id":"agentphone_get_usage","name":"Get Usage","description":"Retrieve current usage statistics for the AgentPhone account","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"}}},"agentphone_get_usage_daily":{"id":"agentphone_get_usage_daily","name":"Get Daily Usage","description":"Get a daily breakdown of usage (messages, calls, webhooks) for the last N days","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"days":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of days to return (1-365, default 30)"}}},"agentphone_get_usage_monthly":{"id":"agentphone_get_usage_monthly","name":"Get Monthly Usage","description":"Get monthly usage aggregation (messages, calls, webhooks) for the last N months","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"months":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of months to return (1-24, default 6)"}}},"agentphone_list_calls":{"id":"agentphone_list_calls","name":"List Calls","description":"List voice calls for this AgentPhone account","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of results to return (default 20, max 100)"},"offset":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of results to skip (min 0)"},"status":{"type":"string","required":false,"visibility":"user-or-llm","description":"Filter by status (completed, in-progress, failed)"},"direction":{"type":"string","required":false,"visibility":"user-or-llm","description":"Filter by direction (inbound, outbound)"},"type":{"type":"string","required":false,"visibility":"user-or-llm","description":"Filter by call type (pstn, web)"},"search":{"type":"string","required":false,"visibility":"user-or-llm","description":"Search by phone number (matches fromNumber or toNumber)"}}},"agentphone_list_contacts":{"id":"agentphone_list_contacts","name":"List Contacts","description":"List contacts for this AgentPhone account","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"search":{"type":"string","required":false,"visibility":"user-or-llm","description":"Filter by name or phone number (case-insensitive contains)"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of results to return (default 50, max 200)"},"offset":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of results to skip (min 0)"}}},"agentphone_list_conversations":{"id":"agentphone_list_conversations","name":"List Conversations","description":"List conversations (message threads) for this AgentPhone account","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of results to return (default 20, max 100)"},"offset":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of results to skip (min 0)"}}},"agentphone_list_numbers":{"id":"agentphone_list_numbers","name":"List Phone Numbers","description":"List all phone numbers provisioned for this AgentPhone account","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of results to return (default 20, max 100)"},"offset":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of results to skip (min 0)"}}},"agentphone_react_to_message":{"id":"agentphone_react_to_message","name":"React to Message","description":"Send an iMessage tapback reaction to a message (iMessage only)","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"messageId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the message to react to"},"reaction":{"type":"string","required":true,"visibility":"user-or-llm","description":"Reaction type: love, like, dislike, laugh, emphasize, or question"}}},"agentphone_release_number":{"id":"agentphone_release_number","name":"Release Phone Number","description":"Release (delete) a phone number. This action is irreversible.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"numberId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the phone number to release"}}},"agentphone_send_message":{"id":"agentphone_send_message","name":"Send Message","description":"Send an outbound SMS or iMessage from an AgentPhone agent","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"agentId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Agent sending the message"},"toNumber":{"type":"string","required":true,"visibility":"user-or-llm","description":"Recipient phone number in E.164 format (e.g. +14155551234)"},"body":{"type":"string","required":true,"visibility":"user-or-llm","description":"Message text to send"},"mediaUrl":{"type":"string","required":false,"visibility":"user-or-llm","description":"Optional URL of an image, video, or file to attach"},"numberId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Phone number ID to send from. If omitted, the agent\'s first assigned number is used."}}},"agentphone_update_contact":{"id":"agentphone_update_contact","name":"Update Contact","description":"Update a contact\'s fields","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"contactId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Contact ID"},"phoneNumber":{"type":"string","required":false,"visibility":"user-or-llm","description":"New phone number in E.164 format"},"name":{"type":"string","required":false,"visibility":"user-or-llm","description":"New contact name"},"email":{"type":"string","required":false,"visibility":"user-or-llm","description":"New email address"},"notes":{"type":"string","required":false,"visibility":"user-or-llm","description":"New freeform notes"}}},"agentphone_update_conversation":{"id":"agentphone_update_conversation","name":"Update Conversation","description":"Update conversation metadata (stored state). Pass null to clear existing metadata.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"conversationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Conversation ID"},"metadata":{"type":"json","required":false,"visibility":"user-or-llm","description":"Custom key-value metadata to store on the conversation. Pass null to clear existing metadata."}}},"agiloft_attach_file":{"id":"agiloft_attach_file","name":"Agiloft Attach File","description":"Attach a file to a field in an Agiloft record.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"contracts\\")"},"recordId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the record to attach the file to"},"fieldName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the attachment field"},"file":{"type":"file","required":false,"visibility":"user-or-llm","description":"File to attach"},"fileName":{"type":"string","required":false,"visibility":"user-or-llm","description":"Name to assign to the file (defaults to original file name)"}}},"agiloft_attachment_info":{"id":"agiloft_attachment_info","name":"Agiloft Attachment Info","description":"Get information about file attachments on a record field.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"contracts\\")"},"recordId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the record to check attachments on"},"fieldName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the attachment field to inspect"}}},"agiloft_create_record":{"id":"agiloft_create_record","name":"Agiloft Create Record","description":"Create a new record in an Agiloft table.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"contracts\\", \\"contacts.employees\\")"},"data":{"type":"string","required":true,"visibility":"user-or-llm","description":"Record field values as a JSON object (e.g., {\\"first_name\\": \\"John\\", \\"status\\": \\"Active\\"})"}}},"agiloft_delete_record":{"id":"agiloft_delete_record","name":"Agiloft Delete Record","description":"Delete a record from an Agiloft table.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"contracts\\", \\"contacts.employees\\")"},"recordId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the record to delete"},"deleteRule":{"type":"string","required":false,"visibility":"user-or-llm","description":"How to treat records that depend on this one: ERROR_IF_DEPENDANTS (default — fails rather than cascading), APPLY_DELETE_WHERE_POSSIBLE, DELETE_WHERE_POSSIBLE_OTHERWISE_UNLINK, APPLY_UNLINK, or UNLINK_WHERE_POSSIBLE_OTHERWISE_DELETE"}}},"agiloft_get_choice_line_id":{"id":"agiloft_get_choice_line_id","name":"Agiloft Get Choice Line ID","description":"Resolve the internal numeric ID of a choice-list value, for use in EWSelect WHERE clauses against choice fields.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"case\\", \\"contracts\\")"},"fieldName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Choice field name (e.g., \\"priority\\", \\"status\\")"},"value":{"type":"string","required":true,"visibility":"user-or-llm","description":"Choice display value to resolve (e.g., \\"High\\", \\"Active\\")"}}},"agiloft_lock_record":{"id":"agiloft_lock_record","name":"Agiloft Lock Record","description":"Lock, unlock, or check the lock status of an Agiloft record.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"contracts\\")"},"recordId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the record to lock, unlock, or check"},"lockAction":{"type":"string","required":true,"visibility":"user-or-llm","description":"Action to perform: \\"lock\\", \\"unlock\\", or \\"check\\""},"force":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Unlock only: release a lock held by another user. Requires membership in the admin group."}}},"agiloft_read_record":{"id":"agiloft_read_record","name":"Agiloft Read Record","description":"Read a record by ID from an Agiloft table.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"contracts\\", \\"contacts.employees\\")"},"recordId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the record to read"},"fields":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated list of field names to include in the response"}}},"agiloft_remove_attachment":{"id":"agiloft_remove_attachment","name":"Agiloft Remove Attachment","description":"Remove an attached file from a field in an Agiloft record.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"contracts\\")"},"recordId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the record containing the attachment"},"fieldName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the attachment field"},"position":{"type":"string","required":true,"visibility":"user-or-llm","description":"Position index of the file to remove (starting from 0)"}}},"agiloft_retrieve_attachment":{"id":"agiloft_retrieve_attachment","name":"Agiloft Retrieve Attachment","description":"Download an attached file from an Agiloft record field.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"contracts\\")"},"recordId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the record containing the attachment"},"fieldName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the attachment field"},"position":{"type":"string","required":true,"visibility":"user-or-llm","description":"Position index of the file in the field (starting from 0)"}}},"agiloft_run_action_button":{"id":"agiloft_run_action_button","name":"Agiloft Run Action Button","description":"Run an action button on an Agiloft record, such as an approval or send-for-signature step.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"contracts\\", \\"case\\")"},"recordId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the record to run the action button on"},"actionButtonField":{"type":"string","required":true,"visibility":"user-or-llm","description":"Logical name of the field holding the action button (e.g., \\"ab_field\\")"}}},"agiloft_saved_search":{"id":"agiloft_saved_search","name":"Agiloft Saved Search (retired)","description":"Retired. Agiloft does not document an endpoint for listing saved searches — use the Search Records operation and set its Saved Search field instead.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":false,"visibility":"user-only","description":"Agiloft instance URL"},"knowledgeBase":{"type":"string","required":false,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":false,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":false,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":false,"visibility":"user-or-llm","description":"Table name"}}},"agiloft_search_records":{"id":"agiloft_search_records","name":"Agiloft Search Records","description":"Search for records in an Agiloft table using a query.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name to search in (e.g., \\"contracts\\", \\"contacts.employees\\")"},"query":{"type":"string","required":false,"visibility":"user-or-llm","description":"Ad hoc EWSearch query. Combine conditions with && (and) or || (or) and quote every value — e.g. \\"summary~=\'test\'&&priority=\'High\'\\". Required unless a saved search is given."},"search":{"type":"string","required":false,"visibility":"user-or-llm","description":"Label of a saved search defined on the table (e.g., \\"C: Status is Closed\\"). Can be combined with a query to narrow it further."},"fields":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated list of field names to include in the results"},"page":{"type":"string","required":false,"visibility":"user-or-llm","description":"Page number for paginated results (starting from 0)"},"limit":{"type":"string","required":false,"visibility":"user-or-llm","description":"Maximum number of records to return per page. Agiloft treats 0 as \\"all records\\", so leave it unset or use a positive value to keep result sizes bounded."}}},"agiloft_select_records":{"id":"agiloft_select_records","name":"Agiloft Select Records","description":"Select record IDs matching a SQL WHERE clause from an Agiloft table.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"contracts\\", \\"contacts.employees\\")"},"where":{"type":"string","required":true,"visibility":"user-or-llm","description":"SQL WHERE clause using database column names (e.g., \\"summary like \'%new%\'\\" or \\"assigned_person=\'John Doe\'\\"). EWSelect has no page size and returns every matching ID, so append a database limit such as \\"limit 0,200\\" to bound the result."}}},"agiloft_update_record":{"id":"agiloft_update_record","name":"Agiloft Update Record","description":"Update an existing record in an Agiloft table.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"contracts\\", \\"contacts.employees\\")"},"recordId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the record to update"},"data":{"type":"string","required":true,"visibility":"user-or-llm","description":"Updated field values as a JSON object (e.g., {\\"status\\": \\"Active\\", \\"priority\\": \\"High\\"})"}}},"ahrefs_anchors":{"id":"ahrefs_anchors","name":"Ahrefs Anchors","description":"Get the anchor text distribution for a target domain or URL\'s backlinks, showing how many links and referring domains use each anchor text.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\" or \\"https://example.com/page\\""},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match)"},"history":{"type":"string","required":false,"visibility":"user-or-llm","description":"Historical scope: \\"live\\" (currently live), \\"all_time\\" (default, includes lost backlinks), or \\"since:YYYY-MM-DD\\" (backlinks found since a date)"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of results to return. Example: 50 (default: 1000)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}}},"ahrefs_backlinks":{"id":"ahrefs_backlinks","name":"Ahrefs Backlinks","description":"Get a list of backlinks pointing to a target domain or URL. Returns details about each backlink including source URL, anchor text, and domain rating.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\" or \\"https://example.com/page\\""},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match). Example: \\"domain\\""},"history":{"type":"string","required":false,"visibility":"user-or-llm","description":"Historical scope: \\"live\\" (currently live backlinks), \\"all_time\\" (default, includes lost backlinks), or \\"since:YYYY-MM-DD\\" (backlinks found since a date)."},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of results to return. Example: 50 (default: 1000)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}}},"ahrefs_backlinks_stats":{"id":"ahrefs_backlinks_stats","name":"Ahrefs Backlinks Stats","description":"Get backlink and referring domain totals for a target domain or URL, both currently live and across all time.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\" or \\"https://example.com/page\\""},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match). Example: \\"domain\\""},"date":{"type":"string","required":false,"visibility":"user-only","description":"Date to report metrics on, in YYYY-MM-DD format (defaults to today)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}}},"ahrefs_batch_analysis":{"id":"ahrefs_batch_analysis","name":"Ahrefs Batch Analysis","description":"Get bulk SEO metrics (Domain Rating, backlinks, referring domains, organic traffic, and more) for multiple domains or URLs in a single request. Useful for comparing many competitors at once.","version":"1.0.0","params":{"targets":{"type":"string","required":true,"visibility":"user-or-llm","description":"Comma-separated list of domains or URLs to analyze. Example: \\"example.com,competitor.com\\""},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode applied to every target: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match)"},"protocol":{"type":"string","required":false,"visibility":"user-or-llm","description":"Protocol applied to every target: \\"both\\" (default), \\"http\\", or \\"https\\""},"country":{"type":"string","required":false,"visibility":"user-or-llm","description":"Country code for traffic data. Example: \\"us\\", \\"gb\\", \\"de\\" (default: \\"us\\")"},"volumeMode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Search volume calculation: \\"monthly\\" or \\"average\\" (default: \\"monthly\\")"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}}},"ahrefs_broken_backlinks":{"id":"ahrefs_broken_backlinks","name":"Ahrefs Broken Backlinks","description":"Get a list of broken backlinks pointing to a target domain or URL. Useful for identifying link reclamation opportunities.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\" or \\"https://example.com/page\\""},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match). Example: \\"domain\\""},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of results to return. Example: 50 (default: 1000)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}}},"ahrefs_domain_rating":{"id":"ahrefs_domain_rating","name":"Ahrefs Domain Rating","description":"Get the Domain Rating (DR) and Ahrefs Rank for a target domain. Domain Rating shows the strength of a website\'s backlink profile on a scale from 0 to 100.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain to analyze (e.g., example.com)"},"date":{"type":"string","required":false,"visibility":"user-only","description":"Date for historical data in YYYY-MM-DD format (defaults to today)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}}},"ahrefs_domain_rating_history":{"id":"ahrefs_domain_rating_history","name":"Ahrefs Domain Rating History","description":"Get the historical Domain Rating (DR) trend for a target domain or URL over a date range, grouped daily, weekly, or monthly.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\""},"dateFrom":{"type":"string","required":true,"visibility":"user-only","description":"Start date of the historical period, in YYYY-MM-DD format"},"dateTo":{"type":"string","required":false,"visibility":"user-only","description":"End date of the historical period, in YYYY-MM-DD format (defaults to today)"},"historyGrouping":{"type":"string","required":false,"visibility":"user-or-llm","description":"Time interval for grouping data points: \\"daily\\", \\"weekly\\", or \\"monthly\\" (default: \\"monthly\\")"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}}},"ahrefs_keyword_overview":{"id":"ahrefs_keyword_overview","name":"Ahrefs Keyword Overview","description":"Get detailed metrics for a keyword including search volume, keyword difficulty, CPC, clicks, and traffic potential.","version":"1.0.0","params":{"keyword":{"type":"string","required":true,"visibility":"user-or-llm","description":"The keyword to analyze"},"country":{"type":"string","required":false,"visibility":"user-or-llm","description":"Country code for keyword data. Example: \\"us\\", \\"gb\\", \\"de\\" (default: \\"us\\")"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}}},"ahrefs_keywords_history":{"id":"ahrefs_keywords_history","name":"Ahrefs Keywords History","description":"Get the historical organic keyword ranking distribution for a target domain or URL over a date range: how many keywords rank in each position bucket at each point in time.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\""},"dateFrom":{"type":"string","required":true,"visibility":"user-only","description":"Start date of the historical period, in YYYY-MM-DD format"},"dateTo":{"type":"string","required":false,"visibility":"user-only","description":"End date of the historical period, in YYYY-MM-DD format (defaults to today)"},"historyGrouping":{"type":"string","required":false,"visibility":"user-or-llm","description":"Time interval for grouping data points: \\"daily\\", \\"weekly\\", or \\"monthly\\" (default: \\"monthly\\")"},"country":{"type":"string","required":false,"visibility":"user-or-llm","description":"Country code for search results. Example: \\"us\\", \\"gb\\", \\"de\\" (default: \\"us\\")"},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}}},"ahrefs_metrics":{"id":"ahrefs_metrics","name":"Ahrefs Metrics","description":"Get a one-call organic and paid search overview for a target domain or URL: organic traffic, organic keywords, paid traffic, paid keywords, and estimated traffic cost.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\""},"country":{"type":"string","required":false,"visibility":"user-or-llm","description":"Country code for traffic data. Example: \\"us\\", \\"gb\\", \\"de\\""},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match). Example: \\"domain\\""},"date":{"type":"string","required":false,"visibility":"user-only","description":"Date to report metrics on, in YYYY-MM-DD format (defaults to today)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}}},"ahrefs_metrics_history":{"id":"ahrefs_metrics_history","name":"Ahrefs Metrics History","description":"Get the historical organic and paid traffic trend for a target domain or URL over a date range: organic traffic/cost and paid traffic/cost at each point in time.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\""},"dateFrom":{"type":"string","required":true,"visibility":"user-only","description":"Start date of the historical period, in YYYY-MM-DD format"},"dateTo":{"type":"string","required":false,"visibility":"user-only","description":"End date of the historical period, in YYYY-MM-DD format (defaults to today)"},"volumeMode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Search volume calculation: \\"monthly\\" or \\"average\\" (default: \\"monthly\\")"},"historyGrouping":{"type":"string","required":false,"visibility":"user-or-llm","description":"Time interval for grouping data points: \\"daily\\", \\"weekly\\", or \\"monthly\\" (default: \\"monthly\\")"},"country":{"type":"string","required":false,"visibility":"user-or-llm","description":"Country code for traffic data. Example: \\"us\\", \\"gb\\", \\"de\\" (default: \\"us\\")"},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}}},"ahrefs_organic_competitors":{"id":"ahrefs_organic_competitors","name":"Ahrefs Organic Competitors","description":"Get domains that compete with a target domain or URL for the same organic keywords, ranked by keyword overlap.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\""},"country":{"type":"string","required":false,"visibility":"user-or-llm","description":"Country code for search results. Example: \\"us\\", \\"gb\\", \\"de\\" (default: \\"us\\")"},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match). Example: \\"domain\\""},"date":{"type":"string","required":false,"visibility":"user-only","description":"Date to report metrics on, in YYYY-MM-DD format (defaults to today)"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of results to return. Example: 50 (default: 1000)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}}},"ahrefs_organic_keywords":{"id":"ahrefs_organic_keywords","name":"Ahrefs Organic Keywords","description":"Get organic keywords that a target domain or URL ranks for in Google search results. Returns keyword details including search volume, ranking position, and estimated traffic.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\" or \\"https://example.com/page\\""},"country":{"type":"string","required":false,"visibility":"user-or-llm","description":"Country code for search results. Example: \\"us\\", \\"gb\\", \\"de\\" (default: \\"us\\")"},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match). Example: \\"domain\\""},"date":{"type":"string","required":false,"visibility":"user-only","description":"Date to report metrics on, in YYYY-MM-DD format (defaults to today)"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of results to return. Example: 50 (default: 1000)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}}},"ahrefs_paid_pages":{"id":"ahrefs_paid_pages","name":"Ahrefs Paid Pages","description":"Get a target domain\'s pages that receive paid search traffic, sorted by estimated paid traffic. Returns page URLs with their paid traffic, keyword counts, and estimated spend.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\""},"country":{"type":"string","required":false,"visibility":"user-or-llm","description":"Country code for traffic data. Example: \\"us\\", \\"gb\\", \\"de\\" (default: \\"us\\")"},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match)"},"date":{"type":"string","required":false,"visibility":"user-only","description":"Date to report metrics on, in YYYY-MM-DD format (defaults to today)"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of results to return. Example: 50 (default: 1000)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}}},"ahrefs_rank_tracker_competitors_overview":{"id":"ahrefs_rank_tracker_competitors_overview","name":"Ahrefs Rank Tracker Competitors Overview","description":"Get competitor rankings for the keywords tracked in an Ahrefs Rank Tracker project: each tracked keyword\'s volume and difficulty alongside every competitor\'s position, traffic, and traffic value. This endpoint is free and does not consume API units.","version":"1.0.0","params":{"projectId":{"type":"number","required":true,"visibility":"user-or-llm","description":"The Rank Tracker project ID (found in the project URL in Ahrefs)"},"date":{"type":"string","required":true,"visibility":"user-only","description":"Date to report rankings for, in YYYY-MM-DD format"},"device":{"type":"string","required":true,"visibility":"user-or-llm","description":"Rankings device type: \\"desktop\\" or \\"mobile\\""},"dateCompared":{"type":"string","required":false,"visibility":"user-only","description":"Comparison date in YYYY-MM-DD format, to compute position/traffic deltas"},"volumeMode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Search volume calculation: \\"monthly\\" or \\"average\\" (default: \\"monthly\\")"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of results to return. Example: 50 (default: 1000)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}}},"ahrefs_rank_tracker_competitors_stats":{"id":"ahrefs_rank_tracker_competitors_stats","name":"Ahrefs Rank Tracker Competitors Stats","description":"Get aggregate competitor stats for an Ahrefs Rank Tracker project: each competitor\'s traffic, traffic value, average position, and share of voice across all tracked keywords. This endpoint is free and does not consume API units.","version":"1.0.0","params":{"projectId":{"type":"number","required":true,"visibility":"user-or-llm","description":"The Rank Tracker project ID (found in the project URL in Ahrefs)"},"date":{"type":"string","required":true,"visibility":"user-only","description":"Date to report metrics for, in YYYY-MM-DD format"},"device":{"type":"string","required":true,"visibility":"user-or-llm","description":"Rankings device type: \\"desktop\\" or \\"mobile\\""},"volumeMode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Search volume calculation: \\"monthly\\" or \\"average\\" (default: \\"monthly\\")"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}}},"ahrefs_rank_tracker_overview":{"id":"ahrefs_rank_tracker_overview","name":"Ahrefs Rank Tracker Overview","description":"Get ranking overview metrics for the keywords tracked in an Ahrefs Rank Tracker project: position, search volume, keyword difficulty, and estimated traffic. This endpoint is free and does not consume API units.","version":"1.0.0","params":{"projectId":{"type":"number","required":true,"visibility":"user-or-llm","description":"The Rank Tracker project ID (found in the project URL in Ahrefs)"},"date":{"type":"string","required":true,"visibility":"user-only","description":"Date to report rankings for, in YYYY-MM-DD format"},"device":{"type":"string","required":true,"visibility":"user-or-llm","description":"Rankings device type: \\"desktop\\" or \\"mobile\\""},"dateCompared":{"type":"string","required":false,"visibility":"user-only","description":"Comparison date in YYYY-MM-DD format, to compute position/traffic deltas"},"volumeMode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Search volume calculation: \\"monthly\\" or \\"average\\" (default: \\"monthly\\")"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of results to return. Example: 50 (default: 1000)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}}},"ahrefs_rank_tracker_serp_overview":{"id":"ahrefs_rank_tracker_serp_overview","name":"Ahrefs Rank Tracker SERP Overview","description":"Get the full SERP (search engine results page) for a keyword tracked in an Ahrefs Rank Tracker project, including every ranking URL with its position, title, and authority metrics. This endpoint is free and does not consume API units.","version":"1.0.0","params":{"projectId":{"type":"number","required":true,"visibility":"user-or-llm","description":"The Rank Tracker project ID (found in the project URL in Ahrefs)"},"keyword":{"type":"string","required":true,"visibility":"user-or-llm","description":"The tracked keyword to retrieve SERP data for"},"country":{"type":"string","required":true,"visibility":"user-or-llm","description":"Country code for the tracked keyword. Example: \\"us\\", \\"gb\\", \\"de\\""},"device":{"type":"string","required":true,"visibility":"user-or-llm","description":"Rankings device type: \\"desktop\\" or \\"mobile\\""},"topPositions":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of top organic positions to return (defaults to all available)"},"date":{"type":"string","required":false,"visibility":"user-only","description":"Timestamp to return the last available SERP Overview at, in YYYY-MM-DDThh:mm:ss format"},"locationId":{"type":"number","required":false,"visibility":"user-or-llm","description":"Location ID of the tracked keyword, if tracked at a specific location"},"languageCode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Language code of the tracked keyword"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}}},"ahrefs_refdomains_history":{"id":"ahrefs_refdomains_history","name":"Ahrefs Referring Domains History","description":"Get the historical referring domains trend for a target domain or URL over a date range, grouped daily, weekly, or monthly.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\""},"dateFrom":{"type":"string","required":true,"visibility":"user-only","description":"Start date of the historical period, in YYYY-MM-DD format"},"dateTo":{"type":"string","required":false,"visibility":"user-only","description":"End date of the historical period, in YYYY-MM-DD format (defaults to today)"},"historyGrouping":{"type":"string","required":false,"visibility":"user-or-llm","description":"Time interval for grouping data points: \\"daily\\", \\"weekly\\", or \\"monthly\\" (default: \\"monthly\\")"},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}}},"ahrefs_referring_domains":{"id":"ahrefs_referring_domains","name":"Ahrefs Referring Domains","description":"Get a list of domains that link to a target domain or URL. Returns unique referring domains with their domain rating, backlink counts, and discovery dates.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\" or \\"https://example.com/page\\""},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match). Example: \\"domain\\""},"history":{"type":"string","required":false,"visibility":"user-or-llm","description":"Historical scope: \\"live\\" (currently live), \\"all_time\\" (default, includes lost domains), or \\"since:YYYY-MM-DD\\" (domains found since a date)."},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of results to return. Example: 50 (default: 1000)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}}},"ahrefs_related_terms":{"id":"ahrefs_related_terms","name":"Ahrefs Related Terms","description":"Get keyword ideas related to a seed keyword: terms the same top-ranking pages also rank for (\\"also rank for\\") or also discuss (\\"also talk about\\"), with volume, difficulty, and CPC.","version":"1.0.0","params":{"keyword":{"type":"string","required":true,"visibility":"user-or-llm","description":"The seed keyword to find related terms for"},"country":{"type":"string","required":false,"visibility":"user-or-llm","description":"Country code for keyword data. Example: \\"us\\", \\"gb\\", \\"de\\" (default: \\"us\\")"},"terms":{"type":"string","required":false,"visibility":"user-or-llm","description":"Type of related keywords to return: \\"also_rank_for\\", \\"also_talk_about\\", or \\"all\\" (default: \\"all\\")"},"viewFor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Whether to derive related terms from the top 10 or top 100 ranking pages (default: \\"top_10\\")"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of results to return. Example: 50 (default: 1000)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}}},"ahrefs_site_audit_page_explorer":{"id":"ahrefs_site_audit_page_explorer","name":"Ahrefs Site Audit Page Explorer","description":"Get crawled pages from an Ahrefs Site Audit project with health and SEO metrics: HTTP status, title, link counts, backlinks, indexability, and traffic. Optionally filter to pages affected by a specific issue.","version":"1.0.0","params":{"projectId":{"type":"number","required":true,"visibility":"user-or-llm","description":"The Site Audit project ID (found in the project URL in Ahrefs)"},"date":{"type":"string","required":false,"visibility":"user-only","description":"Crawl date in YYYY-MM-DDThh:mm:ss format (defaults to the most recent crawl)"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of results to return. Example: 50 (default: 1000)"},"offset":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of results to skip, for pagination"},"issueId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Only return pages affected by this issue ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}}},"ahrefs_top_pages":{"id":"ahrefs_top_pages","name":"Ahrefs Top Pages","description":"Get the top pages of a target domain sorted by organic traffic. Returns page URLs with their traffic, keyword counts, and estimated traffic value.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain to analyze. Example: \\"example.com\\""},"country":{"type":"string","required":false,"visibility":"user-or-llm","description":"Country code for traffic data. Example: \\"us\\", \\"gb\\", \\"de\\" (default: \\"us\\")"},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match). Example: \\"domain\\""},"date":{"type":"string","required":false,"visibility":"user-only","description":"Date to report metrics on, in YYYY-MM-DD format (defaults to today)"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of results to return. Example: 50 (default: 1000)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}}},"airtable_create_records":{"id":"airtable_create_records","name":"Airtable Create Records","description":"Write new records to an Airtable table","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token"},"baseId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Airtable base ID (starts with \\"app\\", e.g., \\"appXXXXXXXXXXXXXX\\")"},"tableId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table ID (starts with \\"tbl\\") or table name"},"records":{"type":"json","required":true,"visibility":"user-or-llm","description":"Array of records to create, each with a `fields` object"},"typecast":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"When true, Airtable automatically converts string values to the field type"}},"oauth":{"required":true,"provider":"airtable"}},"airtable_delete_records":{"id":"airtable_delete_records","name":"Airtable Delete Records","description":"Delete one or more records from an Airtable table by ID","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token"},"baseId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Airtable base ID (starts with \\"app\\", e.g., \\"appXXXXXXXXXXXXXX\\")"},"tableId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table ID (starts with \\"tbl\\") or table name"},"recordIds":{"type":"json","required":true,"visibility":"user-or-llm","description":"Array of record IDs to delete (each starts with \\"rec\\", e.g., [\\"recXXXXXXXXXXXXXX\\"]). Pass a single-element array to delete one record."}},"oauth":{"required":true,"provider":"airtable"}},"airtable_get_base_schema":{"id":"airtable_get_base_schema","name":"Airtable Get Base Schema","description":"Get the schema of all tables, fields, and views in an Airtable base","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token"},"baseId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Airtable base ID (starts with \\"app\\", e.g., \\"appXXXXXXXXXXXXXX\\")"}},"oauth":{"required":true,"provider":"airtable"}},"airtable_get_record":{"id":"airtable_get_record","name":"Airtable Get Record","description":"Retrieve a single record from an Airtable table by its ID","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token"},"baseId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Airtable base ID (starts with \\"app\\", e.g., \\"appXXXXXXXXXXXXXX\\")"},"tableId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table ID (starts with \\"tbl\\") or table name"},"recordId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Record ID to retrieve (starts with \\"rec\\", e.g., \\"recXXXXXXXXXXXXXX\\")"}},"oauth":{"required":true,"provider":"airtable"}},"airtable_list_bases":{"id":"airtable_list_bases","name":"Airtable List Bases","description":"List all bases the authenticated user has access to","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token"},"offset":{"type":"string","required":false,"visibility":"user-or-llm","description":"Pagination offset for retrieving additional bases"}},"oauth":{"required":true,"provider":"airtable"}},"airtable_list_records":{"id":"airtable_list_records","name":"Airtable List Records","description":"Read records from an Airtable table","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token"},"baseId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Airtable base ID (starts with \\"app\\", e.g., \\"appXXXXXXXXXXXXXX\\")"},"tableId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table ID (starts with \\"tbl\\") or table name"},"maxRecords":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of records to return (default: all records)"},"filterFormula":{"type":"string","required":false,"visibility":"user-or-llm","description":"Formula to filter records (e.g., \\"({Field Name} = \'Value\')\\")"}},"oauth":{"required":true,"provider":"airtable"}},"airtable_list_tables":{"id":"airtable_list_tables","name":"Airtable List Tables","description":"List all tables and their schema in an Airtable base","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token"},"baseId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Airtable base ID (starts with \\"app\\", e.g., \\"appXXXXXXXXXXXXXX\\")"}},"oauth":{"required":true,"provider":"airtable"}},"airtable_update_multiple_records":{"id":"airtable_update_multiple_records","name":"Airtable Update Multiple Records","description":"Update multiple existing records in an Airtable table","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token"},"baseId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Airtable base ID (starts with \\"app\\", e.g., \\"appXXXXXXXXXXXXXX\\")"},"tableId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table ID (starts with \\"tbl\\") or table name"},"records":{"type":"json","required":true,"visibility":"user-or-llm","description":"Array of records to update, each with an `id` and a `fields` object"},"typecast":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"When true, Airtable automatically converts string values to the field type"}},"oauth":{"required":true,"provider":"airtable"}},"airtable_update_record":{"id":"airtable_update_record","name":"Airtable Update Record","description":"Update an existing record in an Airtable table by ID","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token"},"baseId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Airtable base ID (starts with \\"app\\", e.g., \\"appXXXXXXXXXXXXXX\\")"},"tableId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table ID (starts with \\"tbl\\") or table name"},"recordId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Record ID to update (starts with \\"rec\\", e.g., \\"recXXXXXXXXXXXXXX\\")"},"fields":{"type":"json","required":true,"visibility":"user-or-llm","description":"An object containing the field names and their new values"},"typecast":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"When true, Airtable automatically converts string values to the field type"}},"oauth":{"required":true,"provider":"airtable"}},"airtable_upsert_records":{"id":"airtable_upsert_records","name":"Airtable Upsert Records","description":"Update existing records or create new ones in an Airtable table, matching on the specified merge fields","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token"},"baseId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Airtable base ID (starts with \\"app\\", e.g., \\"appXXXXXXXXXXXXXX\\")"},"tableId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table ID (starts with \\"tbl\\") or table name"},"records":{"type":"json","required":true,"visibility":"user-or-llm","description":"Array of records to upsert, each with a `fields` object"},"fieldsToMergeOn":{"type":"json","required":true,"visibility":"user-or-llm","description":"Array of field names used to match existing records (max 3). A record is updated when all merge fields match, otherwise it is created. Example: [\\"Name\\"]"},"typecast":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"When true, Airtable automatically converts string values to the field type"}},"oauth":{"required":true,"provider":"airtable"}},"airweave_search":{"id":"airweave_search","name":"Airweave Search","description":"Search your synced data collections using Airweave. Supports semantic search with hybrid, neural, or keyword retrieval strategies. Optionally generate AI-powered answers from search results.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Airweave API Key for authentication"},"collectionId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The readable ID of the collection to search"},"query":{"type":"string","required":true,"visibility":"user-or-llm","description":"The search query text"},"limit":{"type":"number","required":false,"visibility":"user-only","description":"Maximum number of results to return (default: 100)"},"retrievalStrategy":{"type":"string","required":false,"visibility":"user-or-llm","description":"Retrieval strategy: hybrid (default), neural, or keyword"},"expandQuery":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Generate query variations to improve recall"},"rerank":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Reorder results for improved relevance using LLM"},"generateAnswer":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Generate a natural-language answer to the query"}}},"algolia_add_record":{"id":"algolia_add_record","name":"Algolia Add Record","description":"Add or replace a record in an Algolia index","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Admin API Key"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index"},"objectID":{"type":"string","required":false,"visibility":"user-or-llm","description":"Object ID for the record (auto-generated if not provided)"},"record":{"type":"json","required":true,"visibility":"user-or-llm","description":"JSON object representing the record to add"}}},"algolia_batch_operations":{"id":"algolia_batch_operations","name":"Algolia Batch Operations","description":"Perform batch add, update, partial update, or delete operations on records in an Algolia index","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Admin API Key"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index"},"requests":{"type":"json","required":true,"visibility":"user-or-llm","description":"Array of batch operations. Each item has \\"action\\" (addObject, updateObject, partialUpdateObject, partialUpdateObjectNoCreate, deleteObject, delete, clear) and \\"body\\" (the record data; must include objectID for update/delete; use an empty object {} for the index-level delete/clear actions)"}}},"algolia_browse_records":{"id":"algolia_browse_records","name":"Algolia Browse Records","description":"Browse and iterate over all records in an Algolia index using cursor pagination","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia API Key (must have browse ACL)"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index to browse"},"query":{"type":"string","required":false,"visibility":"user-or-llm","description":"Search query to filter browsed records"},"filters":{"type":"string","required":false,"visibility":"user-or-llm","description":"Filter string to narrow down results"},"attributesToRetrieve":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated list of attributes to retrieve"},"hitsPerPage":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of hits per page (default: 1000, max: 1000)"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous browse response for pagination"},"aroundLatLng":{"type":"string","required":false,"visibility":"user-or-llm","description":"Coordinates for geo-search (e.g., \\"40.71,-74.01\\")"},"aroundRadius":{"type":"string","required":false,"visibility":"user-or-llm","description":"Maximum radius in meters for geo-search, or \\"all\\" for unlimited"},"insideBoundingBox":{"type":"json","required":false,"visibility":"user-or-llm","description":"Bounding box coordinates as [[lat1, lng1, lat2, lng2]] for geo-search"},"insidePolygon":{"type":"json","required":false,"visibility":"user-or-llm","description":"Polygon coordinates as [[lat1, lng1, lat2, lng2, lat3, lng3, ...]] for geo-search"}}},"algolia_clear_records":{"id":"algolia_clear_records","name":"Algolia Clear Records","description":"Clear all records from an Algolia index while keeping settings, synonyms, and rules","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Admin API Key (must have deleteIndex ACL)"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index to clear"}}},"algolia_copy_move_index":{"id":"algolia_copy_move_index","name":"Algolia Copy/Move Index","description":"Copy or move an Algolia index to a new destination","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Admin API Key"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the source index"},"operation":{"type":"string","required":true,"visibility":"user-or-llm","description":"Operation to perform: \\"copy\\" or \\"move\\""},"destination":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the destination index"},"scope":{"type":"json","required":false,"visibility":"user-or-llm","description":"Array of scopes to copy (only for \\"copy\\" operation): [\\"settings\\", \\"synonyms\\", \\"rules\\"]. Omit to copy everything including records."}}},"algolia_delete_by_filter":{"id":"algolia_delete_by_filter","name":"Algolia Delete By Filter","description":"Delete all records matching a filter from an Algolia index","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Admin API Key (must have deleteIndex ACL)"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index"},"filters":{"type":"string","required":false,"visibility":"user-or-llm","description":"Filter expression to match records for deletion (e.g., \\"category:outdated\\")"},"facetFilters":{"type":"json","required":false,"visibility":"user-or-llm","description":"Array of facet filters (e.g., [\\"brand:Acme\\"])"},"numericFilters":{"type":"json","required":false,"visibility":"user-or-llm","description":"Array of numeric filters (e.g., [\\"price > 100\\"])"},"tagFilters":{"type":"json","required":false,"visibility":"user-or-llm","description":"Array of tag filters using the _tags attribute (e.g., [\\"published\\"])"},"aroundLatLng":{"type":"string","required":false,"visibility":"user-or-llm","description":"Coordinates for geo-search filter (e.g., \\"40.71,-74.01\\")"},"aroundRadius":{"type":"string","required":false,"visibility":"user-or-llm","description":"Maximum radius in meters for geo-search, or \\"all\\" for unlimited"},"insideBoundingBox":{"type":"json","required":false,"visibility":"user-or-llm","description":"Bounding box coordinates as [[lat1, lng1, lat2, lng2]] for geo-search filter"},"insidePolygon":{"type":"json","required":false,"visibility":"user-or-llm","description":"Polygon coordinates as [[lat1, lng1, lat2, lng2, lat3, lng3, ...]] for geo-search filter"}}},"algolia_delete_index":{"id":"algolia_delete_index","name":"Algolia Delete Index","description":"Delete an entire Algolia index and all its records","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Admin API Key (must have deleteIndex ACL)"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index to delete"}}},"algolia_delete_record":{"id":"algolia_delete_record","name":"Algolia Delete Record","description":"Delete a record by objectID from an Algolia index","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Admin API Key"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index"},"objectID":{"type":"string","required":true,"visibility":"user-or-llm","description":"The objectID of the record to delete"}}},"algolia_get_record":{"id":"algolia_get_record","name":"Algolia Get Record","description":"Get a record by objectID from an Algolia index","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia API Key"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index"},"objectID":{"type":"string","required":true,"visibility":"user-or-llm","description":"The objectID of the record to retrieve"},"attributesToRetrieve":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated list of attributes to retrieve"}}},"algolia_get_records":{"id":"algolia_get_records","name":"Algolia Get Records","description":"Retrieve multiple records by objectID from one or more Algolia indices","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia API Key"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Default index name for all requests"},"requests":{"type":"json","required":true,"visibility":"user-or-llm","description":"Array of objects specifying records to retrieve. Each must have \\"objectID\\" and optionally \\"indexName\\" and \\"attributesToRetrieve\\"."}}},"algolia_get_settings":{"id":"algolia_get_settings","name":"Algolia Get Settings","description":"Retrieve the settings of an Algolia index","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia API Key"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index"}}},"algolia_get_task_status":{"id":"algolia_get_task_status","name":"Algolia Get Task Status","description":"Check whether an Algolia indexing task has finished publishing","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia API Key"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index the task ran against"},"taskID":{"type":"number","required":true,"visibility":"user-or-llm","description":"The taskID returned by a previous write operation"}}},"algolia_list_indices":{"id":"algolia_list_indices","name":"Algolia List Indices","description":"List all indices in an Algolia application","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia API Key"},"page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Page number for paginating indices (default: not paginated)"},"hitsPerPage":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of indices per page (default: 100)"}}},"algolia_partial_update_record":{"id":"algolia_partial_update_record","name":"Algolia Partial Update Record","description":"Partially update a record in an Algolia index without replacing it entirely","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Admin API Key"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index"},"objectID":{"type":"string","required":true,"visibility":"user-or-llm","description":"The objectID of the record to update"},"attributes":{"type":"json","required":true,"visibility":"user-or-llm","description":"JSON object with attributes to update. Supports built-in operations like {\\"stock\\": {\\"_operation\\": \\"Decrement\\", \\"value\\": 1}}"},"createIfNotExists":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Whether to create the record if it does not exist (default: true)"}}},"algolia_search":{"id":"algolia_search","name":"Algolia Search","description":"Search an Algolia index","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia API Key"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index to search"},"query":{"type":"string","required":true,"visibility":"user-or-llm","description":"Search query text"},"hitsPerPage":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of hits per page (default: 20)"},"page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Page number to retrieve (default: 0)"},"filters":{"type":"string","required":false,"visibility":"user-or-llm","description":"Filter string (e.g., \\"category:electronics AND price < 100\\")"},"attributesToRetrieve":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated list of attributes to retrieve"},"facets":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated list of facet attribute names to retrieve counts for (use \\"*\\" for all)"},"getRankingInfo":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Whether to include detailed ranking information in each hit"},"aroundLatLng":{"type":"string","required":false,"visibility":"user-or-llm","description":"Coordinates for geo-search (e.g., \\"40.71,-74.01\\")"},"aroundRadius":{"type":"string","required":false,"visibility":"user-or-llm","description":"Maximum radius in meters for geo-search, or \\"all\\" for unlimited"},"insideBoundingBox":{"type":"json","required":false,"visibility":"user-or-llm","description":"Bounding box coordinates as [[lat1, lng1, lat2, lng2]] for geo-search"},"insidePolygon":{"type":"json","required":false,"visibility":"user-or-llm","description":"Polygon coordinates as [[lat1, lng1, lat2, lng2, lat3, lng3, ...]] for geo-search"}}},"algolia_update_settings":{"id":"algolia_update_settings","name":"Algolia Update Settings","description":"Update the settings of an Algolia index","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Admin API Key (must have editSettings ACL)"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index"},"settings":{"type":"json","required":true,"visibility":"user-or-llm","description":"JSON object with settings to update (e.g., {\\"searchableAttributes\\": [\\"name\\", \\"description\\"], \\"customRanking\\": [\\"desc(popularity)\\"]})"},"forwardToReplicas":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Whether to apply changes to replica indices (default: false)"}}},"amplitude_event_segmentation":{"id":"amplitude_event_segmentation","name":"Amplitude Event Segmentation","description":"Query event analytics data with segmentation. Get event counts, uniques, averages, and more.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude API Key"},"secretKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude Secret Key"},"eventType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Event type name to analyze"},"start":{"type":"string","required":true,"visibility":"user-or-llm","description":"Start date in YYYYMMDD format"},"end":{"type":"string","required":true,"visibility":"user-or-llm","description":"End date in YYYYMMDD format"},"metric":{"type":"string","required":false,"visibility":"user-or-llm","description":"Metric type: uniques, totals, pct_dau, average, histogram, sums, value_avg, or formula (default: uniques)"},"interval":{"type":"string","required":false,"visibility":"user-or-llm","description":"Time interval: 1 (daily), 7 (weekly), or 30 (monthly)"},"groupBy":{"type":"string","required":false,"visibility":"user-or-llm","description":"Property name to group by (prefix custom user properties with \\"gp:\\")"},"groupBy2":{"type":"string","required":false,"visibility":"user-or-llm","description":"Second property name to group by (prefix custom user properties with \\"gp:\\")"},"limit":{"type":"string","required":false,"visibility":"user-or-llm","description":"Maximum number of group-by values (max 1000)"},"filters":{"type":"string","required":false,"visibility":"user-or-llm","description":"JSON array of filter objects applied to the event, e.g. [{\\"subprop_type\\":\\"event\\",\\"subprop_key\\":\\"city\\",\\"subprop_op\\":\\"is\\",\\"subprop_value\\":[\\"San Francisco\\"]}]"},"formula":{"type":"string","required":false,"visibility":"user-or-llm","description":"Required when metric is \\"formula\\", e.g. \\"UNIQUES(A)/UNIQUES(B)\\""},"segment":{"type":"string","required":false,"visibility":"user-or-llm","description":"JSON segment definition(s) applied to the query"},"dataResidency":{"type":"string","required":false,"visibility":"user-or-llm","description":"Data residency region: \\"us\\" (default) or \\"eu\\""}}},"amplitude_funnels":{"id":"amplitude_funnels","name":"Amplitude Funnels","description":"Analyze conversion rates and drop-off between a sequence of events.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude API Key"},"secretKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude Secret Key"},"events":{"type":"string","required":true,"visibility":"user-or-llm","description":"JSON array of event objects, one per funnel step in order, e.g. [{\\"event_type\\":\\"signup\\"},{\\"event_type\\":\\"purchase\\"}]"},"start":{"type":"string","required":true,"visibility":"user-or-llm","description":"Start date in YYYYMMDD format"},"end":{"type":"string","required":true,"visibility":"user-or-llm","description":"End date in YYYYMMDD format"},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Funnel ordering: \\"ordered\\", \\"unordered\\", or \\"sequential\\" (default: ordered)"},"userType":{"type":"string","required":false,"visibility":"user-or-llm","description":"User type: \\"new\\" or \\"active\\" (default: active)"},"interval":{"type":"string","required":false,"visibility":"user-or-llm","description":"Time interval: -300000 (real-time), -3600000 (hourly), 1 (daily), 7 (weekly), or 30 (monthly)"},"conversionWindowSeconds":{"type":"string","required":false,"visibility":"user-or-llm","description":"Conversion window in seconds (default: 2592000, i.e. 30 days)"},"groupBy":{"type":"string","required":false,"visibility":"user-or-llm","description":"Property to group by (limit: one; prefix custom properties with \\"gp:\\")"},"limit":{"type":"string","required":false,"visibility":"user-or-llm","description":"Maximum number of group-by values (default: 100, max: 1000)"},"segment":{"type":"string","required":false,"visibility":"user-or-llm","description":"JSON segment definition(s) applied to the query"},"dataResidency":{"type":"string","required":false,"visibility":"user-or-llm","description":"Data residency region: \\"us\\" (default) or \\"eu\\""}}},"amplitude_get_active_users":{"id":"amplitude_get_active_users","name":"Amplitude Get Active Users","description":"Get active or new user counts over a date range from the Dashboard REST API.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude API Key"},"secretKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude Secret Key"},"start":{"type":"string","required":true,"visibility":"user-or-llm","description":"Start date in YYYYMMDD format"},"end":{"type":"string","required":true,"visibility":"user-or-llm","description":"End date in YYYYMMDD format"},"metric":{"type":"string","required":false,"visibility":"user-or-llm","description":"Metric type: \\"active\\" or \\"new\\" (default: active)"},"interval":{"type":"string","required":false,"visibility":"user-or-llm","description":"Time interval: 1 (daily), 7 (weekly), or 30 (monthly)"},"groupBy":{"type":"string","required":false,"visibility":"user-or-llm","description":"Property name to group by"},"segment":{"type":"string","required":false,"visibility":"user-or-llm","description":"JSON segment definition(s) applied to the query"},"dataResidency":{"type":"string","required":false,"visibility":"user-or-llm","description":"Data residency region: \\"us\\" (default) or \\"eu\\""}}},"amplitude_get_revenue":{"id":"amplitude_get_revenue","name":"Amplitude Get Revenue","description":"Get revenue LTV data including ARPU, ARPPU, total revenue, and paying user counts.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude API Key"},"secretKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude Secret Key"},"start":{"type":"string","required":true,"visibility":"user-or-llm","description":"Start date in YYYYMMDD format"},"end":{"type":"string","required":true,"visibility":"user-or-llm","description":"End date in YYYYMMDD format"},"metric":{"type":"string","required":false,"visibility":"user-or-llm","description":"Metric: 0 (ARPU), 1 (ARPPU), 2 (Total Revenue), 3 (Paying Users)"},"interval":{"type":"string","required":false,"visibility":"user-or-llm","description":"Time interval: 1 (daily), 7 (weekly), or 30 (monthly)"},"groupBy":{"type":"string","required":false,"visibility":"user-or-llm","description":"Property name to group by (limit: one)"},"segment":{"type":"string","required":false,"visibility":"user-or-llm","description":"JSON segment definition(s) applied to the query"},"dataResidency":{"type":"string","required":false,"visibility":"user-or-llm","description":"Data residency region: \\"us\\" (default) or \\"eu\\""}}},"amplitude_group_identify":{"id":"amplitude_group_identify","name":"Amplitude Group Identify","description":"Set group-level properties in Amplitude. Supports $set, $setOnce, $add, $append, $unset operations.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude API Key"},"groupType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Group classification (e.g., \\"company\\", \\"org_id\\")"},"groupValue":{"type":"string","required":true,"visibility":"user-or-llm","description":"Specific group identifier (e.g., \\"Acme Corp\\")"},"groupProperties":{"type":"string","required":true,"visibility":"user-or-llm","description":"JSON object of group properties. Use operations like $set, $setOnce, $add, $append, $unset."},"dataResidency":{"type":"string","required":false,"visibility":"user-or-llm","description":"Data residency region: \\"us\\" (default) or \\"eu\\""}}},"amplitude_identify_user":{"id":"amplitude_identify_user","name":"Amplitude Identify User","description":"Set user properties in Amplitude using the Identify API. Supports $set, $setOnce, $add, $append, $unset operations.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude API Key"},"userId":{"type":"string","required":false,"visibility":"user-or-llm","description":"User ID (required if no device_id)"},"deviceId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Device ID (required if no user_id)"},"userProperties":{"type":"string","required":true,"visibility":"user-or-llm","description":"JSON object of user properties. Use operations like $set, $setOnce, $add, $append, $unset."},"dataResidency":{"type":"string","required":false,"visibility":"user-or-llm","description":"Data residency region: \\"us\\" (default) or \\"eu\\""}}},"amplitude_list_events":{"id":"amplitude_list_events","name":"Amplitude List Events","description":"List all event types in the Amplitude project with their weekly totals and unique counts.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude API Key"},"secretKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude Secret Key"},"dataResidency":{"type":"string","required":false,"visibility":"user-or-llm","description":"Data residency region: \\"us\\" (default) or \\"eu\\""}}},"amplitude_realtime_active_users":{"id":"amplitude_realtime_active_users","name":"Amplitude Real-time Active Users","description":"Get real-time active user counts at 5-minute granularity for the last 2 days.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude API Key"},"secretKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude Secret Key"},"dataResidency":{"type":"string","required":false,"visibility":"user-or-llm","description":"Data residency region: \\"us\\" (default) or \\"eu\\""}}},"amplitude_retention":{"id":"amplitude_retention","name":"Amplitude Retention","description":"Measure how many users return to perform an action after a starting action.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude API Key"},"secretKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude Secret Key"},"startEvent":{"type":"string","required":true,"visibility":"user-or-llm","description":"JSON starting event object, e.g. {\\"event_type\\":\\"_new\\"} or {\\"event_type\\":\\"_active\\"}"},"returnEvent":{"type":"string","required":true,"visibility":"user-or-llm","description":"JSON returning event object, e.g. {\\"event_type\\":\\"_all\\"} or {\\"event_type\\":\\"_active\\"}"},"start":{"type":"string","required":true,"visibility":"user-or-llm","description":"Start date in YYYYMMDD format"},"end":{"type":"string","required":true,"visibility":"user-or-llm","description":"End date in YYYYMMDD format"},"retentionMode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Retention type: \\"bracket\\", \\"rolling\\", or \\"n-day\\" (default: n-day)"},"retentionBrackets":{"type":"string","required":false,"visibility":"user-or-llm","description":"Required when Retention Mode is \\"bracket\\". Day ranges, e.g. [[0,4]]"},"interval":{"type":"string","required":false,"visibility":"user-or-llm","description":"Time interval: 1 (daily), 7 (weekly), or 30 (monthly)"},"groupBy":{"type":"string","required":false,"visibility":"user-or-llm","description":"Property to group by (limit: one; prefix custom properties with \\"gp:\\")"},"segment":{"type":"string","required":false,"visibility":"user-or-llm","description":"JSON segment definition(s) applied to the query"},"dataResidency":{"type":"string","required":false,"visibility":"user-or-llm","description":"Data residency region: \\"us\\" (default) or \\"eu\\""}}},"amplitude_send_event":{"id":"amplitude_send_event","name":"Amplitude Send Event","description":"Track an event in Amplitude using the HTTP V2 API.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude API Key"},"userId":{"type":"string","required":false,"visibility":"user-or-llm","description":"User ID (required if no device_id)"},"deviceId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Device ID (required if no user_id)"},"eventType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the event (e.g., \\"page_view\\", \\"purchase\\")"},"eventProperties":{"type":"string","required":false,"visibility":"user-or-llm","description":"JSON object of custom event properties"},"userProperties":{"type":"string","required":false,"visibility":"user-or-llm","description":"JSON object of user properties to set (supports $set, $setOnce, $add, $append, $unset)"},"time":{"type":"string","required":false,"visibility":"user-or-llm","description":"Event timestamp in milliseconds since epoch"},"sessionId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Session start time in milliseconds since epoch"},"insertId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Unique ID for deduplication (within 7-day window)"},"appVersion":{"type":"string","required":false,"visibility":"user-or-llm","description":"Application version string"},"platform":{"type":"string","required":false,"visibility":"user-or-llm","description":"Platform (e.g., \\"Web\\", \\"iOS\\", \\"Android\\")"},"country":{"type":"string","required":false,"visibility":"user-or-llm","description":"Two-letter country code"},"language":{"type":"string","required":false,"visibility":"user-or-llm","description":"Language code (e.g., \\"en\\")"},"ip":{"type":"string","required":false,"visibility":"user-or-llm","description":"IP address for geo-location"},"price":{"type":"string","required":false,"visibility":"user-or-llm","description":"Price of the item purchased"},"quantity":{"type":"string","required":false,"visibility":"user-or-llm","description":"Quantity of items purchased"},"revenue":{"type":"string","required":false,"visibility":"user-or-llm","description":"Revenue amount"},"productId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Product identifier"},"revenueType":{"type":"string","required":false,"visibility":"user-or-llm","description":"Revenue type (e.g., \\"purchase\\", \\"refund\\")"},"dataResidency":{"type":"string","required":false,"visibility":"user-or-llm","description":"Data residency region: \\"us\\" (default) or \\"eu\\""}}},"amplitude_user_activity":{"id":"amplitude_user_activity","name":"Amplitude User Activity","description":"Get the event stream for a specific user by their Amplitude ID.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude API Key"},"secretKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude Secret Key"},"amplitudeId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Amplitude internal user ID"},"offset":{"type":"string","required":false,"visibility":"user-or-llm","description":"Offset for pagination (default 0)"},"limit":{"type":"string","required":false,"visibility":"user-or-llm","description":"Maximum number of events to return (default 1000, max 1000)"},"direction":{"type":"string","required":false,"visibility":"user-or-llm","description":"Sort direction: \\"latest\\" or \\"earliest\\" (default: latest)"},"dataResidency":{"type":"string","required":false,"visibility":"user-or-llm","description":"Data residency region: \\"us\\" (default) or \\"eu\\""}}},"amplitude_user_profile":{"id":"amplitude_user_profile","name":"Amplitude User Profile","description":"Get a user profile including properties, cohort memberships, and computed properties. Not available for EU data-residency projects.","version":"1.0.0","params":{"secretKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude Secret Key"},"userId":{"type":"string","required":false,"visibility":"user-or-llm","description":"External user ID (required if no device_id)"},"deviceId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Device ID (required if no user_id)"},"getAmpProps":{"type":"string","required":false,"visibility":"user-or-llm","description":"Include Amplitude user properties (true/false, default: false)"},"getCohortIds":{"type":"string","required":false,"visibility":"user-or-llm","description":"Include cohort IDs the user belongs to (true/false, default: false)"},"getComputations":{"type":"string","required":false,"visibility":"user-or-llm","description":"Include computed user properties (true/false, default: false)"}}},"amplitude_user_search":{"id":"amplitude_user_search","name":"Amplitude User Search","description":"Search for a user by User ID, Device ID, or Amplitude ID using the Dashboard REST API.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude API Key"},"secretKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude Secret Key"},"user":{"type":"string","required":true,"visibility":"user-or-llm","description":"User ID, Device ID, or Amplitude ID to search for"},"dataResidency":{"type":"string","required":false,"visibility":"user-or-llm","description":"Data residency region: \\"us\\" (default) or \\"eu\\""}}},"apify_get_dataset_items":{"id":"apify_get_dataset_items","name":"APIFY Get Dataset Items","description":"Retrieve items stored in an APIFY dataset","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"APIFY API token from console.apify.com/account#/integrations"},"datasetId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Dataset ID to read items from. Example: \\"9RnD3Pql2vGZkc5H5\\""},"itemLimit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Max items to return (1-250000). Default: all items. Example: 500"},"offset":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to skip at the start. Default: 0"},"fields":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated list of fields to include. Example: \\"title,url,price\\""}}},"apify_get_run":{"id":"apify_get_run","name":"APIFY Get Run","description":"Get the status and details of an APIFY actor run","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"APIFY API token from console.apify.com/account#/integrations"},"runId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Actor run ID to fetch. Example: \\"HG7ML7M8z78YcAPEB\\""}}},"apify_run_actor_async":{"id":"apify_run_actor_async","name":"APIFY Run Actor (Async)","description":"Run an APIFY actor asynchronously with polling for long-running tasks","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"APIFY API token from console.apify.com/account#/integrations"},"actorId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Actor ID or username/actor-name. Examples: \\"apify/web-scraper\\", \\"janedoe/my-actor\\", \\"moJRLRc85AitArpNN\\""},"input":{"type":"string","required":false,"visibility":"user-or-llm","description":"Actor input as JSON string. Example: {\\"startUrls\\": [{\\"url\\": \\"https://example.com\\"}], \\"maxPages\\": 10}"},"waitForFinish":{"type":"number","required":false,"visibility":"user-or-llm","description":"Initial wait time in seconds (0-60) before polling starts. Example: 30"},"itemLimit":{"type":"number","required":false,"default":100,"visibility":"user-or-llm","description":"Max dataset items to fetch (1-250000). Default: 100. Example: 500"},"memory":{"type":"number","required":false,"visibility":"user-or-llm","description":"Memory in megabytes allocated for the actor run (128-32768). Example: 1024 for 1GB, 2048 for 2GB"},"timeout":{"type":"number","required":false,"visibility":"user-or-llm","description":"Timeout in seconds for the actor run. Example: 300 for 5 minutes, 3600 for 1 hour"},"build":{"type":"string","required":false,"visibility":"user-or-llm","description":"Actor build to run. Examples: \\"latest\\", \\"beta\\", \\"1.2.3\\", \\"build-tag-name\\""}}},"apify_run_actor_sync":{"id":"apify_run_actor_sync","name":"APIFY Run Actor (Sync)","description":"Run an APIFY actor synchronously and get results (max 5 minutes)","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"APIFY API token from console.apify.com/account#/integrations"},"actorId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Actor ID or username/actor-name. Examples: \\"apify/web-scraper\\", \\"janedoe/my-actor\\", \\"moJRLRc85AitArpNN\\""},"input":{"type":"string","required":false,"visibility":"user-or-llm","description":"Actor input as JSON string. Example: {\\"startUrls\\": [{\\"url\\": \\"https://example.com\\"}], \\"maxPages\\": 10}"},"memory":{"type":"number","required":false,"visibility":"user-or-llm","description":"Memory in megabytes allocated for the actor run (128-32768). Example: 1024 for 1GB, 2048 for 2GB"},"timeout":{"type":"number","required":false,"visibility":"user-or-llm","description":"Timeout in seconds for the actor run. Example: 300 for 5 minutes, 3600 for 1 hour"},"build":{"type":"string","required":false,"visibility":"user-or-llm","description":"Actor build to run. Examples: \\"latest\\", \\"beta\\", \\"1.2.3\\", \\"build-tag-name\\""}}},"apify_run_task":{"id":"apify_run_task","name":"APIFY Run Task","description":"Run a saved APIFY actor task synchronously and get dataset items (max 5 minutes)","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"APIFY API token from console.apify.com/account#/integrations"},"taskId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Task ID or username/task-name. Examples: \\"janedoe/my-task\\", \\"moJRLRc85AitArpNN\\""},"input":{"type":"string","required":false,"visibility":"user-or-llm","description":"JSON string that overrides the task\'s saved input. Example: {\\"startUrls\\": [{\\"url\\": \\"https://example.com\\"}]}"},"itemLimit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Max dataset items to return (1-250000). Example: 500"},"memory":{"type":"number","required":false,"visibility":"user-or-llm","description":"Memory in megabytes allocated for the run (128-32768). Example: 1024 for 1GB"},"timeout":{"type":"number","required":false,"visibility":"user-or-llm","description":"Timeout in seconds for the run. Example: 300 for 5 minutes"},"build":{"type":"string","required":false,"visibility":"user-or-llm","description":"Actor build to run. Examples: \\"latest\\", \\"beta\\", \\"1.2.3\\""}}},"apollo_account_bulk_create":{"id":"apollo_account_bulk_create","name":"Apollo Bulk Create Accounts","description":"Create up to 100 accounts at once in your Apollo database. Set run_dedupe=true to deduplicate by domain, organization_id, and name. Master key required.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key (master key required)"},"accounts":{"type":"array","required":true,"visibility":"user-or-llm","description":"Array of accounts to create (max 100). Each account should include a name, and may optionally include domain, phone, phone_status_cd, raw_address, owner_id, linkedin_url, facebook_url, twitter_url, salesforce_id, and hubspot_id."},"append_label_names":{"type":"array","required":false,"visibility":"user-only","description":"Array of label names to add to ALL accounts in this request"},"run_dedupe":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"When true, performs aggressive deduplication by domain, organization_id, and name (defaults to false)"}}},"apollo_account_bulk_update":{"id":"apollo_account_bulk_update","name":"Apollo Bulk Update Accounts","description":"Update up to 1000 existing accounts at once in your Apollo database (higher limit than contacts!). Each account must include an id field. Master key required.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key (master key required)"},"account_ids":{"type":"array","required":false,"visibility":"user-or-llm","description":"Array of account IDs to update with the same values (max 1000). Use with name/owner_id for uniform updates. Use either this OR account_attributes."},"name":{"type":"string","required":false,"visibility":"user-or-llm","description":"When using account_ids, apply this name to all accounts"},"owner_id":{"type":"string","required":false,"visibility":"user-or-llm","description":"When using account_ids, apply this owner to all accounts"},"account_stage_id":{"type":"string","required":false,"visibility":"user-or-llm","description":"When using account_ids, apply this account stage to all accounts"},"account_attributes":{"type":"json","required":false,"visibility":"user-or-llm","description":"Array of account objects with individual updates (each must include id). Example: [{\\"id\\": \\"acc1\\", \\"name\\": \\"Acme\\", \\"owner_id\\": \\"u1\\", \\"account_stage_id\\": \\"s1\\", \\"typed_custom_fields\\": {\\"field_id\\": \\"value\\"}}]"},"async":{"type":"boolean","required":false,"visibility":"user-only","description":"When true, processes the update asynchronously. Only supported when using account_ids; returns 422 if used with account_attributes."}}},"apollo_account_create":{"id":"apollo_account_create","name":"Apollo Create Account","description":"Create a new account (company) in your Apollo database","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key (master key required)"},"name":{"type":"string","required":true,"visibility":"user-or-llm","description":"Company name (e.g., \\"Acme Corporation\\")"},"domain":{"type":"string","required":false,"visibility":"user-or-llm","description":"Company domain without www. prefix (e.g., \\"acme.com\\")"},"phone":{"type":"string","required":false,"visibility":"user-or-llm","description":"Primary phone number for the account"},"owner_id":{"type":"string","required":false,"visibility":"user-only","description":"Apollo user ID of the account owner"},"account_stage_id":{"type":"string","required":false,"visibility":"user-only","description":"Apollo ID for the account stage to assign this account to"},"raw_address":{"type":"string","required":false,"visibility":"user-or-llm","description":"Corporate location (e.g., \\"San Francisco, CA, USA\\")"},"typed_custom_fields":{"type":"json","required":false,"visibility":"user-only","description":"Custom field values as { custom_field_id: value } map"}}},"apollo_account_search":{"id":"apollo_account_search","name":"Apollo Search Accounts","description":"Search your team\'s accounts in Apollo. Display limit: 50,000 records (100 records per page, 500 pages max). Use filters to narrow results. Master key required.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key (master key required)"},"q_organization_name":{"type":"string","required":false,"visibility":"user-or-llm","description":"Filter accounts by organization name (partial-match search)"},"account_stage_ids":{"type":"array","required":false,"visibility":"user-only","description":"Filter by account stage IDs"},"account_label_ids":{"type":"array","required":false,"visibility":"user-only","description":"Filter by account label IDs"},"sort_by_field":{"type":"string","required":false,"visibility":"user-or-llm","description":"Sort field: \\"account_last_activity_date\\", \\"account_created_at\\", or \\"account_updated_at\\""},"sort_ascending":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Sort ascending when true. Defaults to descending."},"page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Page number for pagination (e.g., 1, 2, 3)"},"per_page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Results per page, max 100 (e.g., 25, 50, 100)"}}},"apollo_account_update":{"id":"apollo_account_update","name":"Apollo Update Account","description":"Update an existing account in your Apollo database","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"account_id":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the account to update (e.g., \\"acc_abc123\\")"},"name":{"type":"string","required":false,"visibility":"user-or-llm","description":"Company name (e.g., \\"Acme Corporation\\")"},"domain":{"type":"string","required":false,"visibility":"user-or-llm","description":"Company domain (e.g., \\"acme.com\\")"},"phone":{"type":"string","required":false,"visibility":"user-or-llm","description":"Company phone number"},"owner_id":{"type":"string","required":false,"visibility":"user-only","description":"Apollo user ID of the account owner"},"account_stage_id":{"type":"string","required":false,"visibility":"user-only","description":"Apollo ID for the account stage to assign this account to"},"raw_address":{"type":"string","required":false,"visibility":"user-or-llm","description":"Corporate location (e.g., \\"San Francisco, CA, USA\\")"},"typed_custom_fields":{"type":"json","required":false,"visibility":"user-only","description":"Custom field values as { custom_field_id: value } map"}}},"apollo_contact_bulk_create":{"id":"apollo_contact_bulk_create","name":"Apollo Bulk Create Contacts","description":"Create up to 100 contacts at once in your Apollo database. Supports deduplication to prevent creating duplicate contacts. Master key required.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key (master key required)"},"contacts":{"type":"array","required":true,"visibility":"user-or-llm","description":"Array of contacts to create (max 100). Each contact may include first_name, last_name, email, title, organization_name, account_id, owner_id, contact_stage_id, linkedin_url, phone (single string) or phone_numbers (array of {raw_number, position}), contact_emails, typed_custom_fields, and CRM IDs (salesforce_contact_id, hubspot_id, team_id) for cross-system matching"},"append_label_names":{"type":"array","required":false,"visibility":"user-or-llm","description":"Label names to add to all contacts in this request (e.g., [\\"Hot Lead\\"])"},"run_dedupe":{"type":"boolean","required":false,"visibility":"user-only","description":"Enable deduplication to prevent creating duplicate contacts. When true, existing contacts are returned without modification"}}},"apollo_contact_bulk_update":{"id":"apollo_contact_bulk_update","name":"Apollo Bulk Update Contacts","description":"Update up to 100 existing contacts at once in your Apollo database. Each contact must include an id field. Master key required.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key (master key required)"},"contact_ids":{"type":"array","required":false,"visibility":"user-or-llm","description":"Array of contact IDs to update. Must be paired with an object-form contact_attributes specifying the fields to apply uniformly to all listed contacts."},"contact_attributes":{"type":"json","required":false,"visibility":"user-or-llm","description":"Required. Either an array of per-contact updates (each with id) — used standalone — or a single object of attributes to apply to all contact_ids. Supported fields: owner_id, email, organization_name, title, first_name, last_name, account_id, present_raw_address, linkedin_url, typed_custom_fields"},"async":{"type":"boolean","required":false,"visibility":"user-only","description":"Force asynchronous processing. Automatically enabled for >100 contacts"}}},"apollo_contact_create":{"id":"apollo_contact_create","name":"Apollo Create Contact","description":"Create a new contact in your Apollo database","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"first_name":{"type":"string","required":true,"visibility":"user-or-llm","description":"First name of the contact"},"last_name":{"type":"string","required":true,"visibility":"user-or-llm","description":"Last name of the contact"},"email":{"type":"string","required":false,"visibility":"user-or-llm","description":"Email address of the contact"},"title":{"type":"string","required":false,"visibility":"user-or-llm","description":"Job title (e.g., \\"VP of Sales\\", \\"Software Engineer\\")"},"account_id":{"type":"string","required":false,"visibility":"user-or-llm","description":"Apollo account ID to associate with (e.g., \\"acc_abc123\\")"},"owner_id":{"type":"string","required":false,"visibility":"user-only","description":"User ID of the contact owner (accepted by Apollo but not officially documented for POST /contacts)"},"organization_name":{"type":"string","required":false,"visibility":"user-or-llm","description":"Name of the contact\'s employer (e.g., \\"Apollo\\")"},"website_url":{"type":"string","required":false,"visibility":"user-or-llm","description":"Corporate website URL (e.g., \\"https://www.apollo.io/\\")"},"label_names":{"type":"array","required":false,"visibility":"user-or-llm","description":"Lists/labels to add the contact to (e.g., [\\"Prospects\\"])"},"contact_stage_id":{"type":"string","required":false,"visibility":"user-or-llm","description":"Apollo ID for the contact stage"},"present_raw_address":{"type":"string","required":false,"visibility":"user-or-llm","description":"Personal location for the contact (e.g., \\"Atlanta, United States\\")"},"direct_phone":{"type":"string","required":false,"visibility":"user-or-llm","description":"Primary phone number"},"corporate_phone":{"type":"string","required":false,"visibility":"user-or-llm","description":"Work/office phone number"},"mobile_phone":{"type":"string","required":false,"visibility":"user-or-llm","description":"Mobile phone number"},"home_phone":{"type":"string","required":false,"visibility":"user-or-llm","description":"Home phone number"},"other_phone":{"type":"string","required":false,"visibility":"user-or-llm","description":"Alternative phone number"},"typed_custom_fields":{"type":"json","required":false,"visibility":"user-or-llm","description":"Custom field values keyed by custom field ID"},"run_dedupe":{"type":"boolean","required":false,"visibility":"user-only","description":"When true, Apollo deduplicates against existing contacts"}}},"apollo_contact_search":{"id":"apollo_contact_search","name":"Apollo Search Contacts","description":"Search your team\'s contacts in Apollo","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"q_keywords":{"type":"string","required":false,"visibility":"user-or-llm","description":"Keywords to search for"},"contact_stage_ids":{"type":"array","required":false,"visibility":"user-only","description":"Filter by contact stage IDs"},"contact_label_ids":{"type":"array","required":false,"visibility":"user-only","description":"Filter by Apollo label IDs (lists)"},"sort_by_field":{"type":"string","required":false,"visibility":"user-only","description":"Sort field: contact_last_activity_date, contact_email_last_opened_at, contact_email_last_clicked_at, contact_created_at, or contact_updated_at"},"sort_ascending":{"type":"boolean","required":false,"visibility":"user-only","description":"When true, sort ascending. Must be used together with sort_by_field"},"page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Page number for pagination (e.g., 1, 2, 3)"},"per_page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Results per page, max 100 (e.g., 25, 50, 100)"}}},"apollo_contact_update":{"id":"apollo_contact_update","name":"Apollo Update Contact","description":"Update an existing contact in your Apollo database","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"contact_id":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the contact to update (e.g., \\"con_abc123\\")"},"first_name":{"type":"string","required":false,"visibility":"user-or-llm","description":"First name of the contact"},"last_name":{"type":"string","required":false,"visibility":"user-or-llm","description":"Last name of the contact"},"email":{"type":"string","required":false,"visibility":"user-or-llm","description":"Email address"},"title":{"type":"string","required":false,"visibility":"user-or-llm","description":"Job title (e.g., \\"VP of Sales\\", \\"Software Engineer\\")"},"account_id":{"type":"string","required":false,"visibility":"user-or-llm","description":"Apollo account ID (e.g., \\"acc_abc123\\")"},"owner_id":{"type":"string","required":false,"visibility":"user-only","description":"User ID of the contact owner (accepted by Apollo but not officially documented for PATCH /contacts/{id})"},"organization_name":{"type":"string","required":false,"visibility":"user-or-llm","description":"Name of the contact\'s employer (e.g., \\"Apollo\\")"},"website_url":{"type":"string","required":false,"visibility":"user-or-llm","description":"Corporate website URL (e.g., \\"https://www.apollo.io/\\")"},"label_names":{"type":"array","required":false,"visibility":"user-or-llm","description":"Lists/labels to add the contact to (e.g., [\\"Prospects\\"])"},"contact_stage_id":{"type":"string","required":false,"visibility":"user-or-llm","description":"Apollo ID for the contact stage"},"present_raw_address":{"type":"string","required":false,"visibility":"user-or-llm","description":"Personal location for the contact (e.g., \\"Atlanta, United States\\")"},"direct_phone":{"type":"string","required":false,"visibility":"user-or-llm","description":"Primary phone number"},"corporate_phone":{"type":"string","required":false,"visibility":"user-or-llm","description":"Work/office phone number"},"mobile_phone":{"type":"string","required":false,"visibility":"user-or-llm","description":"Mobile phone number"},"home_phone":{"type":"string","required":false,"visibility":"user-or-llm","description":"Home phone number"},"other_phone":{"type":"string","required":false,"visibility":"user-or-llm","description":"Alternative phone number"},"typed_custom_fields":{"type":"json","required":false,"visibility":"user-or-llm","description":"Custom field values keyed by custom field ID"}}},"apollo_email_accounts":{"id":"apollo_email_accounts","name":"Apollo Get Email Accounts","description":"Get list of team\'s linked email accounts in Apollo","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key (master key required)"}}},"apollo_opportunity_create":{"id":"apollo_opportunity_create","name":"Apollo Create Opportunity","description":"Create a new deal for an account in your Apollo database (master key required)","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key (master key required)"},"name":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the opportunity/deal (e.g., \\"Enterprise License - Q1\\")"},"account_id":{"type":"string","required":false,"visibility":"user-or-llm","description":"ID of the account this opportunity belongs to (e.g., \\"acc_abc123\\")"},"amount":{"type":"string","required":false,"visibility":"user-or-llm","description":"Monetary value as a plain number string with no commas or currency symbols"},"opportunity_stage_id":{"type":"string","required":false,"visibility":"user-only","description":"ID of the opportunity stage"},"owner_id":{"type":"string","required":false,"visibility":"user-only","description":"User ID of the opportunity owner"},"closed_date":{"type":"string","required":false,"visibility":"user-or-llm","description":"Expected close date in YYYY-MM-DD format"},"typed_custom_fields":{"type":"json","required":false,"visibility":"user-only","description":"Custom field values as { custom_field_id: value } map"}}},"apollo_opportunity_get":{"id":"apollo_opportunity_get","name":"Apollo Get Opportunity","description":"Retrieve complete details of a specific deal/opportunity by ID","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"opportunity_id":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the opportunity to retrieve (e.g., \\"opp_abc123\\")"}}},"apollo_opportunity_search":{"id":"apollo_opportunity_search","name":"Apollo Search Opportunities","description":"Search and list all deals/opportunities in your team\'s Apollo account","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"sort_by_field":{"type":"string","required":false,"visibility":"user-or-llm","description":"Sort field: \\"amount\\", \\"is_closed\\", or \\"is_won\\""},"page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Page number for pagination (e.g., 1, 2, 3)"},"per_page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Results per page, max 100 (e.g., 25, 50, 100)"}}},"apollo_opportunity_update":{"id":"apollo_opportunity_update","name":"Apollo Update Opportunity","description":"Update an existing deal/opportunity in your Apollo database","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"opportunity_id":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the opportunity to update (e.g., \\"opp_abc123\\")"},"name":{"type":"string","required":false,"visibility":"user-or-llm","description":"Name of the opportunity/deal (e.g., \\"Enterprise License - Q1\\")"},"amount":{"type":"string","required":false,"visibility":"user-or-llm","description":"Monetary value as a plain number string with no commas or currency symbols"},"opportunity_stage_id":{"type":"string","required":false,"visibility":"user-only","description":"ID of the opportunity stage"},"owner_id":{"type":"string","required":false,"visibility":"user-only","description":"User ID of the opportunity owner"},"closed_date":{"type":"string","required":false,"visibility":"user-or-llm","description":"Expected close date in YYYY-MM-DD format"},"typed_custom_fields":{"type":"json","required":false,"visibility":"user-only","description":"Custom field values as { custom_field_id: value } map"}}},"apollo_organization_bulk_enrich":{"id":"apollo_organization_bulk_enrich","name":"Apollo Bulk Organization Enrichment","description":"Enrich data for up to 10 organizations at once using Apollo","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"domains":{"type":"array","required":true,"visibility":"user-or-llm","description":"Array of company domains to enrich (max 10, no www. or @, e.g., [\\"apollo.io\\", \\"stripe.com\\"])"}}},"apollo_organization_enrich":{"id":"apollo_organization_enrich","name":"Apollo Organization Enrichment","description":"Enrich data for a single organization using Apollo","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"domain":{"type":"string","required":true,"visibility":"user-or-llm","description":"Company domain (e.g., \\"apollo.io\\", \\"acme.com\\")"}}},"apollo_organization_search":{"id":"apollo_organization_search","name":"Apollo Organization Search","description":"Search Apollo\'s database for companies using filters","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"organization_locations":{"type":"array","required":false,"visibility":"user-or-llm","description":"Company HQ locations (cities, US states, or countries)"},"organization_not_locations":{"type":"array","required":false,"visibility":"user-or-llm","description":"Exclude companies whose HQ is in these locations"},"organization_num_employees_ranges":{"type":"array","required":false,"visibility":"user-or-llm","description":"Employee count ranges as \\"min,max\\" strings (e.g., [\\"1,10\\", \\"250,500\\", \\"10000,20000\\"])"},"q_organization_keyword_tags":{"type":"array","required":false,"visibility":"user-or-llm","description":"Industry or keyword tags"},"q_organization_name":{"type":"string","required":false,"visibility":"user-or-llm","description":"Organization name to search for (e.g., \\"Acme\\", \\"TechCorp\\")"},"organization_ids":{"type":"array","required":false,"visibility":"user-or-llm","description":"Apollo organization IDs to include (e.g., [\\"5e66b6381e05b4008c8331b8\\"])"},"q_organization_domains_list":{"type":"array","required":false,"visibility":"user-or-llm","description":"Domain names to filter by (no www. or @, up to 1,000)"},"page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Page number for pagination (e.g., 1, 2, 3)"},"per_page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Results per page, max 100 (e.g., 25, 50, 100)"}}},"apollo_people_bulk_enrich":{"id":"apollo_people_bulk_enrich","name":"Apollo Bulk People Enrichment","description":"Enrich data for up to 10 people at once using Apollo","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"people":{"type":"array","required":true,"visibility":"user-or-llm","description":"Array of people to enrich (max 10)"},"reveal_personal_emails":{"type":"boolean","required":false,"visibility":"user-only","description":"Reveal personal email addresses (uses credits)"},"reveal_phone_number":{"type":"boolean","required":false,"visibility":"user-only","description":"Reveal phone numbers (uses credits, requires webhook_url)"},"webhook_url":{"type":"string","required":false,"visibility":"user-only","description":"Webhook URL for async phone number delivery (required when reveal_phone_number is true)"}}},"apollo_people_enrich":{"id":"apollo_people_enrich","name":"Apollo People Enrichment","description":"Enrich data for a single person using Apollo","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"first_name":{"type":"string","required":false,"visibility":"user-or-llm","description":"First name of the person"},"last_name":{"type":"string","required":false,"visibility":"user-or-llm","description":"Last name of the person"},"name":{"type":"string","required":false,"visibility":"user-or-llm","description":"Full name of the person (alternative to first_name/last_name)"},"id":{"type":"string","required":false,"visibility":"user-or-llm","description":"Apollo ID for the person"},"hashed_email":{"type":"string","required":false,"visibility":"user-or-llm","description":"MD5 or SHA-256 hashed email"},"email":{"type":"string","required":false,"visibility":"user-or-llm","description":"Email address of the person"},"organization_name":{"type":"string","required":false,"visibility":"user-or-llm","description":"Company name where the person works"},"domain":{"type":"string","required":false,"visibility":"user-or-llm","description":"Company domain (e.g., \\"apollo.io\\", \\"acme.com\\")"},"linkedin_url":{"type":"string","required":false,"visibility":"user-or-llm","description":"LinkedIn profile URL"},"reveal_personal_emails":{"type":"boolean","required":false,"visibility":"user-only","description":"Reveal personal email addresses (uses credits)"},"reveal_phone_number":{"type":"boolean","required":false,"visibility":"user-only","description":"Reveal phone numbers (uses credits, requires webhook_url)"},"webhook_url":{"type":"string","required":false,"visibility":"user-only","description":"Webhook URL for async phone number delivery (required when reveal_phone_number is true)"}}},"apollo_people_search":{"id":"apollo_people_search","name":"Apollo People Search","description":"Search Apollo\'s database for people using demographic filters","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"person_titles":{"type":"array","required":false,"visibility":"user-or-llm","description":"Job titles to search for (e.g., [\\"CEO\\", \\"VP of Sales\\"])"},"include_similar_titles":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Whether to return people with job titles similar to person_titles"},"person_locations":{"type":"array","required":false,"visibility":"user-or-llm","description":"Locations to search in (e.g., [\\"San Francisco, CA\\", \\"New York, NY\\"])"},"person_seniorities":{"type":"array","required":false,"visibility":"user-or-llm","description":"Seniority levels (one of: owner, founder, c_suite, partner, vp, head, director, manager, senior, entry, intern)"},"organization_ids":{"type":"array","required":false,"visibility":"user-or-llm","description":"Apollo organization IDs to filter by (e.g., [\\"5e66b6381e05b4008c8331b8\\"])"},"organization_names":{"type":"array","required":false,"visibility":"user-or-llm","description":"Company names to search within (legacy filter)"},"organization_locations":{"type":"array","required":false,"visibility":"user-or-llm","description":"Headquarters locations of the people\'s current employer (e.g., [\'texas\', \'tokyo\', \'spain\'])"},"q_organization_domains_list":{"type":"array","required":false,"visibility":"user-or-llm","description":"Employer domain names (e.g., [\\"apollo.io\\", \\"microsoft.com\\"]) — up to 1,000, no www. or @"},"organization_num_employees_ranges":{"type":"array","required":false,"visibility":"user-or-llm","description":"Employee count ranges for the person\'s current employer. Each entry is \\"min,max\\" (e.g., [\\"1,10\\", \\"250,500\\", \\"10000,20000\\"])"},"contact_email_status":{"type":"array","required":false,"visibility":"user-or-llm","description":"Email statuses to filter by: \\"verified\\", \\"unverified\\", \\"likely to engage\\", \\"unavailable\\""},"q_keywords":{"type":"string","required":false,"visibility":"user-or-llm","description":"Keywords to search for"},"page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Page number for pagination, default 1 (e.g., 1, 2, 3)"},"per_page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Results per page, default 25, max 100 (e.g., 25, 50, 100)"}}},"apollo_sequence_add_contacts":{"id":"apollo_sequence_add_contacts","name":"Apollo Add Contacts to Sequence","description":"Add contacts to an Apollo sequence","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key (master key required)"},"sequence_id":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the sequence to add contacts to (e.g., \\"seq_abc123\\")"},"contact_ids":{"type":"array","required":false,"visibility":"user-or-llm","description":"Array of contact IDs to add to the sequence (e.g., [\\"con_abc123\\", \\"con_def456\\"]). Either contact_ids or label_names must be provided."},"label_names":{"type":"array","required":false,"visibility":"user-or-llm","description":"Array of label names to identify contacts to add to the sequence. Either contact_ids or label_names must be provided."},"send_email_from_email_account_id":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the email account to send from. Use the Get Email Accounts operation to look this up."},"send_email_from_email_address":{"type":"string","required":false,"visibility":"user-only","description":"Specific email address to send from within the email account."},"sequence_no_email":{"type":"boolean","required":false,"visibility":"user-only","description":"Add contacts even if they have no email address"},"sequence_unverified_email":{"type":"boolean","required":false,"visibility":"user-only","description":"Add contacts with unverified email addresses"},"sequence_job_change":{"type":"boolean","required":false,"visibility":"user-only","description":"Add contacts who recently changed jobs"},"sequence_active_in_other_campaigns":{"type":"boolean","required":false,"visibility":"user-only","description":"Add contacts active in other campaigns"},"sequence_finished_in_other_campaigns":{"type":"boolean","required":false,"visibility":"user-only","description":"Add contacts who finished other campaigns"},"sequence_same_company_in_same_campaign":{"type":"boolean","required":false,"visibility":"user-only","description":"Add contacts even if others from the same company are in the sequence"},"contacts_without_ownership_permission":{"type":"boolean","required":false,"visibility":"user-only","description":"Add contacts without ownership permission"},"add_if_in_queue":{"type":"boolean","required":false,"visibility":"user-only","description":"Add contacts even if they are in the queue"},"contact_verification_skipped":{"type":"boolean","required":false,"visibility":"user-only","description":"Skip contact verification when adding"},"user_id":{"type":"string","required":false,"visibility":"user-only","description":"ID of the user performing the action"},"status":{"type":"string","required":false,"visibility":"user-only","description":"Initial status for added contacts: \\"active\\" or \\"paused\\""},"auto_unpause_at":{"type":"string","required":false,"visibility":"user-only","description":"ISO 8601 datetime to automatically unpause contacts"}}},"apollo_sequence_search":{"id":"apollo_sequence_search","name":"Apollo Search Sequences","description":"Search for sequences/campaigns in your team\'s Apollo account (master key required)","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key (master key required)"},"q_name":{"type":"string","required":false,"visibility":"user-or-llm","description":"Search sequences by name (e.g., \\"Outbound Q1\\", \\"Follow-up\\")"},"page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Page number for pagination (e.g., 1, 2, 3)"},"per_page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Results per page, max 100 (e.g., 25, 50, 100)"}}},"apollo_task_create":{"id":"apollo_task_create","name":"Apollo Create Task","description":"Create one or more tasks in Apollo (one task per contact_id, master key required)","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key (master key required)"},"user_id":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the Apollo user the task is assigned to"},"contact_ids":{"type":"array","required":true,"visibility":"user-or-llm","description":"Array of contact IDs. One task is created per contact."},"priority":{"type":"string","required":false,"visibility":"user-or-llm","description":"Task priority: \\"high\\", \\"medium\\", or \\"low\\" (defaults to \\"medium\\")"},"due_at":{"type":"string","required":true,"visibility":"user-or-llm","description":"Due date/time in ISO 8601 format (e.g., \\"2024-12-31T23:59:59Z\\")"},"type":{"type":"string","required":true,"visibility":"user-or-llm","description":"Task type: \\"call\\", \\"outreach_manual_email\\", \\"linkedin_step_connect\\", \\"linkedin_step_message\\", \\"linkedin_step_view_profile\\", \\"linkedin_step_interact_post\\", or \\"action_item\\""},"status":{"type":"string","required":true,"visibility":"user-or-llm","description":"Task status: \\"scheduled\\", \\"completed\\", or \\"skipped\\""},"note":{"type":"string","required":false,"visibility":"user-or-llm","description":"Free-form note providing context for the task"}}},"apollo_task_search":{"id":"apollo_task_search","name":"Apollo Search Tasks","description":"Search for tasks in Apollo","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key (master key required)"},"sort_by_field":{"type":"string","required":false,"visibility":"user-or-llm","description":"Sort field: \\"task_due_at\\" or \\"task_priority\\""},"open_factor_names":{"type":"array","required":false,"visibility":"user-or-llm","description":"Filter by status. Common values: [\\"task_types\\"] for open tasks, [\\"task_completed_at\\"] for completed tasks."},"page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Page number for pagination (e.g., 1, 2, 3)"},"per_page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Results per page, max 100 (e.g., 25, 50, 100)"}}},"appconfig_create_application":{"id":"appconfig_create_application","name":"AppConfig Create Application","description":"Create an application in AWS AppConfig","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"name":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the application to create"},"description":{"type":"string","required":false,"visibility":"user-or-llm","description":"Description of the application"}}},"appconfig_create_configuration_profile":{"id":"appconfig_create_configuration_profile","name":"AppConfig Create Configuration Profile","description":"Create a configuration profile in an AWS AppConfig application","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID to create the configuration profile in"},"name":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the configuration profile"},"locationUri":{"type":"string","required":true,"visibility":"user-or-llm","description":"Where the configuration is stored. Use \\"hosted\\" for AppConfig-hosted configurations, or an SSM/S3 URI"},"description":{"type":"string","required":false,"visibility":"user-or-llm","description":"Description of the configuration profile"},"retrievalRoleArn":{"type":"string","required":false,"visibility":"user-or-llm","description":"ARN of an IAM role to retrieve the configuration (required for non-hosted URIs)"},"type":{"type":"string","required":false,"visibility":"user-or-llm","description":"Profile type: AWS.Freeform (default) or AWS.AppConfig.FeatureFlags"}}},"appconfig_create_environment":{"id":"appconfig_create_environment","name":"AppConfig Create Environment","description":"Create an environment for an AWS AppConfig application","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID to create the environment in"},"name":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the environment to create"},"description":{"type":"string","required":false,"visibility":"user-or-llm","description":"Description of the environment"}}},"appconfig_create_hosted_configuration_version":{"id":"appconfig_create_hosted_configuration_version","name":"AppConfig Create Hosted Configuration Version","description":"Create a new hosted configuration version for an AppConfig configuration profile","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID that owns the configuration profile"},"configurationProfileId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The configuration profile ID to add the version to"},"content":{"type":"string","required":true,"visibility":"user-or-llm","description":"The configuration content (e.g., a JSON or YAML document)"},"contentType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Content type of the configuration (e.g., application/json, text/plain)"},"description":{"type":"string","required":false,"visibility":"user-or-llm","description":"Description of the configuration version"},"latestVersionNumber":{"type":"number","required":false,"visibility":"user-or-llm","description":"The version number of the latest version, used for optimistic concurrency"},"versionLabel":{"type":"string","required":false,"visibility":"user-or-llm","description":"A user-defined label for the configuration version"}}},"appconfig_delete_application":{"id":"appconfig_delete_application","name":"AppConfig Delete Application","description":"Delete an AWS AppConfig application","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID to delete"}}},"appconfig_delete_configuration_profile":{"id":"appconfig_delete_configuration_profile","name":"AppConfig Delete Configuration Profile","description":"Delete an AWS AppConfig configuration profile","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID that owns the configuration profile"},"configurationProfileId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The configuration profile ID to delete"}}},"appconfig_delete_environment":{"id":"appconfig_delete_environment","name":"AppConfig Delete Environment","description":"Delete an AWS AppConfig environment","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID that owns the environment"},"environmentId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The environment ID to delete"}}},"appconfig_delete_hosted_configuration_version":{"id":"appconfig_delete_hosted_configuration_version","name":"AppConfig Delete Hosted Configuration Version","description":"Delete a specific hosted configuration version from an AppConfig profile","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID that owns the configuration profile"},"configurationProfileId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The configuration profile ID that owns the version"},"versionNumber":{"type":"number","required":true,"visibility":"user-or-llm","description":"The version number to delete"}}},"appconfig_get_application":{"id":"appconfig_get_application","name":"AppConfig Get Application","description":"Get details about a single AWS AppConfig application","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID to retrieve"}}},"appconfig_get_configuration":{"id":"appconfig_get_configuration","name":"AppConfig Get Configuration","description":"Retrieve the latest deployed configuration for an AppConfig application, environment, and profile","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID or name to retrieve configuration for"},"environmentId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The environment ID or name to retrieve configuration for"},"configurationProfileId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The configuration profile ID or name to retrieve"}}},"appconfig_get_configuration_profile":{"id":"appconfig_get_configuration_profile","name":"AppConfig Get Configuration Profile","description":"Get details about a single AWS AppConfig configuration profile","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID that owns the configuration profile"},"configurationProfileId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The configuration profile ID to retrieve"}}},"appconfig_get_deployment":{"id":"appconfig_get_deployment","name":"AppConfig Get Deployment","description":"Get details about a specific AWS AppConfig deployment","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID of the deployment"},"environmentId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The environment ID of the deployment"},"deploymentNumber":{"type":"number","required":true,"visibility":"user-or-llm","description":"The sequence number of the deployment"}}},"appconfig_get_environment":{"id":"appconfig_get_environment","name":"AppConfig Get Environment","description":"Get details about a single AWS AppConfig environment","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID that owns the environment"},"environmentId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The environment ID to retrieve"}}},"appconfig_get_hosted_configuration_version":{"id":"appconfig_get_hosted_configuration_version","name":"AppConfig Get Hosted Configuration Version","description":"Retrieve a specific hosted configuration version from an AppConfig profile","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID that owns the configuration profile"},"configurationProfileId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The configuration profile ID to read the version from"},"versionNumber":{"type":"number","required":true,"visibility":"user-or-llm","description":"The version number to retrieve"}}},"appconfig_list_applications":{"id":"appconfig_list_applications","name":"AppConfig List Applications","description":"List applications in AWS AppConfig","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"maxResults":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of applications to return (1-50)"},"nextToken":{"type":"string","required":false,"visibility":"user-or-llm","description":"Pagination token from a previous response"}}},"appconfig_list_configuration_profiles":{"id":"appconfig_list_configuration_profiles","name":"AppConfig List Configuration Profiles","description":"List configuration profiles for an AWS AppConfig application","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID that owns the configuration profiles"},"maxResults":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of configuration profiles to return (1-50)"},"nextToken":{"type":"string","required":false,"visibility":"user-or-llm","description":"Pagination token from a previous response"}}},"appconfig_list_deployment_strategies":{"id":"appconfig_list_deployment_strategies","name":"AppConfig List Deployment Strategies","description":"List deployment strategies available in AWS AppConfig","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"maxResults":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of deployment strategies to return (1-50)"},"nextToken":{"type":"string","required":false,"visibility":"user-or-llm","description":"Pagination token from a previous response"}}},"appconfig_list_deployments":{"id":"appconfig_list_deployments","name":"AppConfig List Deployments","description":"List deployments for an AWS AppConfig environment","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID of the deployments"},"environmentId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The environment ID of the deployments"},"maxResults":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of deployments to return (1-50)"},"nextToken":{"type":"string","required":false,"visibility":"user-or-llm","description":"Pagination token from a previous response"}}},"appconfig_list_environments":{"id":"appconfig_list_environments","name":"AppConfig List Environments","description":"List environments for an AWS AppConfig application","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID that owns the environments"},"maxResults":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of environments to return (1-50)"},"nextToken":{"type":"string","required":false,"visibility":"user-or-llm","description":"Pagination token from a previous response"}}},"appconfig_list_hosted_configuration_versions":{"id":"appconfig_list_hosted_configuration_versions","name":"AppConfig List Hosted Configuration Versions","description":"List hosted configuration versions for an AWS AppConfig configuration profile","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID that owns the configuration profile"},"configurationProfileId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The configuration profile ID to list versions for"},"maxResults":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of versions to return (1-50)"},"nextToken":{"type":"string","required":false,"visibility":"user-or-llm","description":"Pagination token from a previous response"}}},"appconfig_start_deployment":{"id":"appconfig_start_deployment","name":"AppConfig Start Deployment","description":"Start deploying a configuration version to an AWS AppConfig environment","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID to deploy in"},"environmentId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The environment ID to deploy to"},"deploymentStrategyId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The deployment strategy ID to use"},"configurationProfileId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The configuration profile ID to deploy"},"configurationVersion":{"type":"string","required":true,"visibility":"user-or-llm","description":"The configuration version to deploy"},"description":{"type":"string","required":false,"visibility":"user-or-llm","description":"Description of the deployment"}}},"appconfig_stop_deployment":{"id":"appconfig_stop_deployment","name":"AppConfig Stop Deployment","description":"Stop an in-progress AWS AppConfig deployment","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID of the deployment"},"environmentId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The environment ID of the deployment"},"deploymentNumber":{"type":"number","required":true,"visibility":"user-or-llm","description":"The sequence number of the deployment to stop"}}},"appconfig_update_application":{"id":"appconfig_update_application","name":"AppConfig Update Application","description":"Update the name or description of an AWS AppConfig application","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID to update"},"name":{"type":"string","required":false,"visibility":"user-or-llm","description":"New name for the application"},"description":{"type":"string","required":false,"visibility":"user-or-llm","description":"New description for the application"}}},"appconfig_update_configuration_profile":{"id":"appconfig_update_configuration_profile","name":"AppConfig Update Configuration Profile","description":"Update the name, description, or retrieval role of an AppConfig configuration profile","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID that owns the configuration profile"},"configurationProfileId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The configuration profile ID to update"},"name":{"type":"string","required":false,"visibility":"user-or-llm","description":"New name for the configuration profile"},"description":{"type":"string","required":false,"visibility":"user-or-llm","description":"New description for the configuration profile"},"retrievalRoleArn":{"type":"string","required":false,"visibility":"user-or-llm","description":"New ARN of the IAM role used to retrieve the configuration"}}},"appconfig_update_environment":{"id":"appconfig_update_environment","name":"AppConfig Update Environment","description":"Update the name or description of an AWS AppConfig environment","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID that owns the environment"},"environmentId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The environment ID to update"},"name":{"type":"string","required":false,"visibility":"user-or-llm","description":"New name for the environment"},"description":{"type":"string","required":false,"visibility":"user-or-llm","description":"New description for the environment"}}},"arxiv_get_author_papers":{"id":"arxiv_get_author_papers","name":"ArXiv Get Author Papers","description":"Search for papers by a specific author on ArXiv.","version":"1.0.0","params":{"authorName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Author name to search for"},"maxResults":{"type":"number","required":false,"visibility":"user-only","description":"Maximum number of results to return (default: 10, max: 2000)"}}},"arxiv_get_paper":{"id":"arxiv_get_paper","name":"ArXiv Get Paper","description":"Get detailed information about a specific ArXiv paper by its ID.","version":"1.0.0","params":{"paperId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ArXiv paper ID (e.g., \\"1706.03762\\")"}}},"arxiv_search":{"id":"arxiv_search","name":"ArXiv Search","description":"Search for academic papers on ArXiv by keywords, authors, titles, or other fields.","version":"1.0.0","params":{"searchQuery":{"type":"string","required":true,"visibility":"user-or-llm","description":"The search query to execute"},"searchField":{"type":"string","required":false,"visibility":"user-only","description":"Field to search in: all, ti (title), au (author), abs (abstract), co (comment), jr (journal), cat (category), rn (report number)"},"maxResults":{"type":"number","required":false,"visibility":"user-only","description":"Maximum number of results to return (default: 10, max: 2000)"},"sortBy":{"type":"string","required":false,"visibility":"user-only","description":"Sort by: relevance, lastUpdatedDate, submittedDate (default: relevance)"},"sortOrder":{"type":"string","required":false,"visibility":"user-only","description":"Sort order: ascending, descending (default: descending)"}}},"asana_add_comment":{"id":"asana_add_comment","name":"Asana Add Comment","description":"Add a comment (story) to an Asana task","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"taskGid":{"type":"string","required":true,"visibility":"user-or-llm","description":"Asana task GID (numeric string)"},"text":{"type":"string","required":true,"visibility":"user-or-llm","description":"The text content of the comment"}},"oauth":{"required":true,"provider":"asana"}},"asana_add_followers":{"id":"asana_add_followers","name":"Asana Add Followers","description":"Add one or more followers to an Asana task","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"taskGid":{"type":"string","required":true,"visibility":"user-or-llm","description":"GID of the Asana task (numeric string)"},"followers":{"type":"array","required":true,"visibility":"user-or-llm","description":"Array of user GIDs to add as followers to the task"}},"oauth":{"required":true,"provider":"asana"}},"asana_create_project":{"id":"asana_create_project","name":"Asana Create Project","description":"Create a new project in an Asana workspace","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"workspace":{"type":"string","required":true,"visibility":"user-or-llm","description":"Asana workspace GID (numeric string) where the project will be created"},"name":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the project"},"notes":{"type":"string","required":false,"visibility":"user-or-llm","description":"Notes or description for the project"}},"oauth":{"required":true,"provider":"asana"}},"asana_create_section":{"id":"asana_create_section","name":"Asana Create Section","description":"Create a new section in an Asana project","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"projectGid":{"type":"string","required":true,"visibility":"user-or-llm","description":"GID of the Asana project (numeric string) to add the section to"},"name":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the section"}},"oauth":{"required":true,"provider":"asana"}},"asana_create_subtask":{"id":"asana_create_subtask","name":"Asana Create Subtask","description":"Create a subtask under an existing Asana task","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"taskGid":{"type":"string","required":true,"visibility":"user-or-llm","description":"GID of the parent Asana task (numeric string)"},"name":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the subtask"},"notes":{"type":"string","required":false,"visibility":"user-or-llm","description":"Notes or description for the subtask"},"assignee":{"type":"string","required":false,"visibility":"user-or-llm","description":"User GID to assign the subtask to"},"due_on":{"type":"string","required":false,"visibility":"user-or-llm","description":"Due date in YYYY-MM-DD format"}},"oauth":{"required":true,"provider":"asana"}},"asana_create_task":{"id":"asana_create_task","name":"Asana Create Task","description":"Create a new task in Asana","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"workspace":{"type":"string","required":true,"visibility":"user-or-llm","description":"Asana workspace GID (numeric string) where the task will be created"},"name":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the task"},"notes":{"type":"string","required":false,"visibility":"user-or-llm","description":"Notes or description for the task"},"assignee":{"type":"string","required":false,"visibility":"user-or-llm","description":"User GID to assign the task to"},"due_on":{"type":"string","required":false,"visibility":"user-or-llm","description":"Due date in YYYY-MM-DD format"}},"oauth":{"required":true,"provider":"asana"}},"asana_delete_task":{"id":"asana_delete_task","name":"Asana Delete Task","description":"Delete an Asana task by its GID (moves it to the trash)","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"taskGid":{"type":"string","required":true,"visibility":"user-or-llm","description":"GID of the Asana task to delete (numeric string)"}},"oauth":{"required":true,"provider":"asana"}},"asana_get_project":{"id":"asana_get_project","name":"Asana Get Project","description":"Retrieve a single Asana project by its GID","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"projectGid":{"type":"string","required":true,"visibility":"user-or-llm","description":"Asana project GID (numeric string) to retrieve"}},"oauth":{"required":true,"provider":"asana"}},"asana_get_projects":{"id":"asana_get_projects","name":"Asana Get Projects","description":"Retrieve all projects from an Asana workspace","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"workspace":{"type":"string","required":true,"visibility":"user-or-llm","description":"Asana workspace GID (numeric string) to retrieve projects from"}},"oauth":{"required":true,"provider":"asana"}},"asana_get_task":{"id":"asana_get_task","name":"Asana Get Task","description":"Retrieve a single task by GID or get multiple tasks with filters","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"taskGid":{"type":"string","required":false,"visibility":"user-or-llm","description":"The globally unique identifier (GID) of the task. If not provided, will get multiple tasks."},"workspace":{"type":"string","required":false,"visibility":"user-or-llm","description":"Asana workspace GID (numeric string) to filter tasks (required when not using taskGid)"},"project":{"type":"string","required":false,"visibility":"user-or-llm","description":"Asana project GID (numeric string) to filter tasks"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of tasks to return (default: 50)"}},"oauth":{"required":true,"provider":"asana"}},"asana_list_sections":{"id":"asana_list_sections","name":"Asana List Sections","description":"List all sections in an Asana project","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"projectGid":{"type":"string","required":true,"visibility":"user-or-llm","description":"GID of the Asana project (numeric string) to list sections from"}},"oauth":{"required":true,"provider":"asana"}},"asana_list_workspaces":{"id":"asana_list_workspaces","name":"Asana List Workspaces","description":"List all Asana workspaces and organizations the authenticated user belongs to","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"}},"oauth":{"required":true,"provider":"asana"}},"asana_search_tasks":{"id":"asana_search_tasks","name":"Asana Search Tasks","description":"Search for tasks in an Asana workspace","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"workspace":{"type":"string","required":true,"visibility":"user-or-llm","description":"Asana workspace GID (numeric string) to search tasks in"},"text":{"type":"string","required":false,"visibility":"user-or-llm","description":"Text to search for in task names"},"assignee":{"type":"string","required":false,"visibility":"user-or-llm","description":"Filter tasks by assignee user GID"},"projects":{"type":"array","required":false,"visibility":"user-or-llm","description":"Array of Asana project GIDs (numeric strings) to filter tasks by"},"completed":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Filter by completion status"}},"oauth":{"required":true,"provider":"asana"}},"asana_update_task":{"id":"asana_update_task","name":"Asana Update Task","description":"Update an existing task in Asana","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"taskGid":{"type":"string","required":true,"visibility":"user-or-llm","description":"Asana task GID (numeric string) of the task to update"},"name":{"type":"string","required":false,"visibility":"user-or-llm","description":"Updated name for the task"},"notes":{"type":"string","required":false,"visibility":"user-or-llm","description":"Updated notes or description for the task"},"assignee":{"type":"string","required":false,"visibility":"user-or-llm","description":"Updated assignee user GID"},"completed":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Mark task as completed or not completed"},"due_on":{"type":"string","required":false,"visibility":"user-or-llm","description":"Updated due date in YYYY-MM-DD format"}},"oauth":{"required":true,"provider":"asana"}},"ashby_add_candidate_tag":{"id":"ashby_add_candidate_tag","name":"Ashby Add Candidate Tag","description":"Adds a tag to a candidate in Ashby and returns the updated candidate.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ashby API Key"},"candidateId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The UUID of the candidate to add the tag to"},"tagId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The UUID of the tag to add"}}},"ashby_change_application_stage":{"id":"ashby_change_application_stage","name":"Ashby Change Application Stage","description":"Moves an application to a different interview stage. Requires an archive reason when moving to an Archived stage.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ashby API Key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The UUID of the application to update the stage of"},"interviewStageId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The UUID of the interview stage to move the application to"},"archiveReasonId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Archive reason UUID. Required when moving to an Archived stage, ignored otherwise"}}},"ashby_create_application":{"id":"ashby_create_application","name":"Ashby Create Application","description":"Creates a new application for a candidate on a job. Optionally specify interview plan, stage, source, and credited user.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ashby API Key"},"candidateId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The UUID of the candidate to consider for the job"},"jobId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The UUID of the job to consider the candidate for"},"interviewPlanId":{"type":"string","required":false,"visibility":"user-or-llm","description":"UUID of the interview plan to use (defaults to the job default plan)"},"interviewStageId":{"type":"string","required":false,"visibility":"user-or-llm","description":"UUID of the interview stage to place the application in (defaults to first Lead stage)"},"sourceId":{"type":"string","required":false,"visibility":"user-or-llm","description":"UUID of the source to set on the application"},"creditedToUserId":{"type":"string","required":false,"visibility":"user-or-llm","description":"UUID of the user the application is credited to"},"createdAt":{"type":"string","required":false,"visibility":"user-or-llm","description":"ISO 8601 timestamp to set as the application creation date (defaults to now)"}}},"ashby_create_candidate":{"id":"ashby_create_candidate","name":"Ashby Create Candidate","description":"Creates a new candidate record in Ashby.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ashby API Key"},"name":{"type":"string","required":true,"visibility":"user-or-llm","description":"The candidate full name"},"email":{"type":"string","required":false,"visibility":"user-or-llm","description":"Primary email address for the candidate"},"phoneNumber":{"type":"string","required":false,"visibility":"user-or-llm","description":"Primary phone number for the candidate"},"linkedInUrl":{"type":"string","required":false,"visibility":"user-or-llm","description":"LinkedIn profile URL"},"githubUrl":{"type":"string","required":false,"visibility":"user-or-llm","description":"GitHub profile URL"},"website":{"type":"string","required":false,"visibility":"user-or-llm","description":"Personal website URL"},"sourceId":{"type":"string","required":false,"visibility":"user-or-llm","description":"UUID of the source to attribute the candidate to"},"creditedToUserId":{"type":"string","required":false,"visibility":"user-or-llm","description":"UUID of the Ashby user to credit with sourcing this candidate"},"createdAt":{"type":"string","required":false,"visibility":"user-or-llm","description":"Backdated creation timestamp in ISO 8601 (e.g. 2024-01-01T00:00:00Z). Defaults to now."},"alternateEmailAddresses":{"type":"json","required":false,"visibility":"user-or-llm","description":"Array of additional email address strings to add to the candidate, e.g. [\\"a@x.com\\",\\"b@y.com\\"]"}}},"ashby_create_note":{"id":"ashby_create_note","name":"Ashby Create Note","description":"Creates a note on a candidate in Ashby. Supports plain text and HTML content (bold, italic, underline, links, lists, code).","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ashby API Key"},"candidateId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The UUID of the candidate to add the note to"},"note":{"type":"string","required":true,"visibility":"user-or-llm","description":"The note content. If noteType is text/html, supports: , , , ,