> ## Documentation Index
> Fetch the complete documentation index at: https://docs.omnicommerce.sg/llms.txt
> Use this file to discover all available pages before exploring further.

# Run Windmill scripts

> Run scripts directly over HTTP, pass large product ID lists without an AOP, and track job results.

Run a deployed script directly when you already know which operation to perform. An AOP or assistant conversation is optional. Product IDs sent in an HTTP request are script arguments, so passing 500 IDs this way does not consume LLM tokens. Model calls made by the script or a downstream AI operation still incur AI usage.

For a product workflow, see [Import and queue product enrichment](/guides/import-and-enrich). It includes developer-authenticated Zalora import and enrichment APIs and a Windmill example that stops once enrichment is queued.

## Choose an execution API

| API                             | Authentication                                                      | When to use it                                                                                               |
| ------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| OmniCommerce `/api/scripts/...` | Signed-in OmniCommerce browser session and organization permissions | Run an organization's deployed script through the OmniCommerce app.                                          |
| Windmill `/api/w/...`           | Windmill bearer token with permission to run the script             | Server-to-server execution in a Windmill workspace you administer or have explicitly been granted access to. |

<Note>
  OmniCommerce's script endpoints are session-authenticated app endpoints. An
  OmniCommerce API key or OAuth token alone does not authenticate these calls.
  There is currently no public `/api/v1/scripts` execution endpoint. The
  Mintlify bearer-token playground does not supply an app session.
</Note>

## Prepare a script in OmniCommerce

Open `/app/{organizationId}/scripts`, create a general TypeScript script, and open its editor. Your organization must have Windmill configured, and your account must have permission to manage and execute scripts.

For a first run, use this example to count a list of up to 500 product IDs:

```typescript theme={null}
export async function main(productIds: string[]) {
  if (productIds.length === 0 || productIds.length > 500) {
    throw new Error("Supply between 1 and 500 product IDs.");
  }

  const normalized = productIds.map((id) => id.trim());
  if (normalized.some((id) => id.length === 0)) {
    throw new Error("Product IDs must not be blank.");
  }

  return {
    receivedCount: productIds.length,
    uniqueCount: new Set(normalized).size,
  };
}
```

This example inspects the supplied strings only; it does not check catalog ownership or read or update products. The 500-ID bound is an example script rule, not a universal Windmill limit.

In **Arguments**, enter:

```json theme={null}
{
  "productIds": ["product-1", "product-2", "product-1"]
}
```

Click **Test** to execute the editor's current source. The result should be `{"receivedCount":3,"uniqueCount":2}`. Test executes real code, including any side effects in your script. Click **Deploy** and wait for the script to become **Active** before calling the deployed-run endpoint. Saving a draft alone does not deploy it.

Copy `organizationId` and `scriptId` from the editor URL, `/app/{organizationId}/scripts/{scriptId}`. The script ID is different from its `script__...` tool name and its Windmill deployment hash.

## Start a deployed run

```http theme={null}
POST /api/scripts/{scriptId}/runs
Content-Type: application/json
```

Use the following JavaScript from a signed-in page on the OmniCommerce app origin. It uses the existing session cookie; it will not work from the docs site's console or an unrelated website.

```javascript theme={null}
const organizationId = "YOUR_ORGANIZATION_ID";
const scriptId = "YOUR_SCRIPT_ID";

async function postScriptRequest(path, body) {
  const response = await fetch(path, {
    method: "POST",
    credentials: "same-origin",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });
  const payload = await response.json();
  if (!response.ok) {
    throw new Error(payload.error ?? `Request failed (${response.status})`);
  }
  return payload;
}

// Supply your complete selection here, including all 500 IDs if needed.
const productIds = ["product-1", "product-2", "product-1"];
const { run } = await postScriptRequest(
  `/api/scripts/${encodeURIComponent(scriptId)}/runs`,
  { organizationId, args: { productIds } },
);

const runId = run.id;
console.log({ runId, status: run.status, result: run.resultRef?.value });
```

The request body accepts:

| Field            | Description                                                                                                                                |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `organizationId` | Required. The signed-in user's organization.                                                                                               |
| `args`           | JSON object matching the deployed script's parameter schema. Defaults to an empty object.                                                  |
| `versionId`      | Optional OmniCommerce script-version ID. Omit it to use the active version. A supplied version must belong to this script and be accepted. |

The response is HTTP `201` with `{ "run": ... }`. It can contain a finished run or a run still in progress. Retain `run.id` before tracking it; HTTP `201` does not establish that the script or every product operation succeeded.

## Track completion and read results

Continue with the helper and `runId` from the previous example:

```javascript theme={null}
const jobs = await postScriptRequest("/api/scripts/jobs", {
  organizationId,
  action: "await",
  jobIds: [runId],
});

for (const result of jobs.results) {
  console.log({
    runId: result.id,
    status: result.status,
    value: result.resultRef?.value,
    error: result.errorRef?.message,
  });
}
```

Despite its name, `await` refreshes the current state and can return pending jobs. Repeat this status request with a delay, for example every 2–5 seconds, while jobs are pending. It does not submit the script again. Send all returned run IDs when tracking multiple runs; each request accepts 1–100 job IDs.

The response includes `success`, `jobIds`, `succeededCount`, `failedCount`, `pendingCount`, and ordered `results`. These counts describe **script jobs**, not individual products. Top-level `success` is true only when every requested job has succeeded.

