Skip to main content
Cypress App

task

cy.task() runs code in Node.js from your test, through the task event you register in setupNodeEvents. It handles the work a browser can't do on its own:

  • Seed or query a database.
  • Read, clean up, and inspect files on disk.
  • Run a CLI or another external command.
  • Wait for something that happens outside the browser, such as an email.
  • Keep state between spec files.

Your test runs in the browser and your task handler runs in Node.js, so the argument and the return value are both serialized as they cross between the two:

Diagram: your test in the browser calls cy.task('event', arg), which is serialized like JSON.stringify() and sent to your task handler in Node.js. The handler's return value is serialized the same way and yielded to your test.
caution
Anti-Pattern

Don't start a web server with cy.task(). Start it before Cypress instead, as described in Best Practices.

Syntax​

cy.task(event)
cy.task(event, arg)
cy.task(event, arg, options)

Usage​

Correct Usage

log.cy.js
cy.task('log', 'This will be output to the terminal')
const { defineConfig } = require('cypress')

module.exports = defineConfig({
// setupNodeEvents can be defined in either
// the e2e or component configuration
e2e: {
setupNodeEvents(on, config) {
on('task', {
log(message) {
console.log(message)

return null
},
})
},
},
})

The task plugin event handler can return a value or a promise. The command fails if the handler returns undefined or the promise resolves with undefined. This catches typos and events that no handler covers.

If you don't need to return a value, return null to signal that the event was handled.

Incorrect Usage

// Errors, the event name is required
cy.task()
const { defineConfig } = require('cypress')

module.exports = defineConfig({
// setupNodeEvents can be defined in either
// the e2e or component configuration
e2e: {
setupNodeEvents(on, config) {
on('task', {
// Errors, the handler returns undefined
log(message) {
console.log(message)
},
})
},
},
})

Arguments​

event (String)

An event name to be handled via the task event in the setupNodeEvents function.

arg (Object)

An argument to send along with the event. This can be any value that can be serialized by JSON.stringify(), and the handler receives it the way JSON.stringify() would serialize it:

  • Functions, symbols, and undefined are dropped from object properties and become null inside arrays.
  • Regular expressions, Map, and Set become empty objects ({}).
  • Date objects become ISO date strings. Rebuild one in the handler with new Date(value).
  • An argument with circular references can't be serialized, so the command fails.

When you omit arg, the handler receives undefined, so a default parameter value in the handler applies.

To pass multiple values, put them in an object and destructure it in the handler:

hello.cy.js
cy.task('hello', { greeting: 'Hello', name: 'World' })
const { defineConfig } = require('cypress')

module.exports = defineConfig({
// setupNodeEvents can be defined in either
// the e2e or component configuration
e2e: {
setupNodeEvents(on, config) {
on('task', {
// destructure the individual properties
hello({ greeting, name }) {
console.log('%s, %s', greeting, name)

return null
},
})
},
},
})

options (Object)

Pass in an options object to change the default behavior of cy.task().

OptionDefaultDescription
logtrueDisplays the command in the Command log
timeouttaskTimeoutTime to wait for cy.task() to resolve before timing out

Yields ​

cy.task() yields the value returned or resolved by the task event in setupNodeEvents.

Examples​

Read and clean up files on disk with cy.task()​

Read a file that might not exist with cy.task()​

cy.readFile() assumes the file exists. To read a file that might not exist, use a task.

read-file.cy.js
cy.task('readFileMaybe', 'my-file.txt').then((textOrNull) => { ... })
const { defineConfig } = require('cypress')
const fs = require('fs')

module.exports = defineConfig({
// setupNodeEvents can be defined in either
// the e2e or component configuration
e2e: {
setupNodeEvents(on, config) {
on('task', {
readFileMaybe(filename) {
if (fs.existsSync(filename)) {
return fs.readFileSync(filename, 'utf8')
}

return null
},
})
},
},
})

Count the files in a folder with cy.task()​

count-files.cy.js
cy.task('countFiles', 'cypress/downloads').then((count) => { ... })
const { defineConfig } = require('cypress')
const fs = require('fs')

