Writing Hardhat tasks
At its core, Hardhat is a task runner that lets you automate your development workflow. It comes with built-in tasks like compile and test, but you can also add your own custom tasks.
In this guide, we’ll explore how to extend Hardhat’s functionality using tasks. It assumes you’ve initialized a sample project. If you haven’t, read the getting started guide first.
Writing a task
Section titled “Writing a task”Let’s write a simple task that prints the list of available accounts. This will help you understand how tasks work and how to create your own.
First, copy and paste the following code into your Hardhat config file:
import { defineConfig, task } from "hardhat/config";
const printAccounts = task("accounts", "Print the accounts") .setAction(() => import("./tasks/accounts.js")) .build();Now let’s create a tasks/accounts.ts file with the task action, which contains the logic that the task will run:
import { HardhatRuntimeEnvironment } from "hardhat/types/hre";
interface AccountTaskArguments { // No argument in this case}
export default async function ( taskArguments: AccountTaskArguments, hre: HardhatRuntimeEnvironment,) { const { provider } = await hre.network.connect(); console.log(await provider.request({ method: "eth_accounts" }));}Next, add the printAccounts task to the exported configuration object in your Hardhat config file:
export default defineConfig({ // ... rest of the config tasks: [printAccounts],});Now you should be able to run it:
npx hardhat accountspnpm hardhat accountsyarn hardhat accountsWe’re using the task function to define our new task. Its first argument is the name of the task, which is what we use on the command line to run it. The second argument is the description, which appears when you run hardhat help.
The task function returns a task builder object that lets you further configure the task. In this case, we use the setAction method to define the task’s behavior by providing a function that lazy loads another module.
That module exports the action function itself, which implements your custom logic. In this case, we send a request to the network provider to get all the configured accounts and print their addresses.
Add parameters to your tasks, and Hardhat handles their parsing and validation for you. Override existing tasks to customize how different parts of Hardhat work.
Using inline actions
Section titled “Using inline actions”As an alternative to defining task actions in separate files, define them directly inline using setInlineAction. This is convenient for simple tasks where a separate file would be overkill.
Here’s how the accounts task would look using an inline action:
import { defineConfig, task } from "hardhat/config";
const printAccounts = task("accounts", "Print the accounts") .setInlineAction(async (taskArguments, hre) => { const { provider } = await hre.network.connect(); console.log(await provider.request({ method: "eth_accounts" })); }) .build();
export default defineConfig({ // ... rest of the config tasks: [printAccounts],});With setInlineAction, the task logic is defined directly as a function parameter, eliminating the need for a separate file.
Choosing between setAction and setInlineAction
Section titled “Choosing between setAction and setInlineAction”Use setAction when you’re building a plugin or a complex task with a lot of code, or if it needs to import dependencies. This keeps the code organized, improves Hardhat’s load time (as they’re loaded on demand), and makes your setup or plugin more resilient to installation errors.
On the other hand, if you’re building a simple task that only uses the Hardhat Runtime Environment, use setInlineAction to define the task’s behavior inline, without the boilerplate of a separate file.
Here’s a comparison of the two approaches:
setAction() (Lazy-loaded) | setInlineAction() | |
|---|---|---|
| Use cases | Complex tasks Plugin tasks Tasks that import dependencies | Simple user tasks |
| Available for plugins | ✅ Yes (required) | ❌ No |
| Available for users | ✅ Yes | ✅ Yes |
| Performance | Lazy-loaded on demand | Loaded every time you run Hardhat |
| File organization | Separate action files | Defined inline in your config |
Can use import { ... } from 'hardhat'? | ✅ Yes, because they are loaded after Hardhat’s initialization | ❌ No, because they are evaluated during Hardhat’s initialization |
Each task must define exactly one action: call either setAction() or setInlineAction(), but not both.
You can also use setInlineAction with overrideTask to customize existing tasks directly in your config file.
To learn more about how task actions are loaded, see the Task Actions’ lifecycle documentation.
Returning a result
Section titled “Returning a result”Task actions can optionally return a Result to signal success or failure to the CLI. When a task action returns a failed result, the CLI sets the process exit code to 1. This is useful when you want to indicate failure to scripts or CI pipelines without throwing an exception.
To use this, import the Result type from hardhat/types/utils and the helper functions from hardhat/utils/result:
import type { HardhatRuntimeEnvironment } from "hardhat/types/hre";import type { Result } from "hardhat/types/utils";import { successfulResult, errorResult } from "hardhat/utils/result";
interface AccountTaskArguments {}
export default async function ( _taskArguments: AccountTaskArguments, hre: HardhatRuntimeEnvironment,): Promise<Result<string[], string>> { const { provider } = await hre.network.connect(); const accounts = await provider.request({ method: "eth_accounts" });
if (accounts.length === 0) { return errorResult("No accounts found"); }
return successfulResult(accounts);}Result<ValueT, ErrorT> is a discriminated union:
{ success: true; value: ValueT }- a successful result, carrying a value{ success: false; error: ErrorT }- a failed result, carrying error information
Use successfulResult(value?) and errorResult(error?) to create these objects. Both helpers can be called without a parameter, in which case they set value or error to undefined.
If a task action doesn’t return a Result (for example, it returns undefined or any other value), the CLI exit code is left unchanged.