| Status                            | Meaning and next step                                                                                                 |
| --------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `submitting`, `queued`, `running` | Execution is pending. Continue tracking the same run.                                                                 |
| `succeeded`                       | Script execution completed. Read `resultRef.value` and check any product-level failures reported by your script.      |
| `failed`, `timed_out`             | Inspect `errorRef` and determine what completed before starting another run.                                          |
| `cancel_requested`                | Cancellation was requested but is not confirmed. Continue tracking.                                                   |
| `cancelled`                       | Execution was cancelled. Earlier side effects are not rolled back.                                                    |
| `review_required`                 | The execution outcome needs review. Do not assume the script never started or automatically submit it again.          |
| `not_found`, `unavailable`        | A status lookup failed. Verify the run ID and organization, or retry the status lookup when the service is available. |

For the counting example, a successful run's `resultRef` contains:

```json theme={null}
{
  "kind": "inline",
  "value": {
    "receivedCount": 3,
    "uniqueCount": 2
  }
}
```

List recent runs with `GET /api/scripts/{scriptId}/runs?organizationId=YOUR_ORGANIZATION_ID` using the same app session. This returns `{ "items": [...] }` and can help locate a submitted run if the original response was interrupted.

## Request cancellation

```javascript theme={null}
const cancellation = await postScriptRequest("/api/scripts/jobs", {
  organizationId,
  action: "cancel",
  jobIds: [runId],
});
console.log(cancellation.results);
```

Check each result's `requested`, `confirmed`, and `status`. A cancellation request is not confirmation that execution stopped. Poll with `action: "await"` until the outcome is known.

## Call Windmill from an external system

Use this option only with your own authorized Windmill workspace credentials. An OmniCommerce API key is not a Windmill token, and access to OmniCommerce Scripts does not itself provide a Windmill token.

Windmill provides [HTTP webhooks for deployed scripts](https://www.windmill.dev/docs/core_concepts/webhooks). Select the asynchronous endpoint for a long-running batch. A script hash identifies a fixed deployed version.

Save your script arguments in `arguments.json`, containing the `productIds` object shown above with the full list. Send that object directly, without OmniCommerce's `organizationId` or `args` wrapper:

```bash theme={null}
curl --fail-with-body --request POST \
  "$WINDMILL_BASE_URL/api/w/$WINDMILL_WORKSPACE_ID/jobs/run/h/$WINDMILL_SCRIPT_HASH" \
  --header "Authorization: Bearer $WINDMILL_TOKEN" \
  --header "Content-Type: application/json" \
  --data-binary @arguments.json
```

Set `WINDMILL_BASE_URL` to your Windmill origin, such as `https://app.windmill.dev`. Obtain the workspace and deployed script hash from your Windmill script's **Details and Triggers** tab. Keep the token in your external system's secret storage.

The response is a Windmill job UUID. Store it as `WINDMILL_JOB_ID` and check its state:

```bash theme={null}
curl --fail-with-body \
  "$WINDMILL_BASE_URL/api/w/$WINDMILL_WORKSPACE_ID/jobs_u/get/$WINDMILL_JOB_ID" \
  --header "Authorization: Bearer $WINDMILL_TOKEN"
```

When its `type` is `CompletedJob`, inspect `success` and `result`. See Windmill's [job API documentation](https://www.windmill.dev/docs/core_concepts/jobs) for result and log retrieval. Native Windmill calls return Windmill job responses and are tracked in Windmill; they do not create an OmniCommerce script-run record.

## Batch size, retries, and token usage

* OmniCommerce validates `args` against the accepted deployment's schema. The JSON-encoded arguments and returned result each have a 256 KiB limit. A script or downstream API can impose a smaller batch limit.
* Pass large ID lists directly to the script. Putting the same IDs in an AOP prompt, model-generated tool arguments, or a tool result still incurs model tokens when those values reach the model.
* Use bounded batches when calling product APIs, validate every target in the authenticated organization, and retain per-product outcomes. Follow the target API's limits and marketplace capabilities.
* A script's `succeeded` status means its code completed successfully. If it catches individual errors, its returned result must make partial failures explicit.
* Submitting another `POST /runs` creates a new invocation. The current HTTP body does not accept an idempotency key. After a timeout or interrupted response, check existing runs before submitting again.
* For large reports, have your script save the details to authorized storage and return a compact summary and report reference. A generic product `selectionRef` resolver is not provided by these endpoints; it must be implemented by the script's integration if needed.

## Troubleshooting

| Symptom                                 | What to check                                                                                                                                                  |
| --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| OmniCommerce `401`                      | Sign in on the app origin. A developer API key alone does not provide a session for these routes.                                                              |
| OmniCommerce `403`                      | Confirm organization membership and script permissions. Execution/status/cancellation require `automations.execute`; listing runs requires `automations.read`. |
| `400` / invalid arguments               | Check `organizationId`, parameter names/types, the deployed version, and JSON size. Inspect the response's `error` and optional `details`.                     |
| Script not found or no accepted version | Confirm the script belongs to the selected organization, is active and unarchived, and has an accepted deployment.                                             |
| Windmill workspace not provisioned      | Ask your organization administrator to configure Scripts before running a deployed script.                                                                     |
| HTTP request succeeded but work failed  | Inspect the run status and the script's returned per-product results. HTTP success is not business-operation success.                                          |

Use the [AOP API](/guides/aops) when the workflow needs an agent to interpret instructions or choose tools. Use direct script execution when the inputs and operation are already known.
