Run a background service
Run a Web Worker for your app while the Dashboard is open.
A background service is a Web Worker that the Dashboard starts when it loads your organization's applications. It keeps running while the Dashboard is open, even if nobody opens your app, and runs once in each Dashboard tab. Services run for apps with default or unlisted visibility, and not for disabled apps.
Beta API
unstable_defineService can change without notice. A service can run code and log output. It doesn't have access to the App SDK or the user's token.
Declare the service
import {defineApplication, defineCliConfig, defineWebWorker} from 'sanity/cli'
export default defineCliConfig({
app: defineApplication({
organizationId: 'your-organization-id',
slug: 'content-sync',
title: 'Content sync',
entry: './src/App.tsx',
webWorkers: [
defineWebWorker({name: 'sync', title: 'Content sync', src: './src/worker.ts'}),
],
}),
})src is relative to the project root, the folder that contains sanity.cli.ts. The worker's name must be unique across the app, its views, and its other web workers.
Export the worker callback
import {unstable_defineService} from 'sanity/dashboard'
export default unstable_defineService('worker', ({service}) => {
const interval = setInterval(() => {
console.warn(`${service.name} is running`)
}, 10_000)
return () => clearInterval(interval)
})The callback runs once when the worker starts and receives service, which contains the worker's name and type. Return a cleanup function for timers, listeners, and connections.
The worker can use any Web Worker API, such as fetch.
Talk to the app's views
The callback also receives bus, the Dashboard message bus. It connects the worker to the views of the same app, and nothing else: only your app's views can reach the worker, and only your app's views receive what it sends.
bus.emit(topic, payload): send a message.bus.query(stateTopic): read a state topic's current value.bus.subscribe(stateTopic, handler, {signal}): listen to a state topic.
Answer your views
Subscribe to workers.post to answer a post from one of your app's views.
import {unstable_defineService} from 'sanity/dashboard'
export default unstable_defineService('worker', ({bus, service}) => {
const controller = new AbortController()
bus.subscribe(
'workers.post',
(message) => {
const {name, payload} = message
console.warn(`${service.name} answering ${name}`)
message.reply(getSnapshot(payload))
},
{signal: controller.signal},
)
return () => controller.abort()
})A view's post arrives as {name, payload}: name is the worker the view addressed, and payload is what it sent. Call message.reply(value) to resolve the view's promise. message.reject(reason) refuses the post, and the view's promise rejects with REFUSED. If the handler throws, the post is refused with HANDLER_THREW.
Send to your views
Emit workers.message to send a message to the app's views. Only that app's views receive it.
bus.emit('workers.message', {items})Inspect worker output
The Dashboard forwards the worker's console output to the page console and prefixes it with the service name, for example [sanity-workbench] [service:sync] sync is running.
By default, the Dashboard only shows warnings and errors, so it hides console.log, console.info, and console.debug. Use console.warn, or show every level by running this in the browser console and reloading the page:
localStorage.setItem('SANITY_WORKBENCH_LOG_LEVEL', 'debug')Handle errors
If the worker throws, the Dashboard logs the error and stops that service. Other services and views keep running. The Dashboard doesn't restart a failed service; reload the page to start it again.