@noxecane/monday-dsl
v0.2.0
Published
Type-safe Monday.com GraphQL query builder and DSL
Maintainers
Readme
monday-dsl
A type-safe Monday.com GraphQL client and query DSL. No dependency on the official Monday SDK — just a clean, chainable API for querying boards, mutating items, and handling webhooks.
Table of Contents
- Installation
- Setup
- Defining a Schema
- Extending MondayBoard
- Querying Items
- Mutating Items
- Webhook Tracking
- Board Administration
- Error Handling
- API Reference
Installation
npm install @noxecane/monday-dsl
# or
yarn add @noxecane/monday-dslSetup
Create a client with your Monday.com API URL and token, then instantiate a board:
import { MondayFetchClient, MondayBoard } from '@noxecane/monday-dsl'
const client = new MondayFetchClient(
'https://api.monday.com/v2',
process.env.MONDAY_TOKEN!
)
const board = new MondayBoard('YOUR_BOARD_ID', client, schema)Defining a Schema
A schema maps your column names to their Monday.com column IDs and types. Define it as const so TypeScript can infer return types precisely.
const VacationSchema = {
email: { id: 'email__1', type: 'email' },
status: { id: 'status__1', type: 'status' },
start_date: { id: 'date4__1', type: 'date' },
days: { id: 'numbers__1', type: 'number' },
manager: { id: 'people__1', type: 'people' },
} as const
const board = new MondayBoard('123456', client, VacationSchema)Supported Column Types
| Type | TypeScript type returned |
|------------|-----------------------------------|
| text | string |
| email | string |
| phone | string |
| number | number |
| date | string (YYYY-MM-DD) |
| time | string |
| iso | Date \| null |
| status | { label: string; index?: number }|
| dropdown | string[] |
| people | Array<{ id: string; kind: string }> |
| url | { url: string; text: string } |
| connect | string[] (item IDs) |
| asset | Array<{ name: string; url: string }> |
| tag | string[] |
| mirror | string |
Connect columns with nested schemas
When a connect column links to another board you can expand the linked items by providing a linked_schema. The parsed result will be typed objects instead of raw item IDs.
const TaskSchema = {
title: { id: 'text__1', type: 'text' },
priority: { id: 'status__1', type: 'status' },
} as const
const ProjectSchema = {
name: { id: 'name', type: 'text' },
tasks: {
id: 'connect__1',
type: 'connect',
linked_schema: TaskSchema, // expand linked items using this schema
},
} as constWhen linked_schema is present, board.query().returning({ tasks: true }) returns
Array<{ id: string; name: string; [TaskSchema keys] }> objects instead of string[].
Extending MondayBoard
The primary usage pattern is to subclass MondayBoard and add domain-specific query and mutation methods. This keeps board logic in one place and gives callers a clean, intention-revealing API.
import { MondayBoard } from '@noxecane/monday-dsl'
import { VacationSchema } from './schema'
export class VacationRequestBoard extends MondayBoard<typeof VacationSchema> {
constructor(client: MondayClient) {
super('123456', client, VacationSchema)
}
/** Return all requests in a given status. */
async getByStatus(status: string) {
return this.findMany(
q => q.equals('status', status),
{ email: true, status: true, start_date: true, days: true }
)
}
/** Approve a request by item ID. */
async approve(itemId: string) {
return this.mutation()
.update(itemId, { status: 'Approved' })
.exec()
}
}This pattern also makes board classes straightforward to mock or stub in tests.
Querying Items
All queries start with board.query() and end with a terminal method that executes the request.
Basic query
const items = await board.query()
.returning({ email: true, status: true })
// items is typed: Array<{ id: string; name: string; email: string; status: { label: string } }>Filtering
Chain filter methods before the terminal call. Multiple filters are combined with AND by default.
// Exact match (any_of with single value)
.equals('status', 'Approved')
// Match any of several values
.anyOf('status', ['Approved', 'Pending'])
// Exclude values
.notAnyOf('status', ['Rejected'])
// Text search
.contains('email', '@company.com')
// Numeric / date comparisons
.greaterThan('days', 5)
.lessThan('start_date', new Date('2025-12-31'))
.greaterThanOrEquals('days', 3)
.lessThanOrEquals('days', 10)
.between('days', 3, 10)
// Empty / not empty
.isEmpty('manager')
.isNotEmpty('manager')Filtering by group
// Items in a specific group
board.query().inGroup('group_id').returning({ email: true })
// Items across multiple groups
board.query().inGroup('group_a', 'group_b').returning({ email: true })Including group info in results
Pass { includeGroup: true } as a second argument to any terminal method. The returned items will have a group property typed as MondayGroup.
const items = await board.query()
.equals('status', 'Approved')
.returning({ email: true, status: true }, { includeGroup: true })
// items[0].group → { id, title, position, archived }This option is supported on all terminal methods: returning, first, all, page, paginate, getById, and the MondayBoard convenience methods getById, findOne, and findMany.
Ordering and limiting
const items = await board.query()
.contains('status', 'Approved')
.orderBy('start_date', 'asc')
.limit(20)
.returning({ email: true, start_date: true })Terminal methods
// All matching items (first page)
const items = await board.query().returning({ email: true })
// First matching item, or null
const item = await board.query()
.equals('email', '[email protected]')
.first({ email: true, status: true })
// Fetch by known item ID
const item = await board.getById('987654321', { email: true, status: true })
// All items across multiple pages (fetches up to maxPages, default 10)
const all = await board.query().all({ email: true }, /* maxPages */ 20)
// Single page with cursor for manual pagination
const page = await board.query().limit(50).page({ email: true })
// page.items, page.cursor, page.hasMore
// Lazy async iterator for paginated processing
for await (const page of board.query().paginate({ email: true })) {
console.log(page.items)
if (!page.hasMore) break
}Convenience methods
// findOne — applies filters via a callback, returns first match or null
const item = await board.findOne(
q => q.contains('status', 'Approved').greaterThan('days', 5),
{ email: true, status: true }
)
// findMany — applies filters, returns all matches
const items = await board.findMany(
q => q.equals('status', 'Pending'),
{ email: true, days: true }
)Mutating Items
All mutations are batched — chain multiple operations and execute them in a single API call with .exec().
Creating items
const item = await board.mutation()
.create({ name: 'Alice Smith', email: '[email protected]', status: 'Pending', days: 5 })
.exec({ id: true, name: true, email: true })Updating items
await board.mutation()
.update('987654321', { status: 'Approved', days: 7 })
.exec()Passing name alongside column values splits into a column update + rename automatically:
await board.mutation()
.update('987654321', { name: 'Alice Johnson', status: 'Approved' })
.exec()Renaming items
await board.mutation()
.rename('987654321', 'New Item Name')
.exec()Deleting items
await board.mutation()
.delete('987654321')
.exec()Batching multiple operations
const result = await board.mutation()
.create({ name: 'Item A', status: 'Pending' })
.create({ name: 'Item B', status: 'Pending' })
.update('111', { status: 'Approved' })
.delete('222')
.exec({ id: true, name: true })
// Returns the result of the last operationRaw column updates
For columns not in your schema (e.g. dynamic timeline slots), use updateRaw with Monday.com column IDs directly:
await board.mutation()
.updateRaw('987654321', {
'timeline_2026_0': { from: '2026-01-01', to: '2026-01-05' }
})
.exec()Webhook Tracking
BoardTracker is an abstract base class for receiving and routing Monday.com webhook events. Extend it and decorate handler methods.
Defining a tracker
import { BoardTracker, onCreate, onColumnChange, onStatusChange } from '@noxecane/monday-dsl'
import { VacationSchema } from './schema'
export class VacationTracker extends BoardTracker<typeof VacationSchema> {
// Fires when a new item is created
@onCreate()
async handleNewRequest(item: Record<string, any>, event: CreateItemPayload) {
console.log('New request from', item.email)
}
// Fires when the email column changes
@onColumnChange('email')
async handleEmailChange(newValue: any, oldValue: any, event: UpdateColumnValuePayload) {
console.log('Email changed from', oldValue, 'to', newValue)
}
// Fires when status changes to 'Approved'
@onStatusChange('status', { to: 'Approved' })
async handleApproval(newLabel: string, oldLabel: string, event: UpdateColumnValuePayload) {
console.log('Request approved, was:', oldLabel)
}
// Fires on any transition between specific statuses
@onStatusChange('status', { from: 'Pending', to: ['Approved', 'Rejected'] })
async handleDecision(newLabel: string, oldLabel: string, event: UpdateColumnValuePayload) {
// ...
}
}Wiring up the webhook endpoint
const tracker = new VacationTracker(client, VacationSchema)
// Express example
app.post('/webhooks/vacation', async (req, res) => {
const response = await tracker.handleWebhook(req.body)
// Monday.com sends a challenge on first subscription — return it
res.json(response ?? { ok: true })
})Decorator options
| Decorator | Arguments | Description |
|---|---|---|
| @onCreate() | — | Fires on create_pulse events |
| @onColumnChange(column, options?) | column: schema keyoptions.ignoreEmpty: skip if new value is empty | Fires on any column value change |
| @onStatusChange(column, options?) | column: schema keyoptions.to: label(s) to matchoptions.from: label(s) to match | Fires on status transitions |
Board Administration
BoardAdmin handles infrastructure-level operations: fetching board and folder metadata, managing webhooks, columns, users, and groups.
import { BoardAdmin } from '@noxecane/monday-dsl'
const admin = new BoardAdmin(client)Board metadata
Query any subset of board fields. The return type is inferred from the selection.
const info = await admin.board('123456').returning({
id: true,
name: true,
state: true,
board_kind: true,
items_count: true,
created_at: true,
})
// → Pick<Board, 'id' | 'name' | 'state' | 'board_kind' | 'items_count' | 'created_at'>Available fields: id, name, description, state, board_kind, items_count, created_at, updated_at, board_folder_id.
Folder metadata
const folder = await admin.folder('folder-id').returning({
id: true,
name: true,
parent: true, // always returns { id, name }
sub_folders: true, // always returns [{ id, name }]
children: { id: true, name: true, created_at: true },
})parent and sub_folders always return { id, name }. To query a sub-folder in depth, pass its id to a new admin.folder() call. children are boards — any subset of board fields can be selected.
Webhooks
// Query webhooks with field selection
const webhooks = await admin.webhooks('123456').returning({
id: true,
event: true,
config: true, // returns { column?, labelIndex?, groupId? }
})
// Register a webhook
await admin.createWebhook('123456', 'https://your-app.com/webhook', {
event: 'change_specific_column_value',
config: { columnId: 'status__1' }
})
// Delete
await admin.deleteWebhook('webhook-id')
await admin.deleteWebhooks(['id-1', 'id-2', 'id-3'])Columns
const columns = await admin.getColumns('123456')
const settings = await admin.getColumnSettings('123456')
await admin.createColumn('123456', 'approval__1', 'Approval Status', {
type: 'status',
labels: ['Pending', 'Approved', 'Rejected']
})Users and groups
const users = await admin.getAllUsers(100, 1)
const groups = await admin.getGroups('123456')
// groups → [{ id, title, position, archived }]Error Handling
All errors extend MondayError and carry a code string for programmatic handling.
import {
MondayError,
AuthenticationError,
RateLimitError,
MondayApiError,
NetworkError,
QuerySyntaxError,
ValidationError,
ParseError
} from '@noxecane/monday-dsl'
try {
await board.query().returning({ email: true })
} catch (err) {
if (err instanceof AuthenticationError) {
// Invalid or expired token
} else if (err instanceof RateLimitError) {
console.log('Retry after', err.retryAfter, 'seconds')
} else if (err instanceof MondayApiError) {
console.log('API error code:', err.errorCode)
} else if (err instanceof NetworkError) {
console.log('HTTP status:', err.statusCode)
} else if (err instanceof MondayError) {
console.log('Library error:', err.code, err.details)
}
}| Class | code | When thrown |
|---|---|---|
| AuthenticationError | AUTHENTICATION_ERROR | 401/403 HTTP responses |
| RateLimitError | RATE_LIMIT_ERROR | 429 HTTP responses |
| MondayApiError | MONDAY_API_ERROR | Monday.com API-level errors |
| NetworkError | NETWORK_ERROR | Connection failures, 5xx responses |
| QuerySyntaxError | QUERY_SYNTAX_ERROR | GraphQL syntax errors |
| ValidationError | VALIDATION_ERROR | Invalid mutation inputs |
| ParseError | PARSE_ERROR | Malformed API responses |
API Reference
MondayFetchClient
new MondayFetchClient(url: string, token: string)| Method | Signature | Description |
|---|---|---|
| run | run<T>(query: string, mode?: 'debug' \| 'dry'): Promise<T> | Execute a GraphQL query. debug logs the query; dry logs and skips execution. |
| upload | upload<T>(item: string, columnId: string, upload: MondayUpload): Promise<T> | Upload a file to an item column. |
MondayBoard<TSchema>
new MondayBoard(boardId: string, client: MondayClient, schema: TSchema)| Method | Returns | Description |
|---|---|---|
| query() | BoardQuery<TSchema> | Start a chainable query builder |
| mutation() | BoardMutation<TSchema> | Start a chainable mutation builder |
| getById(itemId, selection, options?) | Promise<Item \| null> | Fetch a single item by ID |
| findOne(finder, selection, options?) | Promise<Item \| null> | Apply filters, return first match |
| findMany(finder, selection, options?) | Promise<Item[]> | Apply filters, return all matches |
options accepts { includeGroup: true } to include group: MondayGroup on each returned item.
BoardQuery<TSchema>
All filter methods return this for chaining. Terminal methods execute the request.
Filters
| Method | Description |
|---|---|
| .anyOf(column, values) | Column value is in the array |
| .notAnyOf(column, values) | Column value is not in the array |
| .contains(column, value) | Column contains the search term |
| .equals(column, value) | Exact match |
| .greaterThan(column, value) | > — numbers or dates |
| .greaterThanOrEquals(column, value) | >= — numbers or dates |
| .lessThan(column, value) | < — numbers or dates |
| .lessThanOrEquals(column, value) | <= — numbers or dates |
| .between(column, start, end) | Inclusive range |
| .isEmpty(column) | Column has no value |
| .isNotEmpty(column) | Column has a value |
Options
| Method | Description |
|---|---|
| .limit(n) | Return at most n items |
| .cursor(cursor) | Start from a pagination cursor |
| .orderBy(column, direction?) | Order results ('asc' or 'desc', default 'desc') |
| .inGroup(...groupIds) | Restrict to one or more groups by ID |
Terminal methods
All terminal methods accept an optional options argument. Pass { includeGroup: true } to include group: MondayGroup on each item.
| Method | Returns | Description |
|---|---|---|
| .returning(selection, options?) | Promise<Item[]> | Fetch one page of results |
| .first(selection, options?) | Promise<Item \| null> | Fetch first match |
| .all(selection, maxPages?, options?) | Promise<Item[]> | Fetch all pages (default max: 10) |
| .page(selection, options?) | Promise<PageResult<Item>> | Fetch one page with cursor metadata |
| .paginate(selection, maxPages?, options?) | AsyncGenerator<PageResult<Item>> | Lazy page-by-page iteration |
| .getById(itemId, selection, options?) | Promise<Item \| null> | Fetch by item ID |
BoardMutation<TSchema>
All mutation methods return this for chaining.
| Method | Description |
|---|---|
| .create(input) | Queue a create operation |
| .update(itemId, input) | Queue an update (and optional rename) |
| .updateRaw(itemId, columnValues) | Queue an update with raw Monday.com column IDs |
| .rename(itemId, name) | Queue a rename |
| .delete(itemId) | Queue a delete |
| .exec(selection?) | Execute all queued operations, return last result |
BoardTracker<TSchema>
abstract class BoardTracker<TSchema extends BoardSchema>
constructor(client: MondayClient, schema: TSchema)| Method | Description |
|---|---|
| handleWebhook(payload) | Route an incoming webhook payload to decorated handlers |
Protected properties (available in subclasses)
| Property | Type | Description |
|---|---|---|
| this.client | MondayClient | The underlying HTTP client |
| this.admin | BoardAdmin | Administrative operations on the board |
| this.schema | TSchema | The board schema passed to the constructor |
Trackers are responsible for event routing, not data access. Prefer delegating queries and mutations to a
MondayBoardsubclass rather than callingthis.clientorthis.admindirectly inside handlers.
Decorators
| Decorator | Handler signature | Description |
|---|---|---|
| @onCreate() | (item: Record<string, any>, event: CreateItemPayload) => Promise<void> | Item creation |
| @onColumnChange(column, options?) | (newValue: any, oldValue: any, event: UpdateColumnValuePayload) => Promise<void> | Column value changed |
| @onStatusChange(column, options?) | (newLabel: string, oldLabel: string, event: UpdateColumnValuePayload) => Promise<void> | Status transitioned |
BoardAdmin
new BoardAdmin(client: MondayClient)| Method | Returns | Description |
|---|---|---|
| board(boardId) | BoardQueryBuilder | Board metadata with field selection |
| folder(folderId) | FolderQueryBuilder | Folder metadata with field selection |
| webhooks(boardId) | WebhookQueryBuilder | Webhooks with field selection |
| createWebhook(boardId, url, eventConfig) | Promise<{ id, board_id }> | Register a webhook |
| deleteWebhook(webhookId) | Promise<string> | Delete one webhook |
| deleteWebhooks(webhookIds) | Promise<string[]> | Delete multiple webhooks |
| getGroups(boardId) | Promise<Group[]> | All groups — id, title, position, archived |
| getAllUsers(limit?, page?) | Promise<User[]> | Non-guest users |
| getColumns(boardId) | Promise<Column[]> | All columns |
| getColumnSettings(boardId) | Promise<Record<string, any>> | Column label/settings data |
| createColumn(boardId, id, title, config) | Promise<Column> | Create a column |
| withTemporaryItem(boardId, operation) | Promise<T> | Create a temp item, run operation, auto-delete |
