Some work has no user who waits for it: a report that a cron job asks for, a reminder in one day, a tool that waits for a payment, or a turn that a deploy cut. Nobody opens these threads, so nothing runs them. A worker finds each thread in the session log that has work and no live host, takes it, runs the work, and gives it back.
Every host on this page shares one log store. In production, that is your LogStore. Here, memoryLogState() gives two hosts one memory log, the same as two servers on one database.
Make a host for the worker, and start the worker on it:
import {
createHarnessHost,
createHarnessWorker,
defineHarness,
} from '@tanstack/ai-harness'
import { memoryLogState, memoryLogStore } from '@tanstack/ai-persistence'
import { openaiText } from '@tanstack/ai-openai'
const assistant = defineHarness({
name: 'acme/assistant',
adapter: openaiText('gpt-6.1-sol'),
})
// One log store for every host. Swap in your own store.
const state = memoryLogState()
const hostOn = () =>
createHarnessHost({ persistence: { stores: { log: memoryLogStore({ state }) } } })
const workerHost = hostOn()
const worker = createHarnessWorker({ host: workerHost, harnesses: [assistant] })Run as many workers as you want, on as many machines. Each thread goes to one of them. Each thread opens in its own tenant, with no user.
If a thread fails to open, the worker logs the error and skips that thread for a time. The time doubles after each failure in a row, up to 5 minutes. The other threads still run.
A cron route or a webhook wants the agent to do a job, but it must not wait for the turn. host.submit stores the input in the log and runs nothing:
const apiHost = hostOn()
const receipt = await apiHost.submit({
harness: assistant,
threadId: 'weekly-report',
input: { op: 'prompt', message: 'Write the weekly report.' },
inputId: 'weekly-report-2026-42',
})
console.log(receipt.status) // 'queued'session.schedule runs an input at a time that you pick. The log keeps the schedule, so it runs once, also after a restart or on another machine:
const session = await apiHost.open(assistant, { threadId: 'daily-summary' })
const { scheduleId } = await session.schedule({
at: Date.now() + 60 * 60 * 1000,
input: { op: 'prompt', message: 'Write the daily summary.' },
})
await session.close()To stop a schedule that did not run yet:
const later = await apiHost.open(assistant, { threadId: 'daily-summary' })
await later.cancelSchedule({ scheduleId })
await later.close()A schedule that ran, or an unknown id, changes nothing. While another live host owns the thread, schedule and cancelSchedule throw.
A tool sends a reminder, waits one day, and then sends a follow-up. step.sleep in a durable tool waits, and the log keeps the time it wakes:
import { toolDefinition } from '@tanstack/ai'
import { durableTool } from '@tanstack/ai-harness'
import { z } from 'zod'
declare const mail: {
send: (message: { to: string; text: string }) => Promise<void>
}
export const remind = durableTool(
toolDefinition({
name: 'remind',
description: 'Remind a customer, and follow up one day later',
inputSchema: z.object({ to: z.string() }),
}),
async ({ to }, { step }) => {
await step.do('send-reminder', () => mail.send({ to, text: 'Your invoice is due.' }))
await step.sleep({ name: 'wait-a-day', ms: 24 * 60 * 60 * 1000 })
await step.do('send-follow-up', () => mail.send({ to, text: 'Your invoice is late.' }))
return 'Sent.'
},
)Stop a server with recoverable: true. Its running work stops, and the host gives each busy thread back with a wake, so a worker takes it at once:
process.on('SIGTERM', async () => {
await apiHost.close({ recoverable: true })
process.exit(0)
})A worker host stops the same way. To let its threads end their work first, call worker.stop(). It stops the looks for work, and waits until the threads that the worker runs close:
process.on('SIGTERM', async () => {
await worker.stop()
await workerHost.close()
})To stop the work at once, close the host with recoverable: true instead. Another worker runs that work again.
A client reads these threads as any other thread. See Connect clients.