# Jobs and batch jobs Long work runs in the background as a job, which you follow by ID. | You want to | Use | | --- | --- | | Change rows with a few SQL statements, published all at once | `nexus.start_batch_job()` | | Run your own Python Spark script | `nexus.jobs.submit()` | ## Before you start [#before-you-start] - Your key's **Access** allows writing. - The workspace has credits; otherwise a job is refused before it starts. - Every example assumes `nexus = Nexus.connect()`. A job spends the workspace's credits while it runs. Check your balance in . ## Batch jobs [#batch-jobs] A batch job runs 1 to 50 SQL statements that change rows and publishes them as one change: all land, or none do. ### Start a batch job [#start-a-batch-job] ```python title="close_old_orders.py" import time from klarety import Nexus, RefusedError, WaitingForOwnerError with Nexus.connect() as nexus: try: job = nexus.start_batch_job([ "UPDATE orders SET status = 'closed' WHERE ordered_at < DATE '2026-01-01'", "DELETE FROM orders WHERE status = 'test'", ]) except RefusedError as refused: raise SystemExit(f"Refused: {refused}") try: state = nexus.jobs.describe(job["job_id"]) while state["state"] in ("running", "publishing"): time.sleep(5) state = nexus.jobs.describe(job["job_id"]) print("Ended as", state["state"]) except WaitingForOwnerError as waiting: print("Waiting for the owner's OK:", waiting, waiting.job_id) ``` ### Follow it [#follow-it] `jobs.describe()` reports `running`, then `publishing`, then how the job ended: `succeeded`, `failed`, `declined` or `expired`. Some changes need the data owner's OK. Then `jobs.describe()` raises `WaitingForOwnerError` with the `job_id` to follow. The job is held, not failed: never resend it. ### Rules [#rules] - Each statement must read or change rows. Change columns with [Tables and schemas](/docs/developers/tables) instead. - An empty table has nothing to start from. Add its first rows with `tables.edit_rows()` or a load. ## Spark script jobs [#spark-script-jobs] `jobs.submit()` runs a Python Spark script inside the resources your plan allows. ### Submit a script [#submit-a-script] ```python from klarety import Nexus, RefusedError with Nexus.connect() as nexus: try: job = nexus.jobs.submit(script_uri, args=["--day", "2026-11-09"]) except RefusedError as refused: raise SystemExit(f"Job refused: {refused}") print(job["job_id"], job["state"]) ``` - `script_uri` is a `.py` file in your workspace's own storage; anywhere else is refused. - `args` are passed to the script, up to 64. - A job runs at most as long as your plan allows one statement to run. ### Follow or cancel it [#follow-or-cancel-it] ```python print(nexus.jobs.describe(job["job_id"])) # submitted, running, succeeded, failed or cancelled nexus.jobs.cancel(job["job_id"]) ``` When a job fails, `exit_reason` says why. ## What can go wrong [#what-can-go-wrong] | Exception | Cause | What to do | | --- | --- | --- | | `RefusedError` | A statement does not change rows, the script is not a `.py` file in your storage, or you asked for more than your plan allows. | Read the message and fix the request. | | `InsufficientCreditsError` | The workspace cannot pay for the job. | Add credits. | | `WaitingForOwnerError` | The change waits for the owner's OK. | Follow `job_id`; do not resend. | | `NotFoundError` | No such job. | Check the ID. | ## Next steps [#next-steps] - [Run SQL](/docs/developers/sql) for reads. - [Spark sessions](/docs/developers/spark) for interactive Spark. - [Limits and rate limits](/docs/developers/limits) for your plan's job resources.