Lifecycle callbacks
Pass callbacks undercreateTrainer({ callbacks: { ... } }). They run inside trainer.wait(), dispatched from the backend’s SSE event stream.
Signature
callbacks field on createTrainer is typed Partial<TrainerCallbacks>, so you only specify the events you actually care about. Return values from a callback are discarded; the unknown | Promise<unknown> shape just means TypeScript will not complain if you return something.
When each callback fires
start() without wait(), no callbacks ever run. arkor start calls both for you; programmatic callers must do the same.
Parameters
onStarted({ job })
Fires when the SSE stream reports training.started. Use it for log lines or a “training started” notification.
onLog({ step, loss, evalLoss, learningRate, epoch, samplesPerSecond, job })
Fires repeatedly as training progresses. Each numeric field is number | null: backends only fill in fields they have on a given step (so evalLoss is null on non-eval steps, learningRate may be null between LR-scheduler updates, etc.).
abortSignal only stops your local wait(); call trainer.cancel() afterwards to actually stop the GPU on the backend.
onCheckpoint({ step, adapter, job, infer, artifacts })
Fires when an adapter checkpoint is saved on the backend, while the run is still going. adapter is { kind: "checkpoint", jobId, step }. infer is described in detail on the infer page; in short it takes a chat-style request and returns a raw Response.
onCompleted({ job, artifacts })
Fires once on success. artifacts is unknown[]: the raw artifact list the backend sent. Schemas evolve, so the SDK does not narrow it.
onFailed({ job, error })
Fires once on a backend-reported failure. error is a string (the message the backend sent), not an Error instance.
onFailed is only for backend-side failures. Exceptions thrown inside your other callbacks do not reach onFailed; see Exception handling below for what does happen to them.
Behavior
Sequencing
Each callback is awaited before the next event is dispatched. You can return a promise (writing to a database, posting to Slack, callinginfer) and the SDK will wait for it before processing the next frame. There are no concurrent callback invocations for the same trainer.
Exception handling
Throwing inside a callback rejectswait() immediately with your error. The SDK distinguishes a callback failure from a transport failure: a thrown callback is not routed to the SSE reconnect handler, so the run does not silently retry past it.
This matters because the failing event’s Last-Event-ID has already advanced by the time your callback runs. If a throw were retried, the reconnect would resume after that event, skipping it, swallowing your error, and (for a terminal training.completed event) resolving wait() with empty artifacts as if the run produced nothing. Rejecting instead surfaces the failure where you can act on it.
The reconnect handler is reserved for genuine transport failures (a dropped connection, a transient 5xx). Those are retried with exponential backoff; a thrown callback is not.
For deterministic, non-fatal error handling, catch inside the callback (see the second example below) so a recoverable side-effect failure (a flaky Slack post, a metrics write) doesn’t abort the whole run.
Examples
Minimal: log every event.wait():
Type definitions
TrainingLogContext and CheckpointContext are not exported by name from arkor; mirror the shapes inline if you want typed callback parameters in your own code.
See also
createTrainerfor where these callbacks attach- Run lifecycle for the conceptual flow
inferfor the function exposed toonCheckpoint- Trainer control for
abortSignalandcancel() - Early stopping recipe