In the world of automation and agentic workflows, it's easy to fall into the trap of writing long, monolithic scripts. They start simple but quickly morph into a tangled mess of conditional logic and hard-coded values, becoming impossible to maintain, debug, or reuse. The solution lies in a fundamental shift in thinking: breaking down complex processes into their smallest, indivisible parts.
At .do, we call this The Atomic Unit of Work.
An atomic action is a single, self-contained, and executable task. It's the "Lego brick" of your automation. A workflow doesn't send an email; it executes a send-email action. By building a library of these robust, reusable actions, you transform fragile scripts into scalable, composable systems.
But how do you design an action that's truly reusable and not just a slightly smaller script? Here are five core design principles for crafting powerful atomic actions.
This classic software engineering principle is the cornerstone of atomic design. An action should have one, and only one, reason to change. It performs a single, discrete business operation.
By keeping actions focused, you make them easier to understand, test, and reuse. The send-welcome-email action can now be repurposed for a password reset flow or a re-engagement campaign, completely independent of the user creation process.
For an action to be a reliable building block, its execution must be predictable.
Statelessness means the action doesn't depend on memory or state from previous runs. All the data it needs to perform its job is passed in explicitly as input. It doesn't know or care what happened before or what will happen next.
Idempotency is the property that running an action multiple times with the same inputs will produce the same outcome. This is critical for building resilient workflows that can handle retries and transient failures. For example, a create-customer-in-stripe action, if called twice for the same user, shouldn't create a duplicate customer. It should either return the existing customer ID or update the existing record, but the system's end state is the same.
An action's interface is its contract with the world. This contract should be as clear and explicit as possible. On the .do platform, this is achieved by defining a strict schema for inputs.
Just look at a simple action definition:
import { Action } from '@do-co/agent';
// Define a new atomic action to send a welcome email
const sendWelcomeEmail = new Action('send-welcome-email', {
title: 'Send Welcome Email',
description: 'Sends a standardized welcome email to a new user.',
input: {
to: { type: 'string', required: true },
name: { type: 'string', required: true },
},
async handler({ to, name }) {
console.log(`Sending email to ${to}...`);
// Actual email sending logic would go here
const message = `Welcome to the platform, ${name}!`;
console.log(message);
return { success: true, messageId: `msg_${Date.now()}` };
},
});
The input schema clearly documents what the action needs (to, name), its data types, and whether it's required. This enables automatic validation, provides clear documentation for other developers (or AI agents), and makes composing actions in a workflow a seamless experience. Similarly, the return value provides a predictable output for downstream actions to consume.
An atomic action should be completely unaware of the larger workflow it's part of. It shouldn't contain logic like "if X, then run action Y." That's the job of the workflow, the orchestrator.
The action's sole concern is to execute its task based on the inputs it receives. This strict separation of concerns is what guarantees its reusability. An action that is decoupled can be picked up and dropped into any workflow that can provide its required inputs, making your library of actions incredibly valuable and versatile.
To maximize reusability, separate an action's fixed logic from the variable data it operates on. Avoid hard-coding values like API keys, email templates, or server hostnames directly into the action's handler.
Instead, pass this information in as inputs.
This allows the same send-email action to be used for welcome emails, password resets, and promotional announcements simply by changing the inputs provided by the calling workflow. For secrets like API keys, use secure environment variables that the action can access, rather than passing them as direct inputs.
By adhering to these five principles, you stop writing one-off scripts and start building a robust, scalable library of business capabilities. Each atomic action becomes a trusted, well-understood component that can be composed into increasingly complex and powerful agentic workflows. This "business as code" approach is the future of automation, providing the agility, reliability, and scale that modern operations demand.
What is an atomic action in the .do platform?
An atomic action is the smallest, indivisible unit of work in a workflow. It's a self-contained, executable task—like sending an email, querying a database, or calling an external API. Each action is designed to do one thing well.
How are actions different from workflows?
Actions are the individual steps, while workflows are the orchestration of multiple actions in a specific sequence or logic. You build complex workflows by composing simple, reusable actions together.
Can I create my own custom actions?
Absolutely. The .do SDK allows you to define custom actions with specific inputs, outputs, and business logic. This transforms your unique business operations into reusable, programmable building blocks for any workflow.