Skip to content

Commit 467d74f

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
fix(netsuite): enforce provider response contracts
1 parent b507fdc commit 467d74f

33 files changed

Lines changed: 498 additions & 98 deletions

apps/docs/content/docs/en/integrations/netsuite.mdx

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
2525
## Usage notes
2626

2727
- Record fields and supported actions vary by account, enabled features, custom records, forms, role, and permissions. Use **List Record Types** and **Get Record Metadata** before constructing create, update, upsert, action, or transform bodies. Sim intentionally accepts JSON for these dynamic record shapes instead of guessing a fixed schema.
28-
- Paged operations return one page only. The default limit is 100, the maximum is 1,000, and the offset must be a non-negative multiple of the limit. Sim never fetches later pages automatically. NetSuite limits ordinary collection traversal to 1,000 pages and SuiteQL queries to 100,000 results.
29-
- Homogeneous batch operations accept 1–100 records of one record type and always run asynchronously. Preserve the returned `location` or `jobId`, use **Get Async Status** with **Job Status** until the job completes, choose **List Tasks** to collect task IDs, check each ID with **Task Status**, then use completed IDs with **Get Async Operation Result**.
30-
- Sim limits each materialized request body and successful SuiteTalk response to 16 MiB. Split work into smaller pages or batches when a request or response would exceed that ceiling, even if NetSuite would otherwise accept the payload.
28+
- Paged operations return one page only. The default limit is 100, the maximum is 1,000, and the offset must be a non-negative multiple of the limit. Sim never fetches later pages automatically. Requests must stay within NetSuite's first 100,000 results and first 1,000 pages.
29+
- Homogeneous batch operations accept 1–100 records of one record type and always run asynchronously. NetSuite processes records in parallel, and individual tasks can fail independently; submission is not an all-or-none transaction. Preserve the returned `location` or `jobId`, use **Get Async Status** with **Job Status** until the job completes, choose **List Tasks** to collect task IDs, check each ID with **Task Status**, then use completed IDs with **Get Async Operation Result**. Canceling or timing out the local Sim request does not cancel a batch that NetSuite has already accepted.
30+
- Sim limits each materialized request body and successful SuiteTalk response to 16 MiB. Request JSON is also limited to 100 levels of nesting and 100,000 JSON values. Split work into smaller pages or batches when a request or response would exceed these ceilings, even if NetSuite would otherwise accept the payload.
3131
- When attaching a contact with a role, provide either the role's internal ID or external ID, not both. File attachments do not use a contact role.
3232
- **Attach/Detach**, homogeneous batch operations, **Get Record Form**, and **Get Select Options** require a NetSuite 2026.1-compatible account. Oracle introduced these SuiteTalk REST capabilities in [NetSuite 2026.1](https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_N3950559.html).
3333
- **Get Governance Limits** returns data only for roles allowed by NetSuite; Oracle documents Administrator access for that operation.
@@ -58,7 +58,7 @@ List one page of a NetSuite record collection, optionally filtered with a q expr
5858
| `recordType` | string | Yes | NetSuite REST record type script ID, such as customer or salesOrder |
5959
| `q` | string | No | NetSuite record collection filter expression |
6060
| `limit` | number | No | Results to return in this page \(1-1000; default 100\) |
61-
| `offset` | number | No | Zero-based result offset; must be divisible by limit and select one of the first 1,000 pages |
61+
| `offset` | number | No | Zero-based result offset; must be divisible by limit and stay within the first 100,000 results and 1,000 pages |
6262

6363
#### Output
6464

@@ -125,7 +125,7 @@ Create a NetSuite record using the account-specific record metadata schema.
125125
| Parameter | Type | Description |
126126
| --------- | ---- | ----------- |
127127
| `status` | number | HTTP status returned by NetSuite |
128-
| `data` | json | NetSuite response body; record fields are account-specific and dynamic |
128+
| `data` | json | Empty for standard HTTP 204 creation; replacement creation can return the documented HTTP 201 post-state object |
129129
| `location` | string | Newly created record URL from the Location response header |
130130

131131
### NetSuite Update Record
@@ -150,7 +150,7 @@ Update fields on an existing NetSuite record with PATCH.
150150
| Parameter | Type | Description |
151151
| --------- | ---- | ----------- |
152152
| `status` | number | HTTP status returned by NetSuite |
153-
| `data` | json | NetSuite response body; record fields are account-specific and dynamic |
153+
| `data` | json | Empty for the documented HTTP 204 No Content response |
154154
| `location` | string | Updated record URL from the Location response header |
155155

156156
### NetSuite Upsert Record
@@ -175,7 +175,7 @@ Create or update a NetSuite record by external ID with PUT.
175175
| Parameter | Type | Description |
176176
| --------- | ---- | ----------- |
177177
| `status` | number | HTTP status returned by NetSuite |
178-
| `data` | json | NetSuite response body; record fields are account-specific and dynamic |
178+
| `data` | json | Empty for the documented HTTP 204 No Content response |
179179

