> For AI agents: the complete Sanity documentation index is available at [https://www.sanity.io/docs/llms.txt](https://www.sanity.io/docs/llms.txt).

# 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.

> [!WARNING]
> 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

**sanity.cli.ts**

```typescript
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

**src/worker.ts**

```typescript
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.

**src/worker.ts**

```typescript
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.

**src/worker.ts**

```typescript
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:

**Browser console**

```javascript
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.

## Next steps

[Render a panel](https://www.sanity.io/docs/dashboard-beta/render-a-panel)
Show a UI surface from the dock.

[Extension API reference](https://www.sanity.io/docs/dashboard-beta/extension-api-reference)
Look up service callback properties.

[Talk to your app's worker](https://www.sanity.io/docs/dashboard-beta/talk-to-your-apps-worker)
Post to a worker from a view and listen for its messages.



