A submitted job is not yet a successful result
A Python code execution API accepts source code, schedules it on a worker and returns its actual outcome. In cpuOS, POST /v1/jobs returns a job object with HTTP 202. Keep its ID, poll GET /v1/jobs/:id, then inspect terminal status, exit code and output before using the result. This tutorial builds that entire client using Python's standard library.
The example aggregates three synthetic orders into a small JSON object. It deliberately avoids packages, uploaded files and network calls inside the job. You can check the arithmetic locally, then compare the API result to the same expected values.
Scroll horizontally to see every column.
| Request | Meaning | Client action |
|---|---|---|
| POST /v1/jobs | 202 with the accepted or replayed job | Retain the ID and idempotency key. |
| GET /v1/jobs/:id | 200 with the current job | Poll queued/running; validate terminal results. |
| DELETE /v1/jobs/:id | 200 with cancellation or an existing terminal state | Inspect the returned status; cancellation can race completion. |
Connect a worker and keep the API key in your client
- Create a workspace and API key, then enroll a Node 24 + Docker worker using the quickstart. Wait until it reports online with Docker available.
- Run the client on your development machine or application server with Python 3.13 or later. It makes HTTPS requests; the submitted job runs separately on your worker.
- Use trusted team code on a machine allocated to the pilot. Docker shares the worker's host kernel. Do not expose the submission key to browser clients or anonymous users.
- Keep customer data and secrets out of this example. Job code and output pass through the EU-hosted cpuOS control plane; your worker runs wherever you place it.
The current worker supports Python 3.13 and Node 24 standard-library jobs. Job networking, package installation, file transfer, browsers and persistent sessions are unavailable. An API key authorizes workspace jobs; a worker token belongs on the worker and is not a substitute for that key.
# Run in your client shell. Do not paste the key into submitted code.read -r -s -p 'cpuOS API key: ' CPUOS_API_KEY; printf '\n'export CPUOS_API_KEYexport CPUOS_BASE_URL='https://cpuos.si'# Save the client below as cpuos_orders.py, then run:python3 cpuos_orders.pyDefine a deterministic Python task
Use integer cents instead of binary floating-point prices. SKU A contributes 3,600 cents and SKU B contributes 1,500 cents, for six units and 5,100 cents total. The job prints one JSON value to stdout and uses stderr for diagnostics. Save the following as orders.py and run python3 orders.py locally if you want to verify the transformation first.
import json# Synthetic orders, with prices represented as integer cents.orders = [ {"sku": "A", "unit_cents": 1200, "quantity": 2}, {"sku": "B", "unit_cents": 500, "quantity": 3}, {"sku": "A", "unit_cents": 1200, "quantity": 1},]by_sku = {}for order in orders: revenue = order["unit_cents"] * order["quantity"] by_sku[order["sku"]] = by_sku.get(order["sku"], 0) + revenueprint(json.dumps({ "rows": len(orders), "units": sum(order["quantity"] for order in orders), "revenue_cents": sum(by_sku.values()), "by_sku": by_sku,}, sort_keys=True)){ "rows": 3, "units": 6, "revenue_cents": 5100, "by_sku": { "A": 3600, "B": 1500 }}For your own task, define a schema and domain checks before submission. A valid JSON value does not establish that an answer is correct. This tutorial compares the entire parsed object to a known result; a production application would validate types, required fields and its own business invariants.
Submit and poll with a complete standard-library client
Save this block as cpuos_orders.py. It includes the job source and expected result. The job timeout is 30 seconds; the client checks a separate 120-second waiting budget around HTTP calls, polling and retry waits. Its socket timeout limits inactivity to at most 10 seconds.
A continuously arriving response can outlast that waiting budget. This sample has no hard wall-clock deadline. If you supervise it in a separate process, also arrange job cancellation: stopping the client does not stop its accepted job.
import jsonimport osimport reimport sysimport timeimport uuidfrom urllib.error import HTTPError, URLErrorfrom urllib.parse import urlsplitfrom urllib.request import Request, HTTPRedirectHandler, build_openerBASE_URL = os.environ.get("CPUOS_BASE_URL", "https://cpuos.si").rstrip("/")API_KEY = os.environ.get("CPUOS_API_KEY", "")IDEMPOTENCY_KEY = os.environ.get("CPUOS_IDEMPOTENCY_KEY") or str(uuid.uuid4())if not re.fullmatch(r"cpuos_key_[A-Za-z0-9]{40}", API_KEY): raise SystemExit("Set CPUOS_API_KEY to a valid workspace API key.")if not re.fullmatch(r"[!-~]{1,128}", IDEMPOTENCY_KEY): raise SystemExit("CPUOS_IDEMPOTENCY_KEY must contain 1 to 128 visible ASCII characters.")POLL_SECONDS = 2CLIENT_BUDGET_SECONDS = 120TERMINAL = {"completed", "failed", "cancelled"}CODE = "import json\n\n# Synthetic orders, with prices represented as integer cents.\norders = [\n {\"sku\": \"A\", \"unit_cents\": 1200, \"quantity\": 2},\n {\"sku\": \"B\", \"unit_cents\": 500, \"quantity\": 3},\n {\"sku\": \"A\", \"unit_cents\": 1200, \"quantity\": 1},\n]\nby_sku = {}\nfor order in orders:\n revenue = order[\"unit_cents\"] * order[\"quantity\"]\n by_sku[order[\"sku\"]] = by_sku.get(order[\"sku\"], 0) + revenue\nprint(json.dumps({\n \"rows\": len(orders),\n \"units\": sum(order[\"quantity\"] for order in orders),\n \"revenue_cents\": sum(by_sku.values()),\n \"by_sku\": by_sku,\n}, sort_keys=True))"EXPECTED = {"rows":3,"units":6,"revenue_cents":5100,"by_sku":{"A":3600,"B":1500}}# Keep the Bearer key on the configured origin, including during redirects.class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return Noneopener = build_opener(NoRedirect())origin = urlsplit(BASE_URL)if not origin.hostname or (origin.scheme != "https" and not ( origin.scheme == "http" and origin.hostname in {"localhost", "127.0.0.1"})) or origin.username or origin.password or origin.path or origin.query or origin.fragment: raise ValueError("CPUOS_BASE_URL must be an HTTPS origin (localhost HTTP is allowed).")def remaining(deadline): seconds = deadline - time.monotonic() if seconds <= 0: raise TimeoutError("Client waiting budget exhausted.") return secondsdef pause(seconds, deadline): if seconds >= remaining(deadline): raise TimeoutError("Not enough waiting budget for the next request.") time.sleep(seconds)def request(method, path, deadline, payload=None, idempotency_key=None): data = None if payload is None else json.dumps(payload).encode("utf-8") headers = {"Authorization": "Bearer " + API_KEY, "Content-Type": "application/json"} if idempotency_key: headers["Idempotency-Key"] = idempotency_key for attempt in range(3): delay = 2 ** attempt req = Request(BASE_URL + path, data=data, headers=headers, method=method) try: with opener.open(req, timeout=min(10, remaining(deadline))) as response: result = json.load(response) remaining(deadline) if not isinstance(result, dict): raise ValueError("Expected a JSON object from the jobs API.") return result except HTTPError as exc: try: error = json.load(exc).get("error", {}) except (ValueError, AttributeError): error = {} code = error.get("code", "http_error") retryable = exc.code in {429, 500, 502, 503, 504} if code in {"no_online_nodes", "queue_full"}: retryable = False if not retryable or attempt == 2: raise RuntimeError("HTTP " + str(exc.code) + " (" + code + "): " + error.get("message", "Request failed.")) from None if exc.code == 429: retry_after = exc.headers.get("Retry-After", "60") delay = int(retry_after) if retry_after.isdecimal() else 60 except (URLError, TimeoutError, OSError): remaining(deadline) if attempt == 2: raise RuntimeError("Transport failed after three attempts; submission may exist.") from None pause(delay, deadline)def cancel(job_id): # Cancellation checks its own waiting budget between HTTP operations. result = request("DELETE", "/v1/jobs/" + job_id, time.monotonic() + 10) print("Cancellation response: " + result["status"], file=sys.stderr) return resultdef validate(job): if job["status"] != "completed" or job.get("exitCode") != 0 or job.get("error"): raise RuntimeError("Job " + job["status"] + ": " + str(job.get("error") or job.get("stderr") or "no successful result")) if job.get("outputTruncated"): raise RuntimeError("Output was truncated; do not parse it as a complete result.") result = json.loads(job["stdout"]) if result != EXPECTED: raise ValueError("The synthetic order result does not match its expected contract.") return resultdef main(): deadline = time.monotonic() + CLIENT_BUDGET_SECONDS job = None # Retain this value to recover an uncertain POST with the same payload. print("Idempotency key: " + IDEMPOTENCY_KEY, file=sys.stderr) try: job = request("POST", "/v1/jobs", deadline, { "name": "Synthetic order totals", "template": "python", "code": CODE, "vcpu": 1, "memoryMb": 512, "timeoutSeconds": 30, }, idempotency_key=IDEMPOTENCY_KEY) if not re.fullmatch(r"[A-Za-z0-9]{20}", job.get("id", "")): raise ValueError("Invalid job identifier in the API response.") job_id = job["id"] while job["status"] not in TERMINAL: if job["status"] not in {"queued", "running"}: raise ValueError("Unknown job state.") pause(POLL_SECONDS, deadline) job = request("GET", "/v1/jobs/" + job_id, deadline) print(json.dumps(validate(job), sort_keys=True)) except (Exception, KeyboardInterrupt): if (job and job.get("status") not in TERMINAL and re.fullmatch(r"[A-Za-z0-9]{20}", job.get("id", ""))): try: cancel(job["id"]) except Exception as exc: print("Could not confirm cancellation: " + str(exc), file=sys.stderr) raiseif __name__ == "__main__": try: main() except KeyboardInterrupt: sys.exit(130) except Exception as exc: print(str(exc), file=sys.stderr) sys.exit(1)On success, stdout contains the expected JSON value. The idempotency key is written to stderr so you can retain it after an uncertain submission. The client uses urllib.request for JSON requests and time.monotonic for elapsed waiting time. It does not follow redirects with your Bearer key.
Check terminal status, exit code and complete output
Scroll horizontally to see every column.
| Field or state | Interpretation |
|---|---|
| queued / running | Accepted or executing. stdout is not a live log stream. |
| completed and exitCode = 0 | The process succeeded. Parse and validate the result separately. |
| failed | Process failure, execution error, expired queue or lost worker. Inspect error, stderr and exitCode; exitCode can be null. |
| cancelled | Cancellation was recorded. Do not treat this attempt as a successful tool result. |
| outputTruncated = true | Output exceeded the capture limit. Reject it as an incomplete JSON contract even when exitCode is zero. |
The current source limit is 64 KiB and stdout/stderr capture is bounded at 64 KiB per stream. Requests can allocate 1–2 CPUs, 128–2,048 MiB and 1–120 seconds. Keep machine-readable output small. A worker that loses its lease is recorded as failed; the service does not automatically rerun the job.
A completed process with the wrong totals still fails this client's result check. Return a checked value to an agent rather than passing arbitrary stdout straight into the next action. See running model-generated code with explicit limits for the authorization boundary.
Replay uncertain requests and cancel unfinished work
The client attempts each HTTP request at most three times. It reuses one Idempotency-Key and the exact same payload when retrying submission after a transport error or retryable HTTP response. cpuOS returns the same workspace job for that key and normalized parameters. Different parameters with the same key return HTTP 409 idempotency_conflict; choose a new key for a new execution.
A fresh script run generates a new key and therefore requests a new execution. To recover an earlier uncertain POST, set CPUOS_IDEMPOTENCY_KEY to the key printed by that run and keep the job parameters unchanged. Idempotent replay does not mean retrying a failed computation: replay returns that attempt's current result, including terminal failure.
Polling every two seconds makes about 30 requests a minute for one active client. The workspace limit is 60 API requests or job submissions per minute across keys and clients. Respect HTTP 429 and Retry-After; the client stops if that wait would exceed its remaining budget. For multiple concurrent jobs, coordinate polling in your application.
On client waiting timeout or Ctrl-C, the example tries DELETE /v1/jobs/:id if it has a job ID. Queued jobs are marked cancelled; the worker is notified to stop running work through its polling loop. The response is not a promise of instantaneous process termination. If completion won the race, DELETE returns the existing terminal result. If cancellation cannot be confirmed, check the job in the dashboard before resubmitting.
A transport failure before receiving an ID leaves submission uncertain. Preserve the idempotency key and recover the same intent instead of creating another job blindly. If your client process is killed, its cleanup handler cannot run; the server-side job timeout remains separate from your application's waiting budget.
Diagnose failures before changing the task
Scroll horizontally to see every column.
| Response or result | Next step |
|---|---|
| 400 invalid_input / invalid_json | Check field names, bounds and UTF-8 JSON. Extra job fields are rejected. |
| 401 invalid_api_key | Use a current workspace API key, not a worker token. |
| 404 job_not_found | Check the ID and the API key's workspace. |
| 409 idempotency_conflict | Recover with unchanged parameters, or use a new key for a new intent. |
| 429 rate_limit_exceeded / queue_full | Coordinate request rates, wait for capacity or cancel unnecessary queued jobs. |
| 503 no_online_nodes | Connect a recent-heartbeat Docker worker with the requested CPU and memory. |
| failed with worker_lost / queue_expired | Check the worker and review the attempt before requesting another execution. |
| completed with invalid JSON or wrong totals | Fix the output contract or computation. HTTP success is not result correctness. |
Before replacing the synthetic task, test a nonzero exit, a script longer than its timeout, a deliberately oversized output and queued/running cancellation in a controlled pilot. The published client is validated against local HTTP fixtures and the arithmetic is executed locally; these checks do not substitute for testing your own connected Docker worker.
Wire the checked result into your application
For a different Python task, start with CSV analysis without pandas. The JavaScript execution guide supplies a reusable Node client that reads a local Python or JavaScript source file before submitting it. For worker availability and interrupted attempts, use the worker operations guide.
Use LangChain tool wiring when an agent proposes a calculation, or n8n HTTP jobs for an approved workflow step. Keep API credentials, input validation, resource limits and retry decisions in the application. The execution job needs only its authorized source and small inline input.
For model-generated tasks, gpuOS open models supply the reasoning step on your GPUs. Your application separately submits code to cpuOS and returns a validated result to the model. The code interpreter guide explains this division; this tutorial supplies the complete HTTP client for the execution step.