Scripting API Migration Guide#

This guide is for anyone calling the prototype Scripting endpoints under /prototype/platform/scripts and moving to the supported public Scripting API.

The public Scripting API is available as a feature preview at X-Version: 2026-09-01.scripting.preview. All the prototype endpoints will work until the prototype sunset in October 2027, so you can migrate one call at a time. Every prototype operation has a public equivalent, so nothing you use will be discontinued.

High-Level Changes#

  • The /prototype/platform prefix is gone. /prototype/platform/scripts becomes /scripts.

  • Send X-Version: 2026-09-01.scripting.preview on every request. Prototype requests sent without an X-Version header were served by 2022-01-01; the public API requires the header.

  • Creating a script no longer creates __main__.py. Create __main__.py, the script entry point, as the first source in every new script. A run without it fails with can't find '__main__' module in '/mnt/'.

  • Starting a run returns a link to an operation, not to the run. Poll the operation once, read the run URL from resourceUrl, then continue polling until the operation finishes.

  • Cancelling a run is now synchronous and returns 204 No Content.

  • python-failed is no longer a run status. Failures report status: failed plus failureReason and a structured termination object.

  • Script names can now be changed with a replace operation targeting /name.

  • python3.9 can no longer be set. Only python3.13 is accepted when creating or patching a script’s runtime; python3.9 is deprecated and appears only in responses for scripts that already had it.

  • Pagination, JSON Patch replace semantics, scopes, environment variable handling, and run log and output downloads are unchanged. See Appendix A.

Endpoint Map#

Scopes are unchanged throughout: file:read for reads, downloads, and export; file:write for writes, execution, cancellation, upload, sync, and dependency generation.

Scripts#

Operation

Prototype

Public

Change

List

GET /prototype/platform/scripts

GET /scripts

none

Create

POST /prototype/platform/scripts

POST /scripts

no default __main__.py source is created, so you will have to create it first; if you use container (optional), this places the script in a folder; response adds resourceLimits; runtime only accepts python3.13

Get

GET /prototype/platform/scripts/{scriptId}

GET /scripts/{scriptId}

response adds resourceLimits

Patch

PATCH /prototype/platform/scripts/{scriptId}

PATCH /scripts/{scriptId}

adds /name; /description and /runtime remain patchable, but /runtime only accepts python3.13; still one replace operation per request

Sources#

Operation

Prototype

Public

Change

List

GET .../{scriptId}/sources

GET .../{scriptId}/sources

none

Create

POST .../{scriptId}/sources

POST .../{scriptId}/sources

response adds created and type

Get

GET .../sources/{sourceId}

GET .../sources/{sourceId}

response adds created and type

Patch

PATCH .../sources/{sourceId}

PATCH .../sources/{sourceId}

none; /path remains the only patchable path

Delete

DELETE .../sources/{sourceId}

DELETE .../sources/{sourceId}

none

Content download

POST .../contentDownload

POST .../contentDownload

adds 204 No Content when the source has no content

Content upload

POST .../contentUpload

POST .../contentUpload

none

Push a source tree

not available

POST .../sources/sync

new

Pull a source tree

not available

POST .../sources/export

new

Runs#

Operation

Prototype

Public

Change

Execute

POST .../{scriptId}/execution

POST .../{scriptId}/execution

breaking: changes response contract. Location now points at an operation, not the run. See Polling operations

List

GET .../{scriptId}/runs

GET .../{scriptId}/runs

adds type, $filter on started.dateTime, and dependency runs in the same collection

Get

GET .../runs/{runId}

GET .../runs/{runId}

python-failed removed from status; adds type, termination, resourceUsage, resourceLimits, dependencyState, and duration fields

Cancel

POST .../runs/{runId}/cancellation

POST .../runs/{runId}/cancellation

breaking: changes response code. 202 Accepted becomes 204 No Content

Logs download

POST .../runs/{runId}/logsDownload

POST .../runs/{runId}/logsDownload

none

Output download

POST .../runs/{runId}/outputDownload

