Worker, tools and run server
An extension can run code on the member’s computer in three ways. Each is a JavaScript entry file in the deployed folder, named in matter.json. Matter runs them with its own Node, so the member doesn’t need Node installed, and as the member. They run the same on macOS, Windows and Linux.
Bundle each entry file with what it imports (with esbuild, for example), since the deployment has no node_modules unless you deploy one.
Worker
Section titled “Worker”"worker": "worker.mjs" is a long-running process. It starts on a member’s computer when they first open the extension there, and keeps running, across Matter restarts, until they choose Stop in the extension’s menu. It’s restarted with each new version of the extension.
Use it for work that should happen as data changes, without an agent: sending what someone approved, syncing with a service, tidying up. Watch the tables it cares about with live reads.
A worker, step by step
Section titled “A worker, step by step”matter extension create --worker sets all of this up. To add a worker to a project yourself:
-
Write it, in TypeScript if you like (
src/worker.ts). Import the SDK’s package, as pages do:import matter from '@matter.work/extension-sdk'const context = await matter.context()console.log(`Worker started for ${context.user.name}`)matter.table('Actions').watchIds({ index: 'by_sender_status', eq: { Sender: '$viewer', Status: 'approved' } }, (state) => {if (state.status === 'ready') for (const id of state.value.ids) void send(id)})A worker runs until Matter stops it, and one that exits is started again. Its live reads keep it running.
-
Bundle it, with what it imports, into the deployed folder after the pages are built. A deployment has no
node_modules:"scripts": {"build": "tsc --noEmit && vite build && npm run build:worker","build:worker": "esbuild src/worker.ts --bundle --platform=node --format=esm --target=node22 --outfile=dist/worker.mjs","dev:worker": "tsx watch src/worker.ts"} -
Name it in
matter.json, relative todeploy.dir, and givematter extension devits dev command:"worker": "worker.mjs","dev": { "command": "npm run dev", "url": "http://localhost:5173", "worker": "npm run dev:worker" }
matter extension dev then runs the worker from your project as you change it, as your dev worker, and the deployed one keeps running alongside it. See Developing.
Every member runs it
Section titled “Every member runs it”Each member who has the extension open runs its worker, and in dev your dev worker runs beside your live one. So two workers may act on the same row. Make what each does safe to run twice: claim work with a conditional update, which only one of them wins, before acting on it. Work that should happen once for the whole extension suits an extension routine better.
"tools": "tools.mjs" is an MCP server, on stdio. The extension’s routine runs, and threads the extension starts, get it, so agents can do the extension’s work through operations you define, instead of editing its tables directly. Matter starts it for each run that uses it.
Write tools that check and move state safely, for example with conditional updates, so two runs can’t take the same work. Any MCP server library works. The tools can call whatever they like, using the member’s own settings.
A routine run works in a folder of its own, not in the extension’s files, so open files relative to your module (new URL('./prompts/x.md', import.meta.url)), not the working directory. If the member doesn’t trust the extension’s key on their computer, its routine runs there stop at once.
Run server
Section titled “Run server”"run": "server.mjs" is a local HTTP server behind the extension’s pages. With it, Matter forwards every page request, other than /_matter/*, to your server instead of serving the deployed files. Listen on PORT, on HOST (the loopback address, 127.0.0.1): nothing else on the computer, or the network, should reach it.
Matter tells your server who it’s serving, in headers it adds to each request and signs with MATTER_RUN_SECRET, a secret of this launch:
| Header | What it is |
|---|---|
x-matter-member |
The member’s id. |
x-matter-extension |
The extension’s id. |
x-matter-deployment |
The version’s id. |
x-matter-context |
The context, as base64url JSON. |
x-matter-timestamp |
When Matter forwarded it. |
x-matter-signature |
The signature over the request and these headers. |
Matter strips any x-matter-* headers that come in from outside, and its own cookie and credentials. Check every request with matter.verifyRequest, which takes a Node request or a fetch Request. It resolves to who’s asking, or null for a request Matter didn’t sign, or signed more than five minutes ago:
import { createServer } from 'node:http'import matter from '@matter.work/extension-sdk'
createServer(async (req, res) => { const who = await matter.verifyRequest(req) if (!who) return res.writeHead(403).end() res.end(`Hello ${who.context.user.name}`)}).listen(Number(process.env.PORT), process.env.HOST)A page asked for while the server starts waits up to ten seconds for it to listen.
The environment
Section titled “The environment”The worker, tools and run server get:
| Variable | What it is |
|---|---|
MATTER_API_URL, MATTER_APP_TOKEN |
Where and how to reach the data API. The SDK uses them. |
MATTER_SDK_URL |
The SDK module, as a URL (on every OS). The package imports it. |
MATTER_SDK |
The same module, as a path. |
MATTER_APP_ID, MATTER_MEMBER_ID, MATTER_WORKSPACE_ID |
The extension, the member and the workspace. |
MATTER_DEPLOYMENT_ID |
The version running, or dev in matter extension dev. |
MATTER_DATA_DIR |
A folder of the extension’s own on this computer, kept across versions. |
MATTER_APP_PROCESS |
worker, run or tools; dev-server or dev-worker in dev. |
PORT, HOST |
Where to listen (run server only). |
MATTER_RUN_SECRET |
What matter.verifyRequest checks with (run server, dev server). |
MATTER_THREAD_ID |
The thread the tools serve (tools only). |
The worker’s and run server’s working directory is the deployed folder. Treat it as read-only, and keep files you write in MATTER_DATA_DIR. In dev, MATTER_DATA_DIR is a folder of dev’s own, so your dev worker doesn’t share the live one’s data.
To run one by hand against your workspace’s data:
eval "$(matter extension env)"node dist/worker.mjsEach process’s output goes to a log Matter keeps for the extension on this computer:
matter extension logs # the end of each logmatter extension logs worker -f # the worker's, as it writesmatter extension logs run -n 200 # the run server's last 200 linesmatter extension logs dev-worker -f # your dev worker's, in matter extension devThe logs are worker, run, dev-server and dev-worker; --preview shows a preview’s. A log over 10 MB is set aside, and a new one started.
When a process stops
Section titled “When a process stops”If the worker or run server exits, Matter restarts it, waiting a little longer each time, up to a minute. After five exits in ten minutes it stops trying. A new version starts it again, as does choosing Stop in the extension’s menu and then opening the extension. Stopping a process stops whatever it started, too.