In modern software development, we often face a dilemma. We need to build complex, multi-step business processes, but we also crave simplicity, reusability, and resilience. Monolithic scripts become tangled and brittle, while disconnected function calls lack the necessary observability and error handling for enterprise-grade automation. How do we build robust systems without drowning in complexity?
The answer lies in composition—starting with the smallest, most reliable unit of work and assembling it into something greater. This is the core philosophy behind action.do: providing the foundational building blocks for any agentic workflow. By starting with the atomic action, we can compose sophisticated, high-level business services that are scalable, maintainable, and easy to understand.
Before you can build a castle, you need to understand the brick. In the .do ecosystem, that brick is the atomic action.
As defined in our hero headline, an atomic action is about encapsulating an individual task into a manageable, reusable, and observable unit. But what does that mean in practice?
An atomic action is the smallest, indivisible unit of work in your process. Think of it as a supercharged function with clear, defined responsibilities:
This transforms a simple function call into a managed, enterprise-grade component for reliable business automation.
Let's look at the code. Here’s how you’d define an action to send an email using action.do:
import { Action } from '@do-co/core';
// Define the input for our action
interface SendEmailInput {
to: string;
subject: string;
body: string;
}
// Define the output of our action
interface SendEmailOutput {
messageId: string;
status: 'sent' | 'failed';
}
// Create the action as a service
const sendEmail = new Action<SendEmailInput, SendEmailOutput>({
name: 'send-email',
description: 'Sends a transactional email.',
handler: async (input) => {
// Integration with an email provider (e.g., SendGrid, SES)
// would happen here. For this example, we'll simulate it.
console.log(`Sending email to ${input.to}...`);
const messageId = `msg_${Date.now()}`;
return {
messageId,
status: 'sent',
};
},
});
// This action can now be executed within any workflow or agent.
This send-email action is now a reusable, testable, and observable building block. It’s no longer just a function; it’s a standardized unit of task execution.
One brick is useful, but its true power is realized when combined with others. The core purpose of atomic actions is to be composed into larger, more meaningful processes.
Imagine a common business scenario: onboarding a new user. This single outcome involves several distinct steps. Instead of writing one long script, we can define it as a sequence of atomic actions:
Each of these is a simple, independent action.do. They are easy to build, test in isolation, and reuse in other workflows. For example, the notify-slack-channel action could also be used in an error-reporting workflow.
This is where the magic happens. By orchestrating these atomic actions using a workflow engine like workflow.do, we create more than just a sequence of tasks—we create a high-level business service.
Let's call our new composed service onboard-new-user.
This onboard-new-user service encapsulates the entire business logic of user onboarding. It has a single, clear purpose and can be invoked by any part of your application—your public website, an admin dashboard, or an external API call.
It looks something like this conceptually:
// This is a high-level representation of a workflow
service: onboard-new-user(input: NewUserDetails) {
// Step 1: Execute the database action
const userRecord = execute(create-user-record, { name: input.name, email: input.email });
// Step 2: Execute the email action
execute(send-welcome-email, { to: userRecord.email });
// Step 3: Execute the Slack notification action
execute(notify-slack-channel, { channel: '#new-users', text: `New user signed up: ${userRecord.name}` });
// Step 4: Execute the CRM action
execute(create-crm-lead, { email: userRecord.email, source: 'webapp-signup' });
return { status: 'success', userId: userRecord.id };
}
You have effectively moved from low-level actions to a high-level business capability. This service-oriented approach provides immense benefits:
The journey from a single action.do to a fully-fledged business service demonstrates a powerful paradigm for building modern automation. By focusing on small, atomic units of work, you gain the flexibility to compose them into robust, scalable, and observable agentic workflows. This is the foundation to Execute. Measure. Automate.
Ready to transform your business processes from brittle scripts into resilient services? Start by defining your first atomic action.
What is an 'atomic action' in the context of .do?
An atomic action is the smallest, indivisible unit of work in an agentic workflow. It's a self-contained function (like sending an email or updating a CRM record) with clear inputs and outputs, designed to be reliable, reusable, and observable.
How does action.do differ from a simple function call?
While an action.do wraps a function, it adds a structured layer of observability, error handling, retries, and standardization. This transforms a simple function into a managed, enterprise-grade component for reliable automation.
Can I combine multiple actions?
Absolutely. The core purpose of atomic actions is to serve as building blocks. They are designed to be orchestrated by higher-level services like workflow.do or agent.do to create complex, multi-step business processes.
What are some common examples of an action.do?
Common actions include 'send-email', 'create-support-ticket', 'update-database-record', 'query-api', 'post-to-slack', or 'transcribe-audio'. Each action is focused on performing one specific task perfectly.