Resource library

QA How-To

How to Use Cypress custom commands (2026)

Learn cypress custom commands in 2026 with TypeScript, parent and child APIs, queries, cy.session login, safe logging, retryability, testing, and CI use.

23 min read | 3,013 words

TL;DR

Use Cypress custom commands for narrow, reused domain operations that need the Cypress queue. Type their inputs and yielded subjects, preserve retry behavior, make side effects visible, cache authentication safely, and keep the global command catalog deliberately small.

Key Takeaways

  • Add a global command only for a stable reused domain operation that benefits from the Cypress queue.
  • Use plain TypeScript functions for pure builders and custom queries for synchronous retryable DOM lookup.
  • Declare `prevSubject` for child and dual commands and return the chain or subject callers expect.
  • Cache approved test authentication with `cy.session` and include all identity dimensions in the session key.
  • Keep secrets out of command logs, screenshots, videos, URLs, and request evidence.
  • Avoid global overwrites that force actions or suppress failures, and test every justified overwrite directly.
  • Treat the command catalog as an owned internal API with documentation, deprecation, and upgrade checks.

Cypress custom commands let a test suite add domain-specific operations to the cy chain. They are valuable when they hide a stable implementation boundary, enforce one secure setup path, or express a repeated business action more clearly than raw browser steps.

They are not a license to turn every repeated line into a global command. A poorly designed command hides assertions, breaks retry behavior, creates shared state, and makes a test impossible to understand without opening the support folder. Good commands have a narrow contract, typed inputs, a useful return subject, and predictable logging.

This 2026 guide uses public Cypress APIs and TypeScript. It explains parent, child, and dual commands, custom queries, authentication with cy.session, safe overwrites, error handling, testing, and migration. All examples state their application assumptions rather than inventing Cypress methods.

TL;DR

Need Best choice Why
Repeated domain setup Parent custom command Starts a clear Cypress chain
Operation on current subject Child command Preserves fluent subject context
Optional subject behavior Dual command Supports parent and child invocation
Retryable DOM lookup Built-in query or custom query Preserves query retry semantics
Pure data transformation Plain TypeScript function No command queue is needed
One-file workflow helper Local function or page fragment Avoids unnecessary global API

1. What Cypress Custom Commands Are

A custom command extends Cypress.Commands and becomes available on the Cypress chain. The callback is queued and executed with other Cypress commands. It does not behave like an ordinary synchronous function, even if its body looks similar.

A parent command begins from cy, such as cy.loginAs('editor'). A child command requires a previous subject, such as cy.get('[data-cy=row]').findAction('Delete'). A dual command accepts an optional subject and can be invoked either way. Cypress validates the subject when you declare prevSubject as element, window, document, or another supported type.

Use a custom command when the abstraction is stable across specs and carries domain meaning. Authentication through a test-only API, creating an order through a supported service, or selecting a complex widget value can qualify. A three-line helper used by one spec often belongs as a local function.

The Cypress command queue matters. Returning a chainable lets the caller continue with its yielded subject. Returning an arbitrary value from inside .then() yields that value at that point, but reading it into a normal variable before the command runs will fail. Write commands and call sites as chains.

Commands are globally visible once the support file imports their registration. That convenience is also a cost: name collisions, hidden dependencies, and large autocomplete surfaces. Keep the catalog intentional and document each public contract.

2. Decide Between a Command, Query, and Plain Function

Before adding Cypress.Commands.add, decide what behavior you need.

Abstraction Use it for Retry behavior Example
Custom command Actions, setup, network operations, domain workflows Queued once, inner Cypress queries retry normally cy.createCustomer()
Custom query Synchronous DOM lookup that should retry from the top Whole query function is retried cy.getByDataCy()
Plain function Pure values, object builders, formatting Normal JavaScript execution buildOrder()
Local helper Spec-specific sequence with no global value Depends on returned Cypress chain openCurrentInvoice()
Component or page object Cohesive UI vocabulary Depends on contained queries checkout.submit()

Do not wrap cy.get() in a custom command merely to shorten two characters. A selector helper can create a stable test-contract vocabulary, but a retryable lookup is a query concern. Cypress provides Cypress.Commands.addQuery() for custom queries. Many suites need only built-in cy.get() or an installed Testing Library integration.

Pure data belongs outside Cypress. A builder that returns { sku, quantity } should be a TypeScript function, so it is fast, unit-testable, and usable by API or component code. Putting it on cy adds queue semantics for no benefit.

Ask three questions: Does the helper represent a stable domain operation? Is it reused across feature ownership boundaries? Does placing it on the global chain make the test easier to read without hiding relevant state? If any answer is no, keep it local.

