npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@noxecane/monday-dsl

v0.2.0

Published

Type-safe Monday.com GraphQL query builder and DSL

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

npm install @noxecane/monday-dsl
# or
yarn add @noxecane/monday-dsl

Setup

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 const

When 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 operation

Raw 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 MondayBoard subclass rather than calling this.client or this.admin directly 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 |