180180
### NetSuite Delete Record
181181

@@ -197,7 +197,7 @@ Delete one NetSuite record by internal or external ID.
197197
| Parameter | Type | Description |
198198
| --------- | ---- | ----------- |
199199
| `status` | number | HTTP status returned by NetSuite |
200-
| `data` | json | NetSuite response body; record fields are account-specific and dynamic |
200+
| `data` | json | Empty for the documented HTTP 204 No Content response |
201201

202202
### NetSuite Get Subresource
203203

@@ -266,7 +266,7 @@ Retrieve valid select values for one or more fields on a new or existing record.
266266
| `q` | string | No | Optional select-option filter using CONTAIN, IS, or START_WITH |
267267
| `body` | json | No | Record fields matching the account-specific NetSuite metadata schema |
268268
| `limit` | number | No | Results to return in this page \(1-1000; default 100\) |
269-
| `offset` | number | No | Zero-based result offset; must be divisible by limit and select one of the first 1,000 pages |
269+
| `offset` | number | No | Zero-based result offset; must be divisible by limit and stay within the first 100,000 results and 1,000 pages |
270270

271271
#### Output
272272

@@ -343,18 +343,15 @@ Execute a supported NetSuite record action such as approve, reject, or confirm.
343343
| `recordType` | string | Yes | NetSuite REST record type script ID, such as customer or salesOrder |
344344
| `recordId` | string | Yes | NetSuite internal ID or an external-ID reference beginning with eid: |
345345
| `action` | string | Yes | NetSuite record action ID without the @ prefix |
346-
| `body` | json | No | Record fields matching the account-specific NetSuite metadata schema |
346+
| `body` | json | No | Parameters accepted by the selected NetSuite record action |
347347

348348
#### Output
349349

350350
| Parameter | Type | Description |
351351
| --------- | ---- | ----------- |
352352
| `status` | number | HTTP status returned by NetSuite |
353353
| `data` | json | Documented NetSuite record-action response |
354-
|`links` | array | Oracle HATEOAS links for the response |
355-
|`rel` | string | Link relationship |
356-
|`href` | string | Link target |
357-
|`result` | boolean | Whether NetSuite completed the record action |
354+
|`result` | boolean | True when NetSuite completed the record action |
358355

359356
### NetSuite Transform Record
360357

@@ -378,7 +375,7 @@ Transform a supported source record into another NetSuite record type.
378375
| Parameter | Type | Description |
379376
| --------- | ---- | ----------- |
380377
| `status` | number | HTTP status returned by NetSuite |
381-
| `data` | json | NetSuite response body; record fields are account-specific and dynamic |
378+
| `data` | json | Empty for the documented HTTP 204 No Content response |
382379

383380
### NetSuite Batch Get Records
384381

@@ -520,9 +517,9 @@ Execute one page of a SuiteQL query through SuiteTalk REST web services.
520517
| `clientId` | string | Yes | Client ID of the NetSuite OAuth 2.0 integration record |
521518
| `certificateId` | string | Yes | Certificate ID from OAuth 2.0 Client Credentials \(M2M\) Setup |
522519
| `privateKey` | string | Yes | Oracle-supported RSA \(3072/4096-bit\) or EC \(P-256/P-384/P-521\) PEM private key matching the certificate uploaded to NetSuite |
523-
| `query` | string | Yes | SuiteQL SELECT query |
520+
| `query` | string | Yes | SuiteQL SELECT query; use a complete unique ORDER BY when retrieving multiple pages |
524521
| `limit` | number | No | Results to return in this page \(1-1000; default 100\) |
525-
| `offset` | number | No | Zero-based result offset; must be divisible by limit and select one of the first 1,000 pages |
522+
| `offset` | number | No | Zero-based result offset; must be divisible by limit and stay within the first 100,000 results and 1,000 pages |
526523

527524
#### Output
528525

@@ -552,7 +549,7 @@ List one page of SuiteAnalytics Workbook datasets available to the authenticated
552549
| `certificateId` | string | Yes | Certificate ID from OAuth 2.0 Client Credentials \(M2M\) Setup |
553550
| `privateKey` | string | Yes | Oracle-supported RSA \(3072/4096-bit\) or EC \(P-256/P-384/P-521\) PEM private key matching the certificate uploaded to NetSuite |
554551
| `limit` | number | No | Results to return in this page \(1-1000; default 100\) |
555-
| `offset` | number | No | Zero-based result offset; must be divisible by limit and select one of the first 1,000 pages |
552+
| `offset` | number | No | Zero-based result offset; must be divisible by limit and stay within the first 100,000 results and 1,000 pages |
556553

