# Jobs

A Promise represents work your process is waiting on. A `Job` represents work a backend owns, which you can find again later from a serializable `JobRef`. That could be a fine-tune, a batch inference run, a crawl, a render, a deployment or a remote coding task: training is its first user, but nothing about it is training-specific.

```ts
import { job, type JobBackend, type JobRef } from "june/train";

declare const renders: JobBackend<string>;
declare const ref: JobRef;

const render = job(ref, renders);

await render.status(); // { state: "running", progress: { fraction: 0.5 } }
await render.snapshot(); // the ref and last state, as JSON
await render.cancel();
const url = await render.result(); // polls until the job settles
```

## Why not a Promise

A Promise only exists inside the process that made it. If a three-hour fine-tune were one Promise, a crash or a deploy would lose it, and the only way back would be to start over.

A Job holds nothing but its `ref`. Every call asks its **backend** (a provider's API, files on disk, your database), which owns the job's real state. So a Job rebuilt from a saved ref behaves exactly like the original:

| Promise                         | Job                                             |
| ------------------------------- | ----------------------------------------------- |
| lives and dies with the process | identified by a JSON `ref` you can save         |
| resolves or rejects once        | can be asked for its status any number of times |
| can't be canceled               | `cancel()` asks the backend to stop it          |
| has no progress                 | `status()` reports progress the backend knows   |

## Refs and the lifecycle

```text
start → save the ref → status, as often as you like → your process exits
      → rebuild the Job from the ref → status, result or cancel
```

A `JobRef` is `{ id, kind, data? }`. `kind` names the sort of job (`"render"`, `"fine-tune"`) and appears in errors; `data` holds whatever JSON the backend needs to find the job, such as a region or a directory. Keep secrets out of it: refs are meant to be stored and logged.

Jobs don't make your own work durable. A job keeps running only because something outside your process (a provider, a runner with its state on disk, a service) owns it. Code that runs inside your process, like a workflow's steps, stops when the process does; [snapshots and resume](https://june.dev/docs/workflows/resume.md) reuse the steps that already finished, which is a different thing.

## States

`status()` resolves with a `JobStatus<T>`:

```ts
import type { JobError, JobProgress, JobState } from "june/train";

interface JobStatus<T> {
  state: JobState; // "queued" | "running" | "succeeded" | "failed" | "canceled"
  progress?: JobProgress; // { fraction?, step?, totalSteps?, message?, metrics? }
  result?: T; // once succeeded
  error?: JobError; // { name, message, details? } once failed
}
```

`succeeded`, `failed` and `canceled` are final.

`result(options)` polls `status()` every `pollInterval` milliseconds (5000 by default) until the job settles. It resolves with the result, or rejects with a `JobFailedError` whose `job` is the final snapshot, error included. Its `signal` only stops the waiting: the job keeps running, and you can wait again later. Call `cancel()` to stop the job itself.

## Resuming after a restart

June doesn't persist anything. Save the ref yourself, wherever suits you:

```ts
import { job, type JobBackend, type JobRef } from "june/train";

declare const renders: JobBackend<string>;
declare function submitRender(scene: string): Promise<JobRef>;
declare const db: {
  put(key: string, value: string): Promise<void>;
  get(key: string): Promise<string>;
};

const ref = await submitRender("intro");
await db.put("render", JSON.stringify(ref));

// in another process
const render = job<string>(JSON.parse(await db.get("render")), renders);
const url = await render.result();
```

Whether a job can be found again after a restart is up to its backend. A provider that keeps jobs server-side can always be asked again; a local backend has to keep its state somewhere that outlives the process, such as files on disk.

## Writing a backend

A backend is two calls that take a ref:

```ts
import type { JobBackend, JobRef } from "june/train";

declare const api: {
  get(id: string): Promise<{
    phase: "waiting" | "working" | "done" | "error" | "stopped";
    percent: number;
    url?: string;
    message?: string;
  }>;
  stop(id: string): Promise<void>;
};

export const renders: JobBackend<string> = {
  async status(ref: JobRef) {
    const remote = await api.get(ref.id);
    const progress = { fraction: remote.percent / 100 };
    switch (remote.phase) {
      case "waiting":
        return { state: "queued" };
      case "working":
        return { state: "running", progress };
      case "done":
        return { state: "succeeded", result: remote.url! };
      case "error":
        return {
          state: "failed",
          error: { name: "RenderError", message: remote.message ?? "" },
        };
      case "stopped":
        return { state: "canceled" };
    }
  },
  async cancel(ref: JobRef) {
    await api.stop(ref.id);
  },
};
```

## Fine-tunes are jobs

`startFineTune()` returns a `FineTuneJob`: a `Job<FineTuneResult>` whose backend is the trainer. `trl()` owns its jobs through a Python process and the files in its run directory, and `vertex()` through Vertex AI, so a fine-tune can be checked on, waited for or canceled from any process (for `trl()`, one on the same machine that can read the run directory). See [Fine-tuning](https://june.dev/docs/training/fine-tuning.md#long-running-fine-tunes), and [`<FineTune>`](https://june.dev/docs/training/workflows.md#durability) for the same in a workflow.
