VORQ Docs
Guides

Persist and resume jobs

Store job ids so a reload, crash or restart never loses a job or pays for it twice.

The job lives on the network; a JobHandle is only a view of its id. Lose the handle (a page reload, a crash, a deploy) and the job carries on. What you must not lose is the id.

Store the id before you wait

import { WaitTimeout } from "@vorq-ai/client-sdk";

const handle = await client.submit({ model, input, sla: "batch", rateIn, rateOut, provider });
await store.put(handle.id, { status: "submitted" });       // BEFORE waiting on anything

try {
  const result = await handle.result(900);                 // wait up to 15 minutes here
  await store.put(handle.id, { status: "completed", cost: result.cost });
} catch (error) {
  if (!(error instanceof WaitTimeout)) throw error;
  await store.put(handle.id, { status: "waiting" });       // re-attach later; do NOT resubmit
}

store stands for your own storage: a database in Node, localStorage in a page.

Re-attach later

const handle = client.job(jobId);   // no network call; never creates a job
const result = await handle.result();

client.job(id) builds a handle from the id alone. It cannot create or pay for anything, so resuming after a crash is always safe. Any process or tab holding the same wallet (and so the same derived result key) can re-attach.

  • Without a timeoutSeconds, result() waits for the job's whole SLA window from now. A re-attached handle reads the window from the job row, and assumes 1 hour if the row names none.
  • WaitTimeout carries .jobId; client.job(error.jobId!).result() resumes waiting.
  • A timeout never cancels the job.

Make writes idempotent

Key every write by the job id, so processing the same result twice (a retry, two tabs, a resumed worker) is a no-op rather than a duplicate.

Many jobs at once

result() suspends one promise and blocks nothing, so jobs can be in flight concurrently:

const handles = await Promise.all(
  prompts.map((p) => client.submit({ model, input: p, sla: "async", rateIn, rateOut, provider })),
);
await Promise.all(handles.map((h) => store.put(h.id, { status: "submitted" })));
const results = await Promise.all(handles.map((h) => h.result()));

Each handle polls about once a minute. For many requests of the same kind, a batch prices and uploads them together.

Check without waiting

const status = await client.job(jobId).status(); // "queued" | "in_progress" | "completed" | "failed" | "cancelled"

Cancel a job

await client.job(jobId).cancel();

A cancel is signed by the wallet that owns the job and relayed by the coordinator, so the client needs its signer. Only a job no provider has claimed yet can be cancelled; a claimed one is refused with StateConflictError and runs to completion. The signature carries a timestamp the chain accepts within ±600 seconds, so a machine with a badly wrong clock is refused with ValidationError before anything is sent.

On this page