557554
#### Output
558555

@@ -583,7 +580,7 @@ Execute one page of a standard or custom SuiteAnalytics Workbook dataset.
583580
| `privateKey` | string | Yes | Oracle-supported RSA \(3072/4096-bit\) or EC \(P-256/P-384/P-521\) PEM private key matching the certificate uploaded to NetSuite |
584581
| `datasetId` | string | Yes | SuiteAnalytics dataset script ID |
585582
| `limit` | number | No | Results to return in this page \(1-1000; default 100\) |
586-
| `offset` | number | No | Zero-based result offset; must be divisible by limit and select one of the first 1,000 pages |
583+
| `offset` | number | No | Zero-based result offset; must be divisible by limit and stay within the first 100,000 results and 1,000 pages |
587584

588585
#### Output
589586

@@ -627,6 +624,7 @@ List record types exposed to the authenticated role by the REST metadata catalog
627624
|`links` | array | Oracle HATEOAS links for the response |
628625
|`rel` | string | Link relationship |
629626
|`href` | string | Link target |
627+
|`mediaType` | string | Media type advertised for the linked metadata resource |
630628

631629
### NetSuite Get Record Metadata
632630

@@ -674,7 +672,7 @@ Retrieve job status, list job tasks, or retrieve one task status.
674672
| `data` | json | Documented NetSuite asynchronous job, task collection, or task status |
675673
|`completed` | boolean | Whether processing has completed |
676674
|`endTime` | string | Task completion time |
677-
|`id` | string | Asynchronous task ID |
675+
|`id` | string | Asynchronous job or task ID |
678676
|`progress` | string | Current task progress state |
679677
|`startTime` | string | Task start time |
680678
|`count` | number | Number of task collection entries returned |
@@ -710,7 +708,7 @@ Retrieve the provider response for one task within a completed asynchronous job.
710708
| Parameter | Type | Description |
711709
| --------- | ---- | ----------- |
712710
| `status` | number | HTTP status returned by NetSuite |
713-
| `data` | json | NetSuite response body; record fields are account-specific and dynamic |
711+
| `data` | json | Result payload for the submitted asynchronous operation; record fields are account-specific and dynamic |
714712

715713
### NetSuite Get Server Time
716714

apps/sim/blocks/blocks/netsuite.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ describe('Oracle NetSuite block', () => {
248248
enabled: true,
249249
generationType: 'sql-query',
250250
})
251-
for (const field of ['fields', 'ids', 'replace']) {
251+
for (const field of ['fields', 'ids']) {
252252
expect(getSubBlock(field).wandConfig?.prompt, field).toContain(
253253
'no explanations, no extra text'
254254
)
@@ -424,6 +424,7 @@ describe('Oracle NetSuite block', () => {
424424
condition: { field: 'operation', value: 'netsuite_get_async_status' },
425425
})
426426
expect(getSubBlock('view').value?.({})).toBe('job')
427+
expect(getSubBlock('replace')).not.toHaveProperty('wandConfig')
427428
})
428429