For broader design guidance, see Cypress test automation framework design.

3. Register and Type a Parent Custom Command

Keep command registration in a support module and import it from the relevant Cypress support entry point. A command that creates test data through an API can return the created resource, allowing the test to chain assertions and cleanup.

// cypress/support/commands.ts
export type Project = {
  id: string
  name: string
  status: 'active' | 'archived'
}

Cypress.Commands.add(
  'createProject',
  (name: string): Cypress.Chainable<Project> => {
    return cy.request<Project>({
      method: 'POST',
      url: '/api/test/projects',
      body: { name },
      failOnStatusCode: false,
    }).then((response) => {
      expect(response.status, 'project creation status').to.equal(201)
      return response.body
    })
  },
)

Augment the Cypress type in a declaration file included by the test TypeScript configuration:

// cypress/support/index.d.ts
import type { Project } from './commands'

declare global {
  namespace Cypress {
    interface Chainable {
      createProject(name: string): Chainable<Project>
    }
  }
}

export {}

Import registration once:

// cypress/support/e2e.ts
import './commands'

The call site remains a chain:

it('opens a newly created project', () => {
  const name = `release-${Date.now()}`

  cy.createProject(name).then((project) => {
    cy.visit(`/projects/${project.id}`)
  })

  cy.contains('h1', name).should('be.visible')
})

The /api/test/projects endpoint is an application-specific test contract, not a Cypress feature. It should be authenticated, restricted to non-production environments, validated server-side, and owned like any other API.

4. Build Child and Dual Commands With Subject Contracts

A child command receives the previous subject. Declare prevSubject so Cypress validates the chain and TypeScript communicates the intended use. Return a Cypress chain when the caller should continue from a new subject.

This child command finds an action button within one row-like element:

Cypress.Commands.add(
  'findAction',
  { prevSubject: 'element' },
  (subject, actionName: string) => {
    return cy.wrap(subject).contains('button', actionName)
  },
)
declare global {
  namespace Cypress {
    interface Chainable<Subject = any> {
      findAction(actionName: string): Chainable<JQuery<HTMLButtonElement>>
    }
  }
}

A caller can write:

cy.contains('[data-cy="project-row"]', 'Apollo')
  .findAction('Archive')
  .should('be.enabled')
  .click()

A dual command uses { prevSubject: 'optional' }. This can be useful for a command that takes a screenshot of either the current subject or the whole page, but dual APIs can be confusing. Use one only when both forms share a clear contract, not to save a second name.

Subject validation catches misuse early. If a child command requires an element and the chain yields a primitive, Cypress reports the mismatch. Do not cast an unknown subject until its runtime shape is established.

Be careful with .contains() behavior. It finds elements containing text and may prefer certain element types. If exact structure matters, narrow the query first and assert the yielded element. A child action should not silently click, assert, and navigate unless that whole behavior is truly one domain operation.

5. Preserve Retryability With Cypress Custom Commands

Cypress commands are queued. Queries such as cy.get(), .find(), and .contains() can retry until their assertions pass, but an arbitrary side-effecting custom command is not automatically replayed from the beginning. Retrying actions could create duplicate orders, send repeated messages, or mask a race.

If you need a reusable retryable DOM lookup, prefer a built-in query, a supported Testing Library query, or Cypress.Commands.addQuery(). A minimal data-attribute query can be written with the documented query API:

declare global {
  namespace Cypress {
    interface Chainable {
      getByDataCy(value: string): Chainable<JQuery<HTMLElement>>
    }
  }
}

Cypress.Commands.addQuery('getByDataCy', function (value: string) {
  const selector = `[data-cy="${CSS.escape(value)}"]`
  return () => Cypress.$(selector)
})

export {}

However, custom query internals can be less familiar to a team and may change with framework expectations. The simplest maintainable option is often a normal helper that constructs the selector while the spec calls cy.get(), or direct cy.get('[data-cy="save"]'). Confirm the current Cypress custom-query documentation before adopting low-level examples across a large codebase.

Never implement polling with a synchronous while loop. It blocks the browser and does not let application state advance. Use Cypress's retryable assertions for DOM state. For asynchronous backend operations, poll a supported API with a bounded recursive chain, explicit interval, terminal-failure handling, and diagnostic context.

Do not put assertions deep inside a generic lookup command. A command named getToast should yield a toast, while the test states whether it should contain success or error text. An action command may assert a prerequisite that defines safe use, such as verifying a test-data API returned 201.

