Q31 of 48 · Cypress

What is cy.task and when is it the right tool?

CypressMidcypresscy-tasksetupmid

Short answer

Short answer: `cy.task(name, args)` runs a Node-side function defined in `setupNodeEvents`. Use it for things the browser can't do: seeding the database, reading files outside Cypress's reach, hitting internal Node APIs, sending external notifications. The browser-side test stays fast; Node handles the rest.

Detail

Cypress runs in two processes: the browser (your test code, with cy commands) and Node (the runner, plugins, and setupNodeEvents). cy.task is the bridge — it sends a message from the browser to Node, runs your registered Node function, and returns the result back to the browser test.

Register tasks in setupNodeEvents:

// cypress.config.ts
setupNodeEvents(on, config) {
  on('task', {
    seedUser({ email, role }) {
      // Hit your database directly via a Node client
      return db.users.insert({ email, role });
    },
    clearDownloads() {
      fs.rmSync(path.join(__dirname, 'cypress/downloads'), { recursive: true, force: true });
      fs.mkdirSync(path.join(__dirname, 'cypress/downloads'));
      return null; // tasks must return non-undefined
    },
    log(message) {
      console.log(message);
      return null;
    },
  });
  return config;
}

Use it in tests:

beforeEach(() => {
  cy.task('seedUser', { email: 'alice@x.com', role: 'admin' });
});

When cy.task is the right tool:

  • Database seeding — far faster than seeding via API endpoints, especially for setup that doesn't represent the test's intent.
  • File system ops — clearing downloads/, reading large fixtures, writing test artefacts.
  • Stateful operations across specs — coordinating shared state, reading env-specific secrets.
  • Calls to internal services — message brokers, third-party SDK calls that don't belong in the browser.

When not to use it:

  • Anything you could do in the browser with cy.request against your own API. Tasks are heavier and break the "test the contract" principle.
  • Logic that should be tested. Tasks are setup, not the system under test.

Tasks must return a non-undefined value (return null if you have nothing to report). They serialize args/returns as JSON, so don't pass functions or class instances.

// EXAMPLE

cypress.config.ts + spec

// cypress.config.ts
import { defineConfig } from 'cypress';
import { Pool } from 'pg';

const pool = new Pool({ connectionString: process.env.TEST_DB_URL });

export default defineConfig({
  e2e: {
    setupNodeEvents(on, config) {
      on('task', {
        async resetDb() {
          await pool.query('TRUNCATE users, orders RESTART IDENTITY CASCADE');
          return null;
        },
        async seedUser({ email, role }: { email: string; role: string }) {
          const { rows } = await pool.query(
            'INSERT INTO users(email, role) VALUES($1, $2) RETURNING id',
            [email, role],
          );
          return rows[0].id;
        },
      });
    },
  },
});

// In a spec
beforeEach(() => {
  cy.task('resetDb');
  cy.task('seedUser', { email: 'admin@x.com', role: 'admin' }).then((userId) => {
    cy.wrap(userId).as('adminId');
  });
});

// WHAT INTERVIEWERS LOOK FOR

Knowing it's the browser→Node bridge, the registration pattern in `setupNodeEvents`, and the typical use cases (DB seeding, file ops). Bonus for the 'must return non-undefined' rule.

// COMMON PITFALL

Forgetting to return a value (or `null`) from a task — Cypress treats `undefined` as 'no task registered' and throws.