429430
it('ships the seven practical templates and five focused skills in the agreed scope', () => {

apps/sim/blocks/blocks/netsuite.ts

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -433,7 +433,7 @@ Return ONLY the comma-separated field IDs - no explanations, no extra text.`,
433433
},
434434
{
435435
id: 'body',
436-
title: 'Record Fields (JSON)',
436+
title: 'Request Body (JSON)',
437437
type: 'code',
438438
language: 'json',
439439
placeholder: '{"companyName":"Acme Inc."}',
@@ -653,7 +653,7 @@ Return ONLY the SuiteQL query - no explanations, no markdown, no extra text.`,
653653
id: 'offset',
654654
title: 'Offset',
655655
type: 'short-input',
656-
placeholder: '0 (multiple of limit; first 1000 pages)',
656+
placeholder: '0 (multiple of limit; first 100,000 results)',
657657
condition: { field: 'operation', value: PAGING_OPERATIONS },
658658
mode: 'advanced',
659659
},
@@ -688,13 +688,6 @@ Return ONLY the SuiteQL query - no explanations, no markdown, no extra text.`,
688688
value: ['netsuite_create_record', 'netsuite_update_record', 'netsuite_upsert_record'],
689689
},
690690
mode: 'advanced',
691-
wandConfig: {
692-
enabled: true,
693-
prompt: `Generate a comma-separated list of NetSuite sublist script IDs to replace from the user's request.
694-
695-
Return ONLY the comma-separated sublist IDs - no explanations, no extra text.`,
696-
placeholder: 'Describe the sublists to replace',
697-
},
698691
},
699692
{
700693
id: 'idempotencyKey',
@@ -813,7 +806,10 @@ Return ONLY the comma-separated sublist IDs - no explanations, no extra text.`,
813806
externalId: { type: 'string', description: 'External ID used for upsert' },
814807
q: { type: 'string', description: 'Record or select-option filter expression' },
815808
fields: { type: 'string', description: 'Comma-separated field IDs' },
816-
body: { type: 'json', description: 'Account-specific record fields' },
809+
body: {
810+
type: 'json',
811+
description: 'Account-specific record fields or parameters for a selected record action',
812+
},
817813
subresourcePath: { type: 'string', description: 'Record subresource path' },
818814
relatedType: { type: 'string', description: 'Related contact or file type' },
819815
relatedId: {
@@ -835,7 +831,8 @@ Return ONLY the comma-separated sublist IDs - no explanations, no extra text.`,
835831
limit: { type: 'number', description: 'Page size (1-1000)' },
836832
offset: {
837833
type: 'number',
838-
description: 'Page offset divisible by limit and within the first 1000 pages',
834+
description:
835+
'Page offset divisible by limit and within the first 100,000 results and 1,000 pages',
839836
},
840837
expand: { type: 'string', description: 'Resources to expand' },
841838
expandSubResources: { type: 'boolean', description: 'Expand sublists and subrecords' },

apps/sim/tools/generated/tool-metadata.ts

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

apps/sim/tools/generated/tool-outputs.ts

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

apps/sim/tools/netsuite/attach_record.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ export const netsuiteAttachRecordTool: ToolConfig<NetSuiteAttachParams, NetSuite
9898
{ value: relatedType, label: 'Related type' },
9999
{ value: params.relatedId, label: 'Related ID' }
100100
),
101+
success: { status: 204, body: 'none' },
101102
body: roleId
102103
? { role: { id: roleId } }
103104
: roleExternalId

apps/sim/tools/netsuite/batch_delete_records.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export const netsuiteBatchDeleteRecordsTool: ToolConfig<
6969
return {
7070
method: 'DELETE',
7171
path: buildRecordPath({ value: params.recordType, label: 'Record type' }),
72+
success: { status: 202, body: 'none' },
7273
responseLocation: 'async-job',
7374
query: { ids: normalizeBatchIds(params.ids) },
7475
headers: {

apps/sim/tools/netsuite/batch_get_records.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ export const netsuiteBatchGetRecordsTool: ToolConfig<NetSuiteBatchGetParams, Net
8585
return {
8686
method: 'GET',
8787
path: buildRecordPath({ value: params.recordType, label: 'Record type' }),
88+
success: { status: 202, body: 'none' },
8889
responseLocation: 'async-job',
8990
query: {
9091
expandRecords: true,

apps/sim/tools/netsuite/create_record.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export const netsuiteCreateRecordTool: ToolConfig<NetSuiteCreateRecordParams, Ne
4848
replace: {
4949
type: 'string',
5050
required: false,
51-
visibility: 'user-or-llm',
51+
visibility: 'user-only',
5252
description: 'Comma-separated sublists whose default lines should be replaced',
5353
},
5454
},
@@ -59,6 +59,10 @@ export const netsuiteCreateRecordTool: ToolConfig<NetSuiteCreateRecordParams, Ne
5959
() => ({
6060
method: 'POST',
6161
path: buildRecordPath({ value: params.recordType, label: 'Record type' }),
62+
success: [
63+
{ status: 201, body: 'object' },
64+
{ status: 204, body: 'none' },
65+
],
6266
responseLocation: 'resource',
6367
query: { replace: optionalTrim(params.replace, 'Replace sublists') },
6468
body: params.body,
@@ -69,7 +73,8 @@ export const netsuiteCreateRecordTool: ToolConfig<NetSuiteCreateRecordParams, Ne
6973
status: { type: 'number', description: 'HTTP status returned by NetSuite' },
7074
data: {
7175
type: 'json',
72-
description: 'NetSuite response body; record fields are account-specific and dynamic',
76+
description:
77+
'Empty for standard HTTP 204 creation; replacement creation can return the documented HTTP 201 post-state object',
7378
nullable: true,
7479
},
7580
location: {

apps/sim/tools/netsuite/delete_record.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,15 @@ export const netsuiteDeleteRecordTool: ToolConfig<NetSuiteDeleteRecordParams, Ne
5656
{ value: params.recordType, label: 'Record type' },
5757
{ value: params.recordId, label: 'Record ID' }
5858
),
59+
success: { status: 204, body: 'none' },
5960
}),
6061
signal
6162
),
6263
outputs: {
6364
status: { type: 'number', description: 'HTTP status returned by NetSuite' },
6465
data: {
6566
type: 'json',
66-
description: 'NetSuite response body; record fields are account-specific and dynamic',
67+
description: 'Empty for the documented HTTP 204 No Content response',
6768
nullable: true,
6869
},
6970
},

0 commit comments

Comments
 (0)