Read Cypress retryability and waits for the command-chain model in detail.

6. Implement Authentication With cy.session and cy.request

Login is a common custom-command use case because UI login repeated before every test is slow and unrelated to most feature behavior. Use an approved test authentication path, establish the same browser credential the application uses, and cache it with cy.session().

type Role = 'viewer' | 'editor' | 'admin'

Cypress.Commands.add('loginAs', (role: Role) => {
  return cy.session(
    ['loginAs', role],
    () => {
      cy.request<{ sessionId: string }>({
        method: 'POST',
        url: '/api/test/login',
        body: { role },
      }).then(({ body }) => {
        cy.setCookie('session_id', body.sessionId, {
          sameSite: 'lax',
          secure: true,
        })
      })
    },
    {
      validate() {
        cy.request('/api/me').its('status').should('eq', 200)
      },
    },
  )
})
declare global {
  namespace Cypress {
    interface Chainable {
      loginAs(role: 'viewer' | 'editor' | 'admin'): Chainable<null>
    }
  }
}

The server endpoints and cookie name are application contracts. Replace them with your approved non-production mechanism. Do not create a test backdoor in production, hard-code real credentials, or log returned tokens. If the application uses cross-origin identity, follow Cypress's current origin and session guidance rather than disabling browser security.

The session ID includes the role, preventing an editor session from being restored for a viewer test. Validation confirms that restored browser state still authenticates. It should be fast and meaningful. If role permissions can change during the suite, include the relevant identity or fixture version in the session key.

Keep login separate from navigation. A loginAs command establishes identity, and the test visits its feature route. Combining login, feature flags, data creation, and visit into one command hides important preconditions and reduces reuse.

7. Design Logging, Errors, and Sensitive Data Safely

Custom commands appear in the Cypress command log by default. Good log messages help debugging, but parameters can contain passwords, tokens, personal data, or large payloads. Never log a raw credential merely because Cypress makes logging convenient.

For sensitive input commands, pass { log: false } to the underlying command and create only a redacted message when needed. For cy.type, Cypress supports the log option:

declare global {
  namespace Cypress {
    interface Chainable<Subject = any> {
      typeSecret(value: string): Chainable<JQuery<HTMLElement>>
    }
  }
}

Cypress.Commands.add('typeSecret', { prevSubject: 'element' }, (subject, value: string) => {
  Cypress.log({
    name: 'typeSecret',
    message: '[redacted]',
  })

  return cy.wrap(subject, { log: false }).type(value, { log: false })
})

export {}

The declaration should require a string and yield the element chain. Use this only where obscuring command output is part of a broader secret-handling policy. Screenshots, videos, application logs, network captures, and browser autofill can still expose a value.

Fail with context, but keep context safe. expect(response.status, 'create order status').to.equal(201) is more useful than a generic error. Include resource type, safe synthetic identifier, endpoint label, and observed status. Do not include authorization headers or full customer records.

Avoid broad try/catch blocks around Cypress chains. Commands execute later, so synchronous error handling does not behave like a normal await flow. Use Cypress assertions and documented failure hooks sparingly. A global uncaught-exception handler that always returns false can hide product crashes and should not become a default.

If a command modifies server state, make idempotency and cleanup explicit. Return the created resource identifier. Let the test or fixture register narrowly targeted deletion. Never run global deletion to make a command convenient.

8. Overwrite Built-In Commands Only With a Strong Reason

Cypress.Commands.overwrite() can replace behavior of an existing command while receiving the original function. This is powerful and globally risky. A hidden overwrite can surprise every contributor and make documentation examples behave differently in your repository.

A defensible use is enforcing a safe environment guard before navigation. Even then, configuration validation in the test startup path is often clearer. If you overwrite, preserve the original arguments and return value:

Cypress.Commands.overwrite('visit', (originalFn, url, options) => {
  const baseUrl = Cypress.config('baseUrl')

  if (typeof baseUrl === 'string' && baseUrl.includes('production.example.com')) {
    throw new Error('Cypress execution against production is blocked')
  }

  return originalFn(url, options)
})

This is illustrative. Replace the hostname with an organization-owned rule and cover all production aliases. A better safety system also blocks destructive test credentials server-side and restricts test endpoints by environment and network.

Never overwrite click to force every action or overwrite request to accept broad failures. Those changes hide real defects and invalidate assumptions across the suite. If a special interaction is required for one widget, create a clearly named domain helper and document why.

Record every overwrite in contributor documentation and add a focused test for it. Review Cypress release notes during upgrades because built-in signatures and behavior are part of your compatibility surface. Keep the number of overwrites close to zero.