module.exports = defineConfig({
// setupNodeEvents can be defined in either
// the e2e or component configuration
e2e: {
setupNodeEvents(on, config) {
on('task', {
countFiles(folderName) {
return new Promise((resolve, reject) => {
fs.readdir(folderName, (err, files) => {
if (err) {
return reject(err)
}

resolve(files.length)
})
})
},
})
},
},
})

Clean up and verify downloaded files with cy.task()​

Tasks are a good fit for file system work, such as emptying the downloads folder between tests and then finding the file the test just downloaded. config.downloadsFolder is the resolved absolute path, so the task keeps working if the project overrides downloadsFolder.

downloads.cy.js
cy.task('clearDownloads')

cy.task('latestDownload').then((filepath) => {
expect(filepath).to.be.a('string')
cy.readFile(filepath)
})
const { defineConfig } = require('cypress')
const fs = require('fs')
const path = require('path')

module.exports = defineConfig({
// setupNodeEvents can be defined in either
// the e2e or component configuration
e2e: {
setupNodeEvents(on, config) {
const downloads = config.downloadsFolder

on('task', {
clearDownloads() {
fs.rmSync(downloads, { recursive: true, force: true })
fs.mkdirSync(downloads, { recursive: true })

return null
},

latestDownload() {
const files = fs
.readdirSync(downloads)
.map((name) => ({
name,
time: fs.statSync(path.join(downloads, name)).mtimeMs,
}))
.sort((a, b) => b.time - a.time)

return files.length ? path.join(downloads, files[0].name) : null
},
})
},
},
})

Cypress already trashes the downloads folder before cypress run when trashAssetsBeforeRuns is enabled, which it is by default. A clearDownloads task is for clearing the folder between tests within a run.

Manage test data and state with cy.task()​

Seed a database before each test with cy.task()​

articles.cy.js
describe('e2e', () => {
beforeEach(() => {
cy.task('defaults:db')
cy.visit('/')
})

it('displays article values', () => {
cy.get('.article-list').should('have.length', 10)
})
})
const { defineConfig } = require('cypress')
// the module in your app that seeds the database,
// with a path relative to the Cypress config file
const db = require('./server/db')

module.exports = defineConfig({
// setupNodeEvents can be defined in either
// the e2e or component configuration
e2e: {
setupNodeEvents(on, config) {
on('task', {
async 'defaults:db'() {
await db.seed('defaults')

// seed() may resolve with undefined, so signal success explicitly
return null
},
})
},
},
})

Query a MySQL database from a Cypress test​

Pass the query and the name of the database to run it against in one object, then destructure it in the handler. This example uses the mysql2 client.

query-users.cy.js
const dbName = 'stagingA'
const query = 'SELECT * FROM users'

cy.task('queryDatabase', { dbName, query })
const { defineConfig } = require('cypress')
const mysql = require('mysql2/promise')

// the connection details for each database could
// come from environment variables
const connections = {
stagingA: {
host: 'staging.my.co',
user: 'test',
password: '***',
database: 'users',
},
stagingB: {
host: 'staging-b.my.co',
user: 'test',
password: '***',
database: 'users',
},
}

module.exports = defineConfig({
// setupNodeEvents can be defined in either
// the e2e or component configuration
e2e: {
setupNodeEvents(on, config) {
on('task', {
// destructure the argument into the individual fields
async queryDatabase({ dbName, query }) {
const connectionInfo = connections[dbName]

if (!connectionInfo) {
throw new Error(`No database connection named ${dbName}`)
}

const connection = await mysql.createConnection(connectionInfo)

try {
const [rows] = await connection.query(query)

return rows
} finally {
await connection.end()
}
},
})
},
},
})

Share state between spec files with cy.task()​

The config process keeps running for the whole run, so a value stored in a variable there is still available to later spec files.

create-org.cy.js
cy.task('setValue', { key: 'orgId', value: 42 })
view-org.cy.js
cy.task('getValue', 'orgId').should('eq', 42)
const { defineConfig } = require('cypress')
const store = {}

