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/platformprefix is gone./prototype/platform/scriptsbecomes/scripts.Send
X-Version: 2026-09-01.scripting.previewon every request. Prototype requests sent without anX-Versionheader were served by2022-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 withcan'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-failedis no longer a run status. Failures reportstatus: failedplusfailureReasonand a structuredterminationobject.Script names can now be changed with a
replaceoperation targeting/name.python3.9can no longer be set. Onlypython3.13is accepted when creating or patching a script’sruntime;python3.9is deprecated and appears only in responses for scripts that already had it.Pagination, JSON Patch
replacesemantics, 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 |
|
|
none |
Create |
|
|
no default |
Get |
|
|
response adds |
Patch |
|
|
adds |
Sources#
Operation |
Prototype |
Public |
Change |
|---|---|---|---|
List |
|
|
none |
Create |
|
|
response adds |
Get |
|
|
response adds |
Patch |
|
|
none; |
Delete |
|
|
none |
Content download |
|
|
adds |
Content upload |
|
|
none |
Push a source tree |
not available |
|
new |
Pull a source tree |
not available |
|
new |
Runs#
Operation |
Prototype |
Public |
Change |
|---|---|---|---|
Execute |
|
|
breaking: changes response contract. |
List |
|
|
adds |
Get |
|
|
|
Cancel |
|
|
breaking: changes response code. |
Logs download |
|
|
none |
Output download |
|
|
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 |
|
Read a dependency run |
|
Cancel a dependency run |
|
Read dependency generation logs |
|
Permissions#
Operation |
Prototype |
Public |
Change |
|---|---|---|---|
Get permissions |
|
|
none |
Modify permissions |
|
|
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#
Remove the
/prototype/platformprefix from every Scripting request path.Add
X-Version: 2026-09-01.scripting.previewto 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#
Read
operationLocationfrom the202response body, or theLocationheader, instead of treatingLocationas a run URL.Poll that URL and take the run URL from
resourceUrlas soon as it appears.Use that URL to read or cancel the run, and continue polling the operation until
statusiscompleted,failed, orcancelled, honoringRetry-Afterbetween 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#
Expect
204 No Contentinstead of202 Accepted.Remove any polling that followed the old
Locationheader.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#
Remove any branch that tests for
python-failed.Branch on the six remaining values:
acknowledged,queued,started,completed,cancelled,failed.Read
failureReasonandterminationfor failure detail.failureReasonis human-readable; usetermination.typeandtermination.reasonfor 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#
Call dependency generation after any change to
requirements.txtorruntime.Poll the returned operation and read the dependency run from
resourceUrlas soon as it appears; continue polling until the operation finishes.Check
dependencyState.requirementsInSyncanddependencyState.runtimeInSyncon 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#
Remove the
/prototype/platformprefix from all Scripting paths.Add
X-Version: 2026-09-01.scripting.previewto every request.After creating a script, create
__main__.pyas its first source.Replace run-URL handling on execution with operation polling, and take the run URL from
resourceUrlas soon as it appears.Change cancellation to expect
204, drop the follow-up polling, and handle409.Remove
python-failedhandling; branch on the six statuses and readterminationfor detail.Call dependency generation explicitly after changing
requirements.txtorruntime.Add retry with backoff for
429.Stop creating or patching scripts with
runtime: python3.9; onlypython3.13is accepted.Optional: replace per-file source loops with
sources/syncandsources/export.Optional: pass
containeron create to place new scripts in a folder.Optional: use
$filteronstarted.dateTimewhen 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 |
Scopes |
|
Base URLs |
|
Pagination |
Requests take |
Run download responses |
|
Content upload |
Still |
JSON Patch |
Still one |
Execution parameters |
Request body properties still become environment variables in capitalized snake case, so |
Output capture |
|
Script state |
Still |
Error body |
|
Limits |
Request payloads are still capped at 10 MB, which is why content moves through presigned URLs. |