POST .../runs/{runId}/outputDownload

none

Dependencies#

Dependency management was never exposed in the prototype API. It is new, and dependency generation produces a run in the same collection as your script runs, distinguished by type: dependency.

Operation

Public

Generate a bundle

POST /scripts/{scriptId}/dependencies/generation

Read a dependency run

GET /scripts/{scriptId}/runs/{runId}

Cancel a dependency run

POST /scripts/{scriptId}/runs/{runId}/cancellation

Read dependency generation logs

POST /scripts/{scriptId}/runs/{runId}/logsDownload

Permissions#

Operation

Prototype

Public

Change

Get permissions

GET .../{scriptId}/permissions

GET .../{scriptId}/permissions

none

Modify permissions

POST .../{scriptId}/permissions/modification

POST .../{scriptId}/permissions/modification

none

Behavior Changes#

The Python examples use requests and assume this setup:

import os
import time

import requests

API_BASE = "https://api.app.wdesk.com"
AUTH_HEADERS = {"Authorization": f"Bearer {os.environ['TOKEN']}"}
PUBLIC_HEADERS = {
    **AUTH_HEADERS,
    "X-Version": "2026-09-01.scripting.preview",
}

Paths and the version header#

Migration Steps#

  1. Remove the /prototype/platform prefix from every Scripting request path.

  2. Add X-Version: 2026-09-01.scripting.preview to every request, including your token request.

Examples#

curl#

Prototype

curl -X GET "https://api.app.wdesk.com/prototype/platform/scripts" \
  -H "Authorization: Bearer ${TOKEN}"

Public

curl -X GET "https://api.app.wdesk.com/scripts" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "X-Version: 2026-09-01.scripting.preview"
Python#

Prototype

response = requests.get(
    f"{API_BASE}/prototype/platform/scripts",
    headers=AUTH_HEADERS,
    timeout=30,
)
response.raise_for_status()

Public

response = requests.get(
    f"{API_BASE}/scripts",
    headers=PUBLIC_HEADERS,
    timeout=30,
)
response.raise_for_status()

Polling operations instead of the run#

This is the change that affects the most code. In the prototype API, starting a run returned a Location header pointing straight at the new run, so you had the run ID immediately. The public API returns a link to an operation instead. Poll it once to get the run URL from resourceUrl. The URL is available while the run is queued or started, so you can monitor or cancel the run before the operation finishes.

Scripting uses the standard Workiva GET /operations/{operationId} endpoint for polling. It is not repeated in the Scripting preview reference. Poll the URL returned by the Scripting response and continue sending X-Version: 2026-09-01.scripting.preview.

Dependency generation also exposes its run URL before completion. Source upload and sync expose resources that already exist; source export exposes its generated ZIP URL only when the operation completes.

Migration Steps#

  1. Read operationLocation from the 202 response body, or the Location header, instead of treating Location as a run URL.

  2. Poll that URL and take the run URL from resourceUrl as soon as it appears.

  3. Use that URL to read or cancel the run, and continue polling the operation until status is completed, failed, or cancelled, honoring Retry-After between attempts.

Examples#

curl#

Prototype. The run URL arrives in the first response

curl -i -X POST "https://api.app.wdesk.com/prototype/platform/scripts/${SCRIPT_ID}/execution" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"apiKey": "Az86pm34xyz"}'

HTTP/1.1 202 Accepted
Location: https://api.app.wdesk.com/prototype/platform/scripts/{scriptId}/runs/{runId}
Retry-After: 5

Public. The first response points at an operation

curl -i -X POST "https://api.app.wdesk.com/scripts/${SCRIPT_ID}/execution" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -H "X-Version: 2026-09-01.scripting.preview" \
  -d '{"apiKey": "Az86pm34xyz"}'

HTTP/1.1 202 Accepted
Location: https://api.app.wdesk.com/operations/128f274395254cf17eda6b3eb3d021b9
Retry-After: 5

{
  "operationLocation": "https://api.app.wdesk.com/operations/128f274395254cf17eda6b3eb3d021b9"
}

Poll it once and read the run URL from resourceUrl

curl -X GET "https://api.app.wdesk.com/operations/128f274395254cf17eda6b3eb3d021b9" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "X-Version: 2026-09-01.scripting.preview"

{
  "id": "128f274395254cf17eda6b3eb3d021b9",
  "status": "started",
  "resourceUrl": "/scripts/ae82b647-8e43-44c3-a4e7-2aa3294c87ac/runs/8e1f2c90-3d4b-4a11-9c77-1b2a3c4d5e6f"
}

Keep polling the operation until it reaches a terminal status. Poll the URL you were given rather than assembling one from an ID.

Python#

Prototype

started = requests.post(
    f"{API_BASE}/prototype/platform/scripts/{os.environ['SCRIPT_ID']}/execution",
    headers=AUTH_HEADERS,
    json={"apiKey": "Az86pm34xyz"},
    timeout=30,
)
started.raise_for_status()
run_url = started.headers["Location"]

Public

started = requests.post(
    f"{API_BASE}/scripts/{os.environ['SCRIPT_ID']}/execution",
    headers=PUBLIC_HEADERS,
    json={"apiKey": "Az86pm34xyz"},
    timeout=30,
)
started.raise_for_status()
operation_url = started.json()["operationLocation"]

run_url = None
while True:
    polled = requests.get(operation_url, headers=PUBLIC_HEADERS, timeout=30)
    polled.raise_for_status()
    operation = polled.json()
    run_url = operation.get("resourceUrl", run_url)
    if operation["status"] in {"completed", "failed", "cancelled"}:
        break
    time.sleep(int(polled.headers.get("Retry-After", "5")))

Cancellation is synchronous#

Migration Steps#

  1. Expect 204 No Content instead of 202 Accepted.

  2. Remove any polling that followed the old Location header.

  3. Handle 409 Conflict, returned when the run has already reached a terminal state.

Examples#

curl#

Prototype returned 202 Accepted with a Location header to poll. Public returns

curl -i -X POST "https://api.app.wdesk.com/scripts/${SCRIPT_ID}/runs/${RUN_ID}/cancellation" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "X-Version: 2026-09-01.scripting.preview"

HTTP/1.1 204 No Content
Python#
cancelled = requests.post(
    f"{API_BASE}/scripts/{os.environ['SCRIPT_ID']}/runs/{os.environ['RUN_ID']}/cancellation",
    headers=PUBLIC_HEADERS,
    timeout=30,
)
cancelled.raise_for_status()
assert cancelled.status_code == 204

The run status vocabulary is narrower#

python-failed has been removed. A Python error now reports status: failed, and the detail moves into failureReason and a structured termination object, which also covers memory, disk, and time limits that the prototype status enum could not express.

Migration Steps#

  1. Remove any branch that tests for python-failed.

  2. Branch on the six remaining values: acknowledged, queued, started, completed, cancelled, failed.

  3. Read failureReason and termination for failure detail. failureReason is human-readable; use termination.type and termination.reason for logic.

Examples#

Response comparison#

Prototype

{
  "id": "8e1f2c90-3d4b-4a11-9c77-1b2a3c4d5e6f",
  "status": "python-failed",
  "failureReason": "Python failure"
}

Public

{
  "id": "8e1f2c90-3d4b-4a11-9c77-1b2a3c4d5e6f",
  "type": "script",
  "status": "failed",
  "failureReason": "Python failure",
  "termination": {
    "type": "unexpected_termination",
    "reason": "runner_system_failure",
    "signal": "SIGTERM",
    "message": "Script terminated before completion."
  }
}
Python#

Prototype

if run["status"] == "python-failed":
    handle_failure(run["failureReason"])

Public

if run["status"] == "failed":
    termination = run.get("termination", {})
    handle_failure(
        run.get("failureReason"),
        termination.get("type"),
        termination.get("reason"),
    )

Dependency generation is explicit#