module.exports = defineConfig({
// setupNodeEvents can be defined in either
// the e2e or component configuration
e2e: {
setupNodeEvents(on, config) {
on('task', {
setValue({ key, value }) {
store[key] = value

return null
},
getValue(key) {
// return null rather than undefined when the key is missing
return store[key] ?? null
},
})
},
},
})

Keep these behaviors in mind:

  • The store starts empty each time the config process starts, such as at the beginning of every cypress run.
  • Machines running specs in parallel each have their own config process, so they don't share values.
  • A spec that reads a value another spec wrote only passes when the specs run in that order. Use a store for values you can recreate when they're missing, and cy.session() for caching logins.

Keep a variable across origins with cy.task()​

When a test visits a different origin, Cypress reloads the spec to switch to the new origin, which resets the variables in your spec file. A task keeps the value in Node.js, where the reload doesn't reach it.

href.cy.js
describe('Href visit', () => {
it('captures href', () => {
cy.visit('https://example.cypress.io')
cy.get('a')
.invoke('attr', 'href')
.then((href) => {
// href is not same-origin as current url
// like https://www.cypress-dx.com
cy.task('setHref', href)
})
})

it('visit href', () => {
cy.task('getHref').then((href) => {
// visit non same-origin url https://www.cypress-dx.com
cy.visit(href)
})
})
})
const { defineConfig } = require('cypress')
let href

module.exports = defineConfig({
// setupNodeEvents can be defined in either
// the e2e or component configuration
e2e: {
setupNodeEvents(on, config) {
on('task', {
setHref: (val) => {
return (href = val)
},
getHref: () => {
return href
},
})
},
},
})

Run processes outside the browser with cy.task()​

Run an external command or CLI with cy.task()​

When setup depends on a tool that isn't a Node module, such as a Docker container, a database CLI, or a cloud provider's command line tool, spawn it from the task with child_process.execFileSync(). Passing the arguments as an array avoids the shell entirely, so quoting and PATH resolution behave the same way on every platform.

database.cy.js
cy.task('resetDatabase')
const { defineConfig } = require('cypress')
const { execFileSync } = require('child_process')

module.exports = defineConfig({
// setupNodeEvents can be defined in either
// the e2e or component configuration
e2e: {
setupNodeEvents(on, config) {
on('task', {
resetDatabase() {
execFileSync('docker', ['exec', 'db', 'reset-script'], {
stdio: 'pipe',
})

return null
},
})
},
},
})

A task can also return the command's output to the test:

bucket.cy.js
cy.task('listBucket', 's3://my-test-bucket').should('have.length', 3)
const { defineConfig } = require('cypress')
const { execFileSync } = require('child_process')

module.exports = defineConfig({
// setupNodeEvents can be defined in either
// the e2e or component configuration
e2e: {
setupNodeEvents(on, config) {
on('task', {
listBucket(bucket) {
const output = execFileSync('aws', ['s3', 'ls', bucket], {
encoding: 'utf8',
})

return output.split('\n').filter(Boolean)
},
})
},
},
})

Wait for an email or background job with cy.task()​

cy.task() runs the handler once and doesn't retry it. To wait for something that happens outside the browser, such as an email arriving or a background job finishing, poll inside the handler and resolve once the result is ready.

signup.cy.js
cy.task(
'waitForEmail',
{ to: '[email protected]', timeout: 30000 },
{ timeout: 40000 }
)
.its('subject')
.should('eq', 'Welcome to the app')
const { defineConfig } = require('cypress')
// findEmail comes from your email testing service's client
const { findEmail } = require('./mail-client')

module.exports = defineConfig({
// setupNodeEvents can be defined in either
// the e2e or component configuration
e2e: {
setupNodeEvents(on, config) {
on('task', {
async waitForEmail({ to, timeout }) {
const deadline = Date.now() + timeout

while (Date.now() < deadline) {
const email = await findEmail(to)

if (email) {
return email
}

await new Promise((resolve) => setTimeout(resolve, 1000))
}

throw new Error(`No email to ${to} arrived within ${timeout}ms`)
},
})
},
},
})

