Submitting jobs
The render and simulation job lifecycle — job types, cost estimation, the upload→submit flow, idempotency, and polling.
Type: How-to · Audience: Pipeline TDs, researchers, automation builders
The job lifecycle
estimate (optional) → upload-url → PUT file → submit → poll status → outputs
All five steps use the same /v1/jobs/* namespace. Estimate and upload-url are
optional but recommended: estimate before spending credits, upload before
referencing a scene file.
Large inputs (films, VFX caches, model weights)? The single-PUT
upload-urlis capped at 5 GiB and is not resumable. Use the resumable, integrity-checked multipart flow (POST /v1/uploads) instead, then submit the returnedkeyassceneFile. There is no 25 MB-class limit — bytes go straight to object storage. See Large-file uploads.
Job status values: SUBMITTED → RUNNING → SUCCEEDED | FAILED | CANCELLED
Job types
The jobType field is the discriminator that routes the job to the right backend
and pricing model.
Render jobs (jobType: "render")
Frame-based jobs for Blender, Maya, Nuke, Houdini, V-Ray, etc.
Required fields: sceneFile, frameStart, frameEnd
{
"jobType": "render",
"sceneFile": "uploads/scene-abc123.blend",
"frameStart": 1,
"frameEnd": 250,
"outputFormat": "EXR",
"jobName": "product-shot-v3"
}
Simulation jobs (jobType: "simulation")
Task-based jobs for embarrassingly parallel scientific workloads include molecular docking (AutoDock Vina), sequence search (BLAST), and coastal wave simulation (SWAN/WAVEWATCH III). Tightly coupled MPI workloads (GROMACS, OpenFOAM, WRF) are not yet supported.
Required fields: sceneFile (the input dataset), simulator, taskCount.
Use blastp for protein queries and blastn for nucleotide queries. They are
distinct simulators; the generic blast alias is not supported. AutoDock Vina
and both BLAST simulators are deployment capabilities that are off by default,
so confirm that they are enabled before relying on them. Folding identifiers,
including alphafold and esmfold, are not runnable while no folding runtime
is installed and must not be submitted.
BLAST requires a database pinned by the deployment. Every request identifies its path, name, version, publication date, sequence type, and checksum; there is no implicit database fallback. Successful output is a structured, ranked hit list with the selected database metadata and execution provenance.
A protein BLAST request can use these schema-backed parameters:
{
"jobType": "simulation",
"sceneFile": "uploads/queries.fasta",
"simulator": "blastp",
"taskCount": 1,
"simulationParams": {
"blastDatabasePath": "db/uniref-2026-07/uniref",
"blastDatabaseName": "uniref",
"blastDatabaseVersion": "2026-07",
"blastDatabaseDate": "2026-07-01",
"blastDatabaseType": "prot",
"blastDatabaseChecksum": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"blastEvalue": 0.00001,
"blastMaxHits": 100
}
}
An AutoDock Vina request can specify an exact receptor and search box:
{
"jobType": "simulation",
"sceneFile": "uploads/ligands.sdf",
"simulator": "autodock-vina",
"taskCount": 1,
"simulationParams": {
"receptorPdbId": "1HSG",
"boxCenterAngstrom": [1.25, -2.5, 3.75],
"boxSizeAngstrom": [20, 20, 20],
"exhaustiveness": 8,
"numModes": 9
}
}
Estimate with the same simulation inputs, review the quoted cost as the
confirmation checkpoint, and submit only after approval. A successful job's
outputs include downloadable result artifacts; BLAST returns ranked hits and
database provenance, while AutoDock Vina returns ranked poses and run
provenance.
Molecular renders (jobType: "molecular")
Publication renders of a MolViewSpec scene — turntables, fly-throughs and
figure movies of a structure. sceneFile is the uploaded scene manifest (not a
DCC scene), and the backend is chosen with metadata.renderEngine:
renderEngine |
Backend | Use it for |
|---|---|---|
molstar |
headless Mol* raster | Turntables, fly-throughs, figure movies — fast; the default |
cycles |
ray-traced (Blender Cycles) | Publication stills and cinematic sequences |
Required fields: sceneFile, frameStart, frameEnd
{
"jobType": "molecular",
"sceneFile": "uploads/8f3c…/scenes/kinase-story.json",
"frameStart": 1,
"frameEnd": 120,
"metadata": { "renderEngine": "molstar", "resolution": "1920x1080", "fps": 30 }
}
Bring your own structure. The scene may reference a structure you uploaded
rather than a public PDB entry. Upload the coordinate file (.pdb, .cif,
.bcif, .sdf, .pdbqt, …), then have the scene name it by its returned storage
key. The render nodes read those bytes from local disk — an uploaded structure
is never fetched over the network, so unpublished and proprietary structures
never leave your account's storage.
Scenes are portable in both directions: export one as .mvsx (a self-contained
MolViewSpec archive bundling the coordinates, openable in any MolViewSpec viewer),
or bring an .mvsj / .mvsx produced elsewhere and render it here.
Cost estimation
Always estimate before submitting expensive jobs.
curl -X POST https://kinocloud.io/api/v1/jobs/estimate \
-H "Authorization: Bearer $KINOCLOUD_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"jobType": "render",
"frameStart": 1,
"frameEnd": 1000,
"metadata": {
"software": "blender",
"renderer": "cycles",
"totalSizeGB": 2.5
}
}'
The breakdown field shows per-component costs so you can tune the job
before committing.
The upload → submit flow
Scene files and input datasets must be uploaded to KinoCloud storage first. The presigned URL flow keeps large files off your API server.
import requests, os
API = "https://kinocloud.io/api/v1"
KEY = os.environ["KINOCLOUD_API_KEY"]
# 1. Presign
r = requests.post(f"{API}/jobs/upload-url",
headers={"Authorization": f"Bearer {KEY}"},
json={"filename": "scene.blend",
"contentType": "application/x-blend",
"size": os.path.getsize("scene.blend")})
r.raise_for_status()
upload = r.json()
# 2. Upload directly to cloud storage (no auth header)
with open("scene.blend", "rb") as f:
requests.put(upload["uploadUrl"],
headers={"Content-Type": "application/x-blend"},
data=f).raise_for_status()
# 3. Submit using the returned s3Key
r = requests.post(f"{API}/jobs/submit",
headers={"Authorization": f"Bearer {KEY}"},
json={"jobType": "render",
"sceneFile": upload["s3Key"],
"frameStart": 1, "frameEnd": 250,
"outputFormat": "EXR"})
r.raise_for_status()
job = r.json()
print(job["jobId"]) # job_01JABCDE12345
Idempotent submission
Add an Idempotency-Key header to make retries safe. If KinoCloud has already
processed a request with that key, it replays the original response instead of
submitting a duplicate job.
curl -X POST https://kinocloud.io/api/v1/jobs/submit \
-H "Authorization: Bearer $KINOCLOUD_API_KEY" \
-H "Idempotency-Key: render-job-2026-06-08-scene-v3" \
-H "Content-Type: application/json" \
-d '{ "jobType": "render", "sceneFile": "...", "frameStart": 1, "frameEnd": 250 }'
Idempotency keys are scoped per user. Use a key that uniquely identifies the logical job — a hash of the scene file + frame range works well.
The response includes "idempotentReplay": true when a cached result is returned.
Rate limits
The API enforces per-key rate limits. When a limit is hit, the response is:
HTTP 429 Too Many Requests
Retry-After: 15
Back off for at least Retry-After seconds before retrying. Implement
exponential backoff for long-running polling loops.
Polling pattern (Python)
import time, requests, os
API = "https://kinocloud.io/api/v1"
KEY = os.environ["KINOCLOUD_API_KEY"]
headers = {"Authorization": f"Bearer {KEY}"}
def poll_job(job_id: str, interval: int = 10) -> dict:
while True:
r = requests.get(f"{API}/jobs/{job_id}/status", headers=headers)
r.raise_for_status()
status = r.json()["status"]
if status in ("SUCCEEDED", "FAILED", "CANCELLED"):
return r.json()
time.sleep(interval)
result = poll_job("job_01JABCDE12345")
if result["status"] == "SUCCEEDED":
outputs = requests.get(
f"{API}/jobs/{result['jobId']}/outputs", headers=headers
).json()
for file in outputs["files"]:
print(file["name"], file["downloadUrl"])
Cancelling a job
curl -X POST https://kinocloud.io/api/v1/jobs/JOB_ID/cancel \
-H "Authorization: Bearer $KINOCLOUD_API_KEY"
Cancellation is best-effort: if the job has already reached SUCCEEDED or
FAILED, the cancel request returns 200 but has no effect.