9. Test and Review Cypress Custom Commands

Custom command code deserves focused verification. A small spec can exercise success, invalid input, server failure, returned subject, and sensitive logging behavior against a controlled page or API. Do not rely only on dozens of feature specs to indirectly cover a foundational command.

For an API setup command, intercepting cy.request() is not generally the same as intercepting browser traffic, because cy.request() is issued outside the browser network layer. Use a safe local service, a purpose-built test endpoint, or test the pure request-builder logic separately. Do not claim cy.intercept() proves a cy.request() call.

Review commands with this checklist:

  • Does the name describe a domain operation or precise subject action?
  • Are inputs typed and validated at the correct boundary?
  • Does it return the subject the caller expects?
  • Are important assertions visible in the test?
  • Can the operation create duplicate or shared state?
  • Does it preserve Cypress retry behavior?
  • Are logs and artifacts safe?
  • Is it imported once in the correct support file?
  • Could a plain function or local helper be clearer?

Run TypeScript checking, linting, the focused command spec, and representative end-to-end or component specs. Test in the CI browser and configuration used by the suite. A command that works only after another spec has populated state is defective.

Document an example call, yielded subject, side effects, prerequisites, and cleanup. This is especially important for commands used by multiple teams.

10. Scale Cypress Custom Commands Without Creating a Junk Drawer

Organize by responsibility, not by an endlessly growing commands.ts. Registration can live in modules such as auth.commands.ts, projects.commands.ts, and widgets.commands.ts, with one support entry importing them. Avoid registering the same name in multiple files or conditionally registering commands based on spec order.

Treat the command catalog as a public API. Names should use domain vocabulary, deprecated commands should point to a replacement, and breaking changes should be reviewed with consumers. Delete commands that no longer protect a real boundary.

Do not create one command per page-object method. A test that reads cy.openPage().fillName().clickSave().verifyToast() can hide behavior and produce a chain that no one can debug. Use commands for globally valuable setup or interaction boundaries, and keep feature flow visible in the spec.

During migration, add the new command, move a representative consumer, compare diagnostic quality, then migrate remaining callers. Do not change every spec mechanically before proving the contract. If a command bundles several responsibilities, split callers by behavior rather than recreating the same bundle with a new name.

Measure impact through readability, first-attempt reliability, setup time, duplicated maintenance, and diagnosis time. A smaller command catalog can be healthier than a large one. Cypress best practices for maintainable tests provides complementary suite-level guidance.

Keep commands version-agnostic where possible and review official documentation during Cypress upgrades. Type declarations are a valuable alarm: a compiler failure can reveal that a custom signature no longer matches the Cypress chain.

Create a lightweight ownership table in contributor documentation. For each public command, record the module, intended consumers, yielded subject, server dependency, and cleanup rule. This turns global helpers into reviewable contracts. It also makes deletion safer because maintainers can identify consumers and replacement evidence before removing registration.

When two teams need similar commands, compare the underlying domain operation before merging them. Superficially similar creation flows may require different tenants, permissions, or data retention. A single command with many boolean flags is often less clear than two narrow operations. Prefer typed option objects when variation is legitimate, and reject combinations the application cannot support.

Interview Questions and Answers

Q1: What is a Cypress custom command?

It is an operation registered through Cypress.Commands and exposed on the Cypress chain. I use it for stable, reused domain actions or setup boundaries. It participates in the Cypress queue, so it is not an ordinary synchronous helper.

Q2: What is the difference between parent and child commands?

A parent command starts from cy and does not require a previous subject. A child command declares prevSubject and receives the subject yielded by the prior chain. A dual command makes that subject optional.

Q3: When should you use a plain function instead?

Use a plain function for pure transformations, builders, selector strings, or local logic that does not need the Cypress queue. It is easier to unit-test and does not expand the global command API.

Q4: Do custom commands retry automatically?

The built-in queries and assertions inside a command retain their normal retry behavior. The whole custom action is not safely replayed by default. For a reusable retryable DOM lookup, use a built-in query or a properly implemented custom query.

Q5: Why return a Cypress chain from a command?

Returning the chain preserves queue ordering and yields a predictable subject to the caller. It allows the test to continue with .then() or assertions. Failing to return can make subject behavior confusing.

Q6: How would you create a login command?

I use an approved non-production authentication API, establish the browser credential, and cache it with cy.session(). The session key includes identity dimensions such as role, and validation confirms restored state. Secrets stay out of logs.

Q7: Why are command overwrites risky?

They alter familiar built-in behavior globally. A force-click overwrite or broad failure suppression can hide defects across every spec. I overwrite only for a documented cross-cutting policy, preserve the original signature, and test it directly.

Q8: How do you type custom commands?

I augment the global Cypress.Chainable interface in a TypeScript declaration included by the test project. The signature describes inputs and yielded subject. Registration and declaration must stay synchronized.

Common Mistakes

  • Adding every repeated line to the global cy namespace.
  • Using a command for a pure object builder.
  • Hiding a complete business scenario and all assertions in one command.
  • Forgetting to return the Cypress chain or intended yielded value.
  • Assuming the whole custom action automatically retries.
  • Registering the same command from several support imports.
  • Sharing mutable users or records inside a convenient setup command.
  • Caching sessions without identity dimensions or validation.
  • Logging passwords, tokens, or personal data.
  • Using global exception suppression to make commands appear stable.
  • Overwriting click to force every interaction.
  • Treating cy.intercept() as proof of requests issued by cy.request().
  • Leaving declarations out of the Cypress TypeScript project.
  • Building a command catalog with no ownership or deprecation path.

Conclusion

Cypress custom commands are most effective as a small, typed domain API over stable testing boundaries. Choose a command only when the Cypress queue and global reuse add real value. Preserve the yielded subject, make side effects explicit, protect retry semantics, validate sessions, and keep sensitive data out of every log and artifact.

Start by auditing your current command file. Move pure builders to ordinary TypeScript, split hidden workflows, add types and focused tests to the commands that remain, and delete wrappers that only rename built-ins. A smaller, clearer command surface makes the entire Cypress suite easier to trust.

Interview Questions and Answers

What is a Cypress custom command?

It is an operation registered on the Cypress chain through `Cypress.Commands`. I use one for stable reused setup or domain behavior. It follows Cypress queue semantics rather than normal synchronous execution.

Compare parent, child, and dual commands.

A parent command begins from `cy` and has no previous subject. A child command requires and receives a subject. A dual command makes the subject optional and should be rare because two invocation forms can confuse users.

What is the difference between a command and a query?

A command can perform asynchronous actions and normally runs once in queue order. A query synchronously reads state, is idempotent, and can be retried by Cypress. Side effects never belong in a retryable query.

Why return a chain from a custom command?

Returning preserves execution order and defines the subject yielded to the caller. It lets the test continue with assertions or `.then()`. An omitted return often creates confusing subject behavior.

How do you type a custom command?

I augment the global `Cypress.Chainable` interface with the command input and yielded subject. The declaration file must be included by the Cypress TypeScript project. Registration and type signature are reviewed together.

How do you implement reusable authentication?

I call an approved non-production login boundary inside `cy.session()`, establish browser credentials, and validate restored state. The session ID includes role, user, tenant, and other identity dimensions. Secrets remain redacted.

Why are command overwrites dangerous?

They change familiar built-in behavior for every test. Force-click or failure-suppression overwrites can hide product defects across the suite. I use an overwrite only for a documented cross-cutting policy and test it directly.

How do you review a custom command?

I check domain intent, typed inputs, yielded subject, retry semantics, data ownership, cleanup, logs, errors, and test isolation. I also ask whether a plain or local helper would be clearer. Global commands need an owner and deprecation path.

Frequently Asked Questions

How do I create a custom command in Cypress?

Register it with `Cypress.Commands.add()` in a support module and import that module from the support entry. Add a matching `Cypress.Chainable` declaration for TypeScript.

What is prevSubject in Cypress custom commands?

`prevSubject` declares whether a command requires or accepts the subject yielded by the prior chain. Values such as `element`, `window`, and `document` also enable Cypress validation.

Do Cypress custom commands retry?

Queries and assertions inside a command keep their built-in retry behavior, but an arbitrary action is not safely replayed from the start. Use a custom query for synchronous, idempotent, retryable DOM lookup.

Should login be a Cypress custom command?

It often should when several specs use one approved authentication boundary. Combine the command with `cy.session()` and validation, while keeping navigation separate.

When should I use a plain function instead of a custom command?

Use a plain function for pure builders, formatting, selector construction, and local logic that does not need the command queue. It is simpler to unit-test and does not expand the global API.

Can I overwrite built-in Cypress commands?

Yes, commands can be overwritten with `Cypress.Commands.overwrite()`, but the change is global and risky. Preserve the original signature, return its result, document the reason, and test the behavior.

Where should Cypress custom commands be stored?

Store registrations in support modules grouped by responsibility and import them once from the support entry. Keep TypeScript declarations in an included declaration file or correctly augmented module.

Related Guides