Extension reference
Source formats, triggers, capabilities, records, webhooks, and daemons.
An extension is a TypeScript module that you commit under .garage/. An
extension can do these operations:
- respond to a push or an event
- expose a callable tool
- send a webhook
- store scoped data
- run on a schedule
The authoring commands and APIs are experimental. Local authoring requires Node.js 24 or later.
garage ext new <name> writes .garage/extensions/<name>.ts. It also writes a
package.json and a tsconfig.json. These two files give your editor the types
from @thegarage/ext. Pass --force to overwrite an existing source file.
See Build and run your first extension for a worked example and Build and promote extensions for the activation procedure on its own.
Extension types
| Source path | Authoring function | Use |
|---|---|---|
.garage/extensions/<name>.ts |
defineExtension |
Push, event, or explicit dispatch runs |
.garage/tools/<name>.ts |
defineTool |
Typed calls from the CLI, HTTP API, or MCP |
.garage/webhooks/<name>.ts |
defineWebhook |
Signed outbound HTTP delivery |
.garage/daemons/<name>.ts |
defineDaemon |
Scheduled or event-driven background work |
All source types use @thegarage/ext for types and authoring helpers.
Extensions
defineExtension() accepts:
| Field | Required | Description |
|---|---|---|
description |
No | One-line summary |
input |
For dispatch | Zod input schema |
output |
For dispatch | Zod output schema |
dispatch |
No | Expose the extension for explicit calls |
on |
No | Push and repository-event triggers |
runtime |
No | Execution runtime; defaults to worker |
capabilities |
No | Host APIs requested by the extension |
record |
No | Record schema owned by the extension |
run(ctx, input) |
Yes | Extension entry point |
onRecordCreate |
No | Record creation handler |
onRecordTransition |
No | Record transition handler |
import { defineExtension, z } from '@thegarage/ext'
export default defineExtension({
description: 'Return a greeting.',
dispatch: true,
capabilities: [],
input: z.object({ name: z.string().min(1) }),
output: z.object({ message: z.string() }),
async run(ctx, input) {
const message = `Hello, ${input.name}`
await ctx.log(message)
return { message }
},
})
@thegarage/ext re-exports z.
Triggers
An extension can use a push trigger, a repository-event trigger, explicit dispatch, or a combination of these.
export default defineExtension({
on: {
push: { branches: ['main'] },
event: { types: ['release.*'] },
},
dispatch: true,
// input, output, capabilities, and run
})
| Field | Description |
|---|---|
on.push.branches |
Short branch names or full refs |
on.event.types |
Event-type globs |
on.event.subject |
Exact subject filter |
dispatch |
Exposes the extension through garage runs dispatch |
Emit an event with:
await ctx.events.emit({
type: 'release.published',
subject: 'v1.2.0',
payload: { commit: ctx.sha },
})
The extension must request the events capability.
Tools
defineTool() creates a directly callable extension. It requires an input
schema and an output schema. It also enables dispatch automatically.
import { defineTool, z } from '@thegarage/ext'
export default defineTool({
description: 'Read a committed file.',
capabilities: ['repoRead'],
input: z.object({ path: z.string().min(1) }),
output: z.object({ content: z.string() }),
async run(ctx, input) {
return ctx.git.read({ path: input.path, ref: ctx.ref })
},
})
Run promoted tools with:
garage tools list
garage tools run read-file --input '{"path":"README.md"}'
The repository MCP endpoint also publishes a promoted tool.
Capabilities and context
An extension declares each host API it uses.
| Capability | Context methods |
|---|---|
| none | ctx.log |
repoRead |
ctx.git.read, tree, blob, commits, commit, diff, compare, refs |
repoWrite |
ctx.git.write |
kvRead / kvWrite |
ctx.kv.get, list, put, delete |
recordsRead / recordsWrite |
ctx.records.get, list, create, update, delete |
events |
ctx.events.emit |
network |
ctx.net.fetch |
List capabilities in the extension source:
capabilities: ['repoRead', 'repoWrite']
garage isolates scoped KV and records by repository and by extension.
Write repository files
ctx.git.write() commits all files and removals in one operation:
const result = await ctx.git.write({
files: [{ path: 'generated.txt', content: 'generated\n' }],
message: 'Update generated files',
ref: ctx.ref,
})
await ctx.log(`Created ${result.commit.oid}`)
At least one file or removal is required.
Dependencies
Declare runtime packages in .garage/package.json:
{
"dependencies": {
"semver": "^7.8.0"
}
}
This file is not the package.json that garage ext new writes at the
repository root. The root file carries @thegarage/ext as a devDependency for
the editor types, and the build never reads it. .garage/package.json is the
runtime manifest.
The manifest accepts a dependencies object and an optional private: true.
The manifest does not support these items:
- scripts
- development dependencies
- peer dependencies
- aliases
- dist-tags
- Git, HTTP, or file specifiers
garage supplies @thegarage/ext and zod. Do not declare them in the manifest.
Each build resolves the package ranges again. A package can fail to build for one of these reasons:
- it needs a native addon
- it needs an install script
- it needs a Node.js API that is not available
- its dependency versions conflict
Records
An extension or tool can own one record type declared with defineRecord():
import { defineExtension, defineRecord, z } from '@thegarage/ext'
export default defineExtension({
dispatch: true,
capabilities: ['recordsWrite'],
record: defineRecord({
name: 'issue',
fields: {
title: { kind: 'string', required: true },
status: {
kind: 'string',
enum: ['open', 'closed'],
default: 'open',
},
},
indexes: ['status'],
}),
input: z.object({ title: z.string().min(1) }),
output: z.object({ id: z.string() }),
async run(ctx, input) {
const issue = await ctx.records.create({ title: input.title })
return { id: issue.id }
},
})
A record field supports string, integer, number, boolean, and json. A
filter in records.list({ where }) must use an indexed field. An update and a
delete both require expectedRevision.
onRecordCreate and onRecordTransition respond to a record change. Promotion
applies a supported additive change to the schema. Promotion rejects an unsafe
or ambiguous change.
Webhooks
defineWebhook() declares an outbound webhook:
import { defineWebhook } from '@thegarage/ext'
export default defineWebhook({
url: 'https://example.com/hooks/garage',
on: { push: { branches: ['main'] } },
})
garage signs deliveries with X-Garage-Signature-256.
| Command | Description |
|---|---|
garage webhooks secret <webhook> |
Retrieve the verification secret |
garage webhooks deliveries |
List delivery attempts |
garage webhooks redeliver <id> |
Retry a delivery |
garage webhooks enable <webhook> |
Enable delivery |
garage webhooks disable <webhook> |
Disable delivery |
Daemons
defineDaemon() declares scheduled work:
import { defineDaemon } from '@thegarage/ext'
export default defineDaemon({
schedule: { intervalMinutes: 5 },
capabilities: ['kvWrite'],
async run(ctx) {
await ctx.kv.put('last-run', new Date().toISOString())
},
})
The interval must be an integer of five minutes or more. An on.event filter
and onEvent(ctx, event) can also wake a daemon for a matching repository
event. garage runs one invocation of a daemon at a time. If a daemon misses one
or more scheduled intervals, garage starts one run. It does not start one run
for each missed interval.