0.1.0-beta.13
March 29, 2026Type-safe background tasks
New task system with the same builder chain as procedures — $use(), $input(), $errors(), $task():
const sendEmail = s.$input(z.object({ to: z.string(), subject: z.string(), body: z.string() })).$task({
name: 'send-email',
resolve: async ({ input, ctx }) => {
await ctx.mailer.send(input)
return { sent: true }
},
})Dispatch from procedures
Call task.dispatch(input, ctx) from inside a procedure. Passing ctx links the task to the parent request's trace:
const createUser = s.$resolve(async ({ input, ctx }) => {
const user = await ctx.db.users.create(input)
sendEmail.dispatch({ to: input.email, subject: 'Welcome' }, ctx)
return user
})The analytics dashboard shows the task as a queue span on the parent request's waterfall:
users/create — 2 spans, 4.2ms total
├─ db.users.create (db) 1.1ms
└─ task:send-email (queue) 92.3msCron scheduling
Add cron to $task() for automatic scheduling. Tasks are auto-discovered when the router is served:
const dailyReport = s.$task({
name: 'daily-report',
cron: '0 9 * * *',
resolve: async ({ ctx }) => ctx.db.reports.generateDaily(),
})Task tracing
Tasks get their own RequestTrace instance. Use ctx.trace() to record spans visible in the analytics dashboard.
See Tasks docs and Analytics docs for the full guide.
createSafeClient()
Every procedure returns { error, data } instead of throwing:
import { createSafeClient } from 'silgi/client'
const client = createSafeClient<AppRouter>(link)
const { error, data, isError, isSuccess } = await client.users.list()Client-side OpenTelemetry
New withOtel() link plugin creates spans for every client call:
import { withOtel } from 'silgi/client/plugins'
const link = withOtel(baseLink, {
tracer: trace.getTracer('my-frontend'),
})
// Spans: rpc.client/users.list, rpc.client/users.create, etc.Stream consumption utilities
import { consumeWithReconnect } from 'silgi/client/consume'
await consumeWithReconnect({
connect: (lastEventId) => client.events.subscribe(undefined, { lastEventId }),
onEvent: (data) => updateUI(data),
maxReconnects: 10,
reconnectDelay: 2000,
})WebSocket client link
import { WSLink } from 'silgi/client/ws'
const link = new WSLink({ url: 'ws://localhost:3000/ws' })
const client = createClient<AppRouter>(link)Built-in routes moved under /api/
All built-in routes now live under the /api/ prefix to avoid collisions with application routes:
| Before | After |
|---|---|
/analytics | /api/analytics |
/analytics/_api/stats | /api/analytics/stats |
/analytics/_api/errors | /api/analytics/errors |
/analytics/_api/requests | /api/analytics/requests |
/reference | /api/reference |
/openapi.json | /api/openapi.json |
The _api infix in analytics sub-routes was removed — it's redundant now that everything is under /api/.
If you query the JSON API directly, update your URLs accordingly.
More improvements
- Async
DynamicLink: Selector can returnPromise<ClientLink> - Retry-After header:
withRetryrespectsRetry-Afterfrom 429/503 responses - Streaming batch:
BatchLinkacceptsstreamingoption for progressive delivery - Cron cleanup:
stopCronJobs()called onSIGINT,SIGTERM, andserver.close()