If you manage requirements.txt through the source endpoints, note that nothing regenerates the dependency bundle for you. After changing requirements.txt or a script’s runtime, call POST /scripts/{scriptId}/dependencies/generation, or the next run uses a stale dependencies.zip.

Migration Steps#

  1. Call dependency generation after any change to requirements.txt or runtime.

  2. Poll the returned operation and read the dependency run from resourceUrl as soon as it appears; continue polling until the operation finishes.

  3. Check dependencyState.requirementsInSync and dependencyState.runtimeInSync on a run to confirm it used a current bundle.

Example#

There is no example for this using curl.

Python#

Prototype runtime patches started dependency generation implicitly

requests.patch(
    f"{API_BASE}/prototype/platform/scripts/{os.environ['SCRIPT_ID']}",
    headers=AUTH_HEADERS,
    json=[{"op": "replace", "path": "/runtime", "value": "python3.13"}],
    timeout=30,
).raise_for_status()

Public clients must make the generation request explicitly

requests.patch(
    f"{API_BASE}/scripts/{os.environ['SCRIPT_ID']}",
    headers=PUBLIC_HEADERS,
    json=[{"op": "replace", "path": "/runtime", "value": "python3.13"}],
    timeout=30,
).raise_for_status()

generation = requests.post(
    f"{API_BASE}/scripts/{os.environ['SCRIPT_ID']}/dependencies/generation",
    headers=PUBLIC_HEADERS,
    timeout=30,
)
generation.raise_for_status()
operation_url = generation.json()["operationLocation"]

Rate limiting#

429 Too Many Requests is a documented response on every Scripting operation. Add retry with backoff, and honor Retry-After. Polling an operation can be limited to one request per second.

Migration Checklist#

  1. Remove the /prototype/platform prefix from all Scripting paths.

  2. Add X-Version: 2026-09-01.scripting.preview to every request.

  3. After creating a script, create __main__.py as its first source.

  4. Replace run-URL handling on execution with operation polling, and take the run URL from resourceUrl as soon as it appears.

  5. Change cancellation to expect 204, drop the follow-up polling, and handle 409.

  6. Remove python-failed handling; branch on the six statuses and read termination for detail.

  7. Call dependency generation explicitly after changing requirements.txt or runtime.

  8. Add retry with backoff for 429.

  9. Stop creating or patching scripts with runtime: python3.9; only python3.13 is accepted.

  10. Optional: replace per-file source loops with sources/sync and sources/export.

  11. Optional: pass container on create to place new scripts in a folder.

  12. Optional: use $filter on started.dateTime when listing runs instead of paging through full history.

Appendix A: What Has Not Changed From the Prototype API#

If you use the prototype endpoints, you can keep this code as it is.

Authentication

OAuth 2.0 client credentials from POST /oauth2/token, sent as Authorization: Bearer {token}.

Scopes

file:read and file:write, applied to the same operations as before.

Base URLs

https://api.app.wdesk.com, plus the eu, apac, and ca equivalents.

Pagination

Requests take $maxpagesize and $next; responses carry data and @nextLink. $maxpagesize still accepts 1 to 1000 and defaults to 1000, and @nextLink is still absent on the last page.

Run download responses

logsDownload and outputDownload still return 200 OK with {"downloadUrl": "..."}, and still return 204 No Content when there is nothing to download. Presigned URLs are still short-lived.

Content upload

Still 202 Accepted with uploadUrl in the body and an operation to poll in the Location header.

JSON Patch

Still one replace operation per request. /description and /runtime remain patchable on a script, and /path remains patchable on a source.

Execution parameters

Request body properties still become environment variables in capitalized snake case, so apiKey becomes API_KEY. Values must be strings.

Output capture

print("::set-output ...") still populates run output.

Script state

Still active, trashed, or purged, and still read-only. There was no way to delete or trash a script through the prototype API and there is none in the public API.

Error body

{"code", "message", "target", "details"} with the same codes.

Limits

Request payloads are still capped at 10 MB, which is why content moves through presigned URLs.