Give the cy.task() timeout option a few seconds more than the handler's own deadline. The handler then gives up first, and the test fails with its message instead of a generic timeout.

Write cy.task() handlers in setupNodeEvents​

Return a promise from an async cy.task() handler​

pause.cy.js
cy.task('pause', 1000)
const { defineConfig } = require('cypress')

module.exports = defineConfig({
// setupNodeEvents can be defined in either
// the e2e or component configuration
e2e: {
setupNodeEvents(on, config) {
on('task', {
pause(ms) {
return new Promise((resolve) => {
// tasks should not resolve with undefined
setTimeout(() => resolve(null), ms)
})
},
})
},
},
})

Type the value cy.task() yields in TypeScript​

In TypeScript, cy.task() yields unknown unless you pass a type parameter describing what the task returns. Cypress doesn't check the type at runtime, so keep it in step with the handler.

users.cy.ts
type User = { id: number; name: string }

cy.task<User[]>('getUsers').then((users) => {
// users is typed as User[]
expect(users[0].name).to.eq('Ada')
})
const { defineConfig } = require('cypress')

module.exports = defineConfig({
// setupNodeEvents can be defined in either
// the e2e or component configuration
e2e: {
setupNodeEvents(on, config) {
on('task', {
getUsers() {
return [
{ id: 1, name: 'Ada' },
{ id: 2, name: 'Grace' },
]
},
})
},
},
})

Run Node.js code before each Cypress test​

There is no setupNodeEvents event for each test. To run Node.js code before every test, call cy.task() from a global beforeEach in your support file.

cypress/support/e2e.js
beforeEach(function () {
cy.task('logTestStart', this.currentTest.fullTitle())
})
const { defineConfig } = require('cypress')

module.exports = defineConfig({
// setupNodeEvents can be defined in either
// the e2e or component configuration
e2e: {
setupNodeEvents(on, config) {
on('task', {
logTestStart(title) {
console.log(`Starting: ${title}`)

return null
},
})
},
},
})

Notes​

cy.task() doesn't support long-running processes​

cy.task() doesn't support tasks that keep running, such as:

  • Starting a server.
  • Watching for file changes.
  • Any process that needs to be interrupted to stop.

A task must end within the timeout, or Cypress fails the current test.

Change the cy.task() timeout for a command or suite​

cy.task() waits up to taskTimeout, 60 seconds by default. Keep tasks short where you can: Cypress doesn't run any other commands until cy.task() finishes, so a long task slows down every test that calls it.

To give one command more time, pass the timeout option:

// fails if seeding the database takes longer than 20 seconds
cy.task('seedDatabase', null, { timeout: 20000 })

To change it for a suite or test, set taskTimeout in the test configuration. The value applies while that suite or test runs, then returns to the previous value.

describe('has data available from database', { taskTimeout: 90000 }, () => {
before(() => {
cy.task('seedDatabase')
})

// tests

after(() => {
cy.task('resetDatabase')
})
})

To change it for the rest of the spec file, set it with Cypress.config():

Cypress.config('taskTimeout', 30000)
Cypress.config('taskTimeout') // => 30000

Errors thrown in a cy.task() handler fail the test​

When a handler throws or returns a rejected promise, cy.task() fails the test with the error's message.

Return null when "not found" is an answer the test can act on, and throw when the test can't continue:

const { defineConfig } = require('cypress')
const users = [{ email: '[email protected]', name: 'Ada' }]

module.exports = defineConfig({
// setupNodeEvents can be defined in either
// the e2e or component configuration
e2e: {
setupNodeEvents(on, config) {
on('task', {
// a missing user is an expected answer, so return null
findUser(email) {
return users.find((user) => user.email === email) ?? null
},
// a test that deletes a missing user has bad setup, so throw
deleteUser(email) {
const index = users.findIndex((user) => user.email === email)

if (index === -1) {
throw new Error(`No user with email ${email}`)
}

users.splice(index, 1)

return null
},
})
},
},
})
users.cy.js
cy.task('deleteUser', '[email protected]')
CypressError: `cy.task('deleteUser')` failed with the following error:

> No user with email [email protected]

