mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
fix(agiloft): repoint the block at the alrest surface and fix EWLogin (#6562)
* fix(agiloft): make the block work, and align it with the REST documentation
The native Agiloft block could not authenticate against any instance. A
customer reported it; production traces for their workspace confirm every
failure mode verbatim. Fixing that exposed a second, larger problem, and a
per-endpoint audit against the full published documentation found the rest.
Authentication
- EWLogin sent only $KB/$login/$password as query parameters. A live instance
answers `400 EWWrongDataException ... One has to specify $table, $KB, $lang
parameters`. $table is required even though only $KB/$login/$password/$lang
are documented. Parameters now travel in a form-encoded body, which the docs
permit and which keeps the password out of URLs and access logs.
- The authentication scheme is read from the login response and trimmed;
Agiloft returns it as "Bearer " with a trailing space.
- EWLogout was missing $lang.
Surfaces
- Record create, read, update, search and saved-search now use the endpoints
that accept the token EWLogin issues; the legacy operations authenticate from
inline credentials, which is what that surface expects. Nothing sends both
forms at once — the documented 400 for doing so is what the original report
had run into.
- EWSelect passes credentials in a POST body, one of the five operations
documented to support it.
- Attachment retrieval uses the documented EWRetrieve endpoint, with
filePosition rather than position, and no longer needs a login/logout pair.
Defects found in the audit
- remove_attachment reported zero on every call: its body is the EWREST
assignment form but the route ran JSON.parse then Number(), yielding NaN.
- The EWREST parser could not read EWActionButton's documented response, which
puts both assignments on one line.
- EWLock treated any 200 as success, including the documented
{error, error_description} envelope, and invented an 'UNKNOWN' status.
- EWTable discarded the linked-field details, required flag and text field type
it had asked for, making includeLinkedInfo inert.
- select_records had no result ceiling at all; both it and search now cap and
report a truncated flag rather than reporting a capped length as a total.
- Optional string inputs rejected null, so a blank Page field failed validation
before any request was made.
- Upsert treated the documented 202 async acknowledgement as a missing-ID
failure, and returned no callback ID for the caller to poll.
- Every response contract required an output that the 401 and 500 paths never
return.
Coverage added
- Table and field discovery (EWTable), upsert (EWUpsert), async status
(EWAsyncStatus), natural language search (EWNLPSearch), action buttons
(EWActionButton), the REPLACE_WITH_ANOTHER delete rule with its substitute
records, $async on upsert, and <fieldName>$overwrite on attach.
- Reads with a named field list go through the search projection; an unfiltered
contract record runs to roughly 184KB and swamps downstream agent context.
- Errors are readable: Agiloft wraps failures in HTML around a typed exception
and an internal task id, and the JSON endpoints now request real status codes
rather than a 200 the caller has to interpret.
Not implemented: $searchSQL and $operationHints=NOLOCK are EWRead/EWUpdate
parameters and those operations do not run on that surface here; EWQuestion,
EWHotlinks, EWOData, EWBroadcast and webhook registration have no documentation
beyond their names.
Verified against the published documentation, not against a live instance.
* fix(agiloft): give natural language search a sentence that paints
check:canvas-sentences failed: the nlp_search card resolved to nothing on an
untouched canvas, so it painted empty. Its only basic-mode field was the
long-input query, and the field list is advanced, so every segment dropped.
The sentence now leads with the knowledge base, matching the shape List Tables
already uses — both operations are knowledge-base scoped rather than
table-scoped, so it also reads more accurately.
* fix(agiloft): stop retrying refusals, and expose the outputs the new operations return
Five findings from review that had gone unanswered.
An Agiloft refusal was surfacing as HTTP 500. readAlrestJson throws when the
envelope reports success:false, the route catch mapped that to 500, and the
tool runner retries 500s — so a create the server had already rejected could be
retried and duplicate the record. Refusals now return a settled failure with the
message intact; genuine faults still 500.
list_tables could not run in its primary mode. EWTable is knowledge-base scoped,
but some instances reject EWLogin without a $table, so whole-knowledge-base
discovery failed at login with nothing to fall back to. It now says what the
caller can do about it rather than surfacing the raw login error.
Upsert corrupted structured values. Every field went through String(), so a
multi-value field collapsed into one joined string instead of the documented
repeated key/value pairs, and an object silently wrote "[object Object]" into
the record. Arrays now encode as repeated pairs and objects are refused, since
Agiloft documents no encoding for them.
Two outputs were invisible in the editor. `records` was conditioned on
search_records alone, so natural language search results could not be chained,
and `callbackId` on run_action_button alone, so a queued upsert's callback could
not be wired into Async Status even though both values exist at runtime.
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
### Agiloft Saved Search (retired)
|
||||
|
||||
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 |
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user