# Training in workflows

`fineTune()` and `evaluate()` are the canonical APIs. Use `<FineTune>` and `<Evaluate>` only when training or evaluation is itself one stage of a larger June workflow, and should take part in its dependencies, trace, cancellation and composition. Don't wrap a fine-tune in JSX just because JSX exists: a script that trains and evaluates once is clearer as two awaited calls.

## Imperative or JSX

| Use                        | When                                                                                       |
| -------------------------- | ------------------------------------------------------------------------------------------ |
| `fineTune()`, `evaluate()` | scripts, servers, tests, and anywhere you don't need a workflow                            |
| `startFineTune()`, `Job`   | jobs you start in one process and check on in another                                      |
| `<FineTune>`, `<Evaluate>` | training as one part of a larger workflow: preparing data, training, evaluating, deploying |

In a workflow, training gets what every step gets: a trace node, results in `steps` and `--json`, snapshots, `<Catch>`, the run's `signal` and `timeout`, and its concurrency limit.

The components are thin JSX over the same calls. A long-running fine-tune is still a [job](https://june.dev/docs/training/jobs.md) underneath: the trainer runs it in its own process or provider, and `<FineTune>` only starts it, polls it and renders its children with the result. JSX doesn't make the training run inside React.

A workflow that trains, then evaluates the tuned model next to the base model:

```tsx
import { openai } from "june/sdk/openai";
import { Evaluate, FineTune, dataset, type EvalSuite } from "june/train";
import { trl } from "june/trainers";
import type { LanguageModel } from "ai";

declare const suite: EvalSuite<string, string, string>;
declare function serve(uri: string): LanguageModel;

const data = dataset("./fonzie.jsonl").split({
  train: 0.9,
  validation: 0.1,
  seed: 42,
});

export default function TrainVoice() {
  return (
    <FineTune
      id="fonzie"
      model="Qwen/Qwen3-8B"
      trainer={trl()}
      dataset={data}
      method={{ type: "lora", rank: 32 }}
    >
      {(result) => (
        <>
          <Evaluate
            id="tuned-eval"
            name="tuned"
            target={serve(
              result.artifact.type === "adapter" ? result.artifact.uri : "",
            )}
            suite={suite}
          />
          <Evaluate
            id="base-eval"
            name="base"
            target={openai("gpt-5-mini")}
            suite={suite}
          />
        </>
      )}
    </FineTune>
  );
}
```

```sh
june run train-voice.tsx
```

## `<FineTune>`

Takes `fineTune()`'s options (`model`, `trainer`, `dataset`, `method`, `config`, `providerOptions`, `pollInterval`) plus:

| Prop       |                                                                                 |
| ---------- | ------------------------------------------------------------------------------- |
| `id`       | the step's id                                                                   |
| `children` | rendered once training succeeds; a function child receives the `FineTuneResult` |
| `onJob`    | called with the job's ref once it starts, to save for `resume`                  |
| `resume`   | a saved ref: wait on that job instead of starting another                       |

Children render only after the job succeeds, so evals and anything that serves the artifact depend on it like any other step result. A failed or canceled job fails the step with a `JobFailedError`, and its children never render; wrap it in `<Catch>` to handle that.

## `<Evaluate>`

Takes `id`, `target`, `suite`, and optionally `name`, `concurrency` and `cacheKey`. Its result is the `EvalRun`; a function child receives it. A target can be any AI SDK model, including a tuned one, or a function.

Its cache key covers the suite's cases, the graders' names, the target's model id or function name, `name`, and `cacheKey`. Under a `<FineTune>`, it also covers that fine-tune's key, so a retrain on new data re-runs the eval instead of reusing an old snapshot result. June can't see what a grader or function target does, so change `cacheKey` when they change.

## Trace

Both render a node with the usual step fields (`status`, `startedAt`, `endedAt`, `durationMs`, `error`, `result`) plus:

| Field      |                                                                                                             |
| ---------- | ----------------------------------------------------------------------------------------------------------- |
| `job`      | `<FineTune>`: `{ id, kind, state }`, the job's id and normalized state                                      |
| `progress` | `{ fraction, step, totalSteps, message, metrics }` as the trainer reports, or `<Evaluate>`'s finished cases |

`june run` prints job state changes and progress as they happen:

```text
▸ fonzie
  ↳ fine-tune job trl-3f1a9c2e queued
  ↳ fine-tune job trl-3f1a9c2e running
  ↳ 120/400
  ↳ 400/400
  ↳ fine-tune job trl-3f1a9c2e succeeded
✓ fonzie
▸ tuned-eval
  ↳ 50/50
✓ tuned-eval
```

The trace carries the job's id, state, progress and the result, never the request: `providerOptions`, datasets and anything else passed to the trainer stay out of it.

## Cancellation

Aborting the run, through its `signal`, `timeout`, or `handle.abort()`, cancels the trainer's job, and so does a failure elsewhere in the run. The cancel starts at once, even while a status check is in flight, and the run rejects only once it's been sent (or after 30 seconds), so `june run` exiting on Ctrl-C or `--timeout` doesn't leave a paid job running. A cancel that fails doesn't replace the run's error. Unmounting a `<FineTune>` under a `<Catch>` that falls back cancels it the same way.

## Durability

A run lives in one process, but a fine-tune doesn't have to. Two things carry training across a restart:

- **Snapshots.** A finished `<FineTune>`'s result is in the run's `snapshot()`. Its cache key covers the model, trainer, method, config, provider options and the datasets' fingerprints, so a resumed run reuses the artifact only when it would train on the same data the same way.
- **Job refs.** A fine-tune that was still running when the process died isn't in the snapshot. Save its ref with `onJob`, and pass it back as `resume`, and the step waits on that job instead of training again:

```tsx
import { FineTune, dataset, type FineTuneRef } from "june/train";
import { trl } from "june/trainers";

declare function saveRef(ref: FineTuneRef): void;
declare const savedRef: FineTuneRef | undefined;

export default function Train() {
  return (
    <FineTune
      id="fonzie"
      model="Qwen/Qwen3-8B"
      trainer={trl()}
      dataset={dataset("./fonzie.jsonl")}
      method="lora"
      onJob={saveRef}
      resume={savedRef}
    />
  );
}
```

Whether a job can be found again depends on its trainer: `trl()` keeps its state in files and `vertex()` in Vertex, so both can. June has no database; storing snapshots and refs is up to you. See [Jobs](https://june.dev/docs/training/jobs.md) for how refs work, and [Fine-tuning](https://june.dev/docs/training/fine-tuning.md#long-running-fine-tunes) for the same without a workflow.