https://on.cypress.io/api/task

In the Cypress App, the error points to the cy.task() line in your spec. Expand Stack trace to find the From Node.js Internals section, which shows where your handler threw.

Command Log for a failed test: the task row for deleteUser is highlighted in red, followed by a CypressError reading cy.task('deleteUser') failed with the following error: No user with email grace@example.com, a code frame pointing to the cy.task() line in the spec, and a collapsed Stack trace toggle.

Cypress merges every on('task') registration​

Cypress merges every on('task') object you register, so you can split your tasks into modules by area. Each module exports an object of handlers, like this state.ts:

// cypress/tasks/state.ts
const store = {}

module.exports.stateTasks = {
setValue({ key, value }) {
store[key] = value

return null
},
getValue(key) {
return store[key] ?? null
},
}
// cypress/tasks/state.ts
const store: Record<string, unknown> = {}

export const stateTasks = {
setValue({ key, value }: { key: string; value: unknown }) {
store[key] = value

return null
},
getValue(key: string) {
return store[key] ?? null
},
}

Then register each module, here alongside a users.ts module built the same way:

const { defineConfig } = require('cypress')
const { stateTasks } = require('./cypress/tasks/state')
const { userTasks } = require('./cypress/tasks/users')

module.exports = defineConfig({
// setupNodeEvents can be defined in either
// the e2e or component configuration
e2e: {
setupNodeEvents(on, config) {
on('task', stateTasks)
on('task', userTasks)
},
},
})

Plugins that export their own tasks merge the same way, alongside yours.

caution
Duplicate task keys

If multiple task objects use the same key, the later registration overwrites that particular key, the same way merging multiple objects with duplicate keys overwrites the first one. Cypress prints a warning in the terminal that names each duplicated task.

Access secrets at runtime with cy.task()​

cy.task() reads secrets at runtime, which suits values from an external secret manager or an API that isn't available at configuration time. For secrets that are available at configuration time, use cy.env() instead.

secrets.cy.js
// executes at runtime
cy.task('getSecret', 'API_KEY', { log: false }).then((apiKey) => {
cy.request({
method: 'POST',
url: 'https://api.example.com/data',
headers: {
Authorization: `Bearer ${apiKey}`,
},
log: false,
})
})

Pass { log: false } to both commands. Otherwise the Command Log shows the cy.task() call, its console output shows the secret it yields, and the cy.request() console output shows the Authorization header. With log: false, neither command appears in the Command Log.

const { defineConfig } = require('cypress')

module.exports = defineConfig({
// setupNodeEvents can be defined in either
// the e2e or component configuration
e2e: {
setupNodeEvents(on, config) {
// Task definition - executes when cy.task() is called in tests
on('task', {
getSecret(secretName) {
// This reads from process.env at runtime, not at configuration time
// Useful when secrets are fetched dynamically or only needed conditionally
const secret = process.env[secretName]

if (!secret) {
throw new Error(`Secret ${secretName} is not set`)
}

return secret
},
})
},
},
})

Rules​

Requirements ​

  • cy.task() requires being chained off of cy.
  • cy.task() requires a non-empty string as its first argument.
  • cy.task() requires the task event to be registered in setupNodeEvents, with a handler for the given event name.
  • cy.task() requires being invoked from the spec file or support file in end-to-end tests.
  • cy.task() requires the task to eventually end.

Assertions ​

  • cy.task() runs the assertions you chain once and doesn't retry them.

Timeouts ​

  • cy.task() can time out waiting for the task to end.

Command Log​

This example uses the Count the files in a folder with cy.task() task defined above.

cy.task('countFiles', 'cypress/e2e')

The command above displays in the Command Log as:

Command Log row for the task command, showing the event name countFiles and its argument

When you click the task command in the Command Log, the console shows the following:

Console output for the task command, listing Command, Task, Arg, and the Yielded file count

History​

VersionChanges
5.6.0Added a type parameter for the value cy.task() yields
5.3.0A task called without an argument receives undefined instead of null
4.4.0The error for a task that resolves undefined names the resolution value it needs
3.0.0cy.task() command added

See also​