How to Integrate Custom Commands in Cypress: A Complete Guide with Code Examples

Use Cypress.Commands.add() to extend the cy object with new commands, or Cypress.Commands.overwrite() to modify existing ones—all from your cypress/support/commands.ts file.

Integrating custom commands into your Cypress test suite lets you encapsulate repetitive workflows, extend the cy API, and keep tests readable. According to the cypress-io/cypress source code, the browser-side driver handles all registration through the Cypress.Commands object defined in packages/driver/src/cypress/commands.ts. This guide walks through the internals, command types, and practical implementations.

Where Custom Commands Live in Cypress Internals

Cypress runs across three runtimes: the Node CLI, the Electron main process, and the browser-side driver. The custom command API lives exclusively in the driver (@packages/driver), ensuring your functions execute in the same sandbox as the application under test.

When the test runner initializes, the Commands factory in packages/driver/src/cypress/commands.ts builds two internal registries:

  • commands — stores definitions for chainable custom commands
  • queries — stores value-returning helpers (like cy.get, cy.find)

Calls from test files serialize to the driver, which executes your functions and handles retries, logging, and error bubbling exactly like native commands.

Adding Custom Commands with Cypress.Commands.add()

The Cypress.Commands.add(name, [options], fn) method is the primary entry point. The driver validates the name against built-ins and reserved properties, then derives the command type from the optional prevSubject flag.

Simple Parent Commands

Parent commands start a new command chain without requiring a previous subject.

// cypress/support/commands.ts
Cypress.Commands.add('login', (username: string, password: string) => {
  cy.visit('/login')
  cy.get('#user').type(username)
  cy.get('#pass').type(password)
  cy.get('form').submit()
})

Usage in tests:

cy.login('alice', 'password123')
cy.url().should('include', '/dashboard')

Child Commands with prevSubject

Child commands operate on a subject yielded by a previous command. Set prevSubject: 'element' (or 'document', 'window') in options.

Cypress.Commands.add(
  'selectOption',
  { prevSubject: 'element' },
  (subject: JQuery<HTMLElement>, option: string) => {
    cy.wrap(subject).select(option)
  }
)

Usage:

cy.get('select#colors').selectOption('red')

Dual-Type Commands with Optional Subjects

Use prevSubject: 'optional' to create commands that work both ways—independently or chained.

Cypress.Commands.add(
  'logMessage',
  { prevSubject: 'optional' },
  (subject: JQuery<HTMLElement> | void, msg: string) => {
    if (subject) {
      cy.wrap(subject).then(() => console.log(msg))
    } else {
      console.log(msg)
    }
  }
)

Both patterns work:

cy.logMessage('Standalone message')
cy.get('#some-element').logMessage('Message after element')

Overwriting Existing Cypress Commands

Modify built-in behavior with Cypress.Commands.overwrite(name, fn). The original implementation is passed as the first argument to your function.

Cypress.Commands.overwrite(
  'click',
  (originalFn, subject: JQuery<HTMLElement>, options?: Partial<Cypress.ClickOptions>) => {
    cy.log('About to click an element')
    return originalFn(subject, options).then(() => {
      cy.log('Click completed')
    })
  }
)

The wrapper pattern preserves native retry logic and logging while letting you inject pre/post processing.

Adding Query Helpers with addQuery()

Queries return values rather than chainables. They're registered via addQuery in the same commands.ts file.

Cypress.Commands.addQuery('sessionStorageItem', (key: string) => {
  return () => {
    return window.sessionStorage.getItem(key)
  }
})

Usage:

cy.sessionStorageItem('token').should('eq', 'abc123')

Unlike commands, queries don't return a Cypress chainable—they yield the value directly for assertions.

Command Type Resolution and Validation

The driver uses getTypeByPrevSubject (in packages/driver/src/cypress/commands.ts) to determine command type:

prevSubject value Resulting type Behavior
undefined (default) parent Starts new chain
'element' / 'document' / 'window' child Requires matching subject
'optional' dual Works either way
false parent Explicitly no subject

Name conflicts trigger internalError if you attempt to shadow built-ins like get, visit, or log. The PLACEHOLDER_COMMANDS set (containing .mount, .hover, etc.) allows special exceptions for commands added without triggering duplicate errors.

Complete Working Example

Here's a production-ready cypress/support/commands.ts combining all patterns:

// cypress/support/commands.ts

// Extend Cypress namespace for TypeScript
declare global {
  namespace Cypress {
    interface Chainable {
      login(username: string, password: string): Chainable<void>
      selectOption(value: string): Chainable<Element>
      logMessage(message: string): Chainable<Element | void>
      sessionStorageItem(key: string): Chainable<string | null>
    }
  }
}

// Parent command
Cypress.Commands.add('login', (username, password) => {
  cy.visit('/login')
  cy.get('[data-testid=username]').type(username)
  cy.get('[data-testid=password]').type(password)
  cy.get('[data-testid=submit]').click()
})

// Child command
Cypress.Commands.add(
  'selectOption',
  { prevSubject: 'element' },
  (subject, value) => {
    cy.wrap(subject).select(value)
    return cy.wrap(subject)
  }
)

// Dual command
Cypress.Commands.add(
  'logMessage',
  { prevSubject: 'optional' },
  (subject, message) => {
    const timestamp = new Date().toISOString()
    if (subject) {
      cy.wrap(subject).then($el => {
        console.log(`[${timestamp}] ${message}:`, $el)
      })
      return cy.wrap(subject)
    }
    console.log(`[${timestamp}] ${message}`)
  }
)

// Query helper
Cypress.Commands.addQuery('sessionStorageItem', (key) => {
  const val = window.sessionStorage.getItem(key)
  return () => val
})

Key Source Files for Reference

File Purpose
packages/driver/src/cypress/commands.ts Core implementation of add, overwrite, addQuery, overwriteQuery; contains getTypeByPrevSubject, internalError, PLACEHOLDER_COMMANDS
cli/types/cypress.d.ts TypeScript definitions including command API overloads
packages/driver/src/cypress/command_queue.ts Underlying queue mechanics for command execution and retries

Summary

  • Cypress.Commands.add() registers new commands in the driver-side registry, with type derived from prevSubject
  • prevSubject: 'element' creates child commands; 'optional' creates dual commands; omitting it creates parent commands
  • Cypress.Commands.overwrite() wraps original implementations so you can augment or replace behavior
  • addQuery() registers value-returning helpers distinct from chainable commands
  • All registration flows through packages/driver/src/cypress/commands.ts, which validates names against built-ins and reserved properties

Frequently Asked Questions

How do I add TypeScript types for custom commands?

Extend the Cypress.Chainable interface in a declaration merged namespace. Place this in cypress/support/commands.ts or a dedicated cypress/support/index.d.ts. The definitions in cli/types/cypress.d.ts show how Cypress structures these overloads for built-ins.

Can I overwrite built-in commands like cy.get or cy.visit?

Technically yes, but the driver throws via internalError for reserved names. Only commands in PLACEHOLDER_COMMANDS (like .mount, .hover) can be added without conflict. For others, use overwrite, which the driver permits for extending behavior while preserving the original reference.

What's the difference between a command and a query?

Commands return a chainable and participate in Cypress's retry- and log-system. Queries (added via addQuery) return a value directly and are stored in the separate queries registry. Use queries for simple value extraction; commands for operations that need chaining and automatic retries.

Where should I register custom commands?

Define them in cypress/support/commands.ts (or .js), which the scaffolded cypress.config.js imports by default. This file runs before specs, making additions available globally via cy.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →