You've grasped the core concept: on the .do platform, the atomic action is the fundamental building block of any powerful agentic workflow. It's the single, indivisible, and executable unit of work. You've probably already built a few simple actions, like the sendWelcomeEmail example, and seen how they can be triggered to perform tasks.
But as you move from simple scripts to orchestrating complex, mission-critical business processes, the design of your actions becomes paramount. Building robust, scalable, and maintainable systems isn't just about what your actions do—it's about how they are designed to handle the complexities of the real world.
Let's move beyond the basics and explore four advanced principles that will transform your atomic actions from simple functions into resilient, enterprise-grade components.
In a distributed system, you must assume that things will fail. A network request might time out, a third-party API could be temporarily unavailable, or a message queue might deliver the same event twice. Idempotency is the property of an action where executing it multiple times with the same input has the exact same effect as executing it only once.
Why it matters: Imagine a non-idempotent action to "charge a customer $10." If a network hiccup causes your workflow to retry that action, you've just double-charged your customer. An idempotent design prevents this. It ensures that no matter how many times an action is retried, the outcome remains correct.
How to achieve it:
Let's enhance our email example to be idempotent.
import { Action } from '@do-co/agent';
import { db } from './database'; // Fictional database client
const sendWelcomeEmail = new Action('send-welcome-email', {
title: 'Send Welcome Email',
description: 'Sends a welcome email, ensuring it is only sent once.',
input: {
userId: { type: 'string', required: true },
idempotencyKey: { type: 'string', required: true }, // e.g., the trigger event ID
},
async handler({ userId, idempotencyKey }) {
// 1. Check if this action has already been completed
const existingLog = await db.findLog({ key: idempotencyKey });
if (existingLog) {
console.log(`Idempotency key ${idempotencyKey} already processed. Skipping.`);
return { success: true, messageId: existingLog.messageId, status: 'skipped' };
}
// 2. Fetch user details
const user = await db.findUser({ id: userId });
if (!user) {
return { success: false, error: 'User not found' };
}
// 3. Perform the action
console.log(`Sending email to ${user.email}...`);
const message = `Welcome to the platform, ${user.name}!`;
const messageId = `msg_${Date.now()}`; // From actual email service
// 4. Log the completion with the idempotency key
await db.createLog({ key: idempotencyKey, messageId });
return { success: true, messageId, status: 'sent' };
},
});
The "atomic" in atomic action is a crucial clue. An action should do one thing and do it well. But what constitutes "one thing"?
The Guideline: A well-designed action represents a single, logical business operation that you might want to retry or reuse independently.
Instead of one monolithic onboardNewUser action, you should create several smaller, atomic actions:
Your agentic workflow then becomes the composer, orchestrating these reusable "LEGO bricks" in the correct sequence and handling the logic between them.
Things go wrong. An API key is invalid, a database is unreachable, or input data is malformed. A robust action doesn't just crash; it provides clear, structured information about the failure back to the workflow engine.
This allows the orchestrating agent to make intelligent decisions:
How to achieve it: Your action's handler should catch exceptions and return a consistent, structured error object. This turns unknown exceptions into predictable data that the system can act upon.
// ... inside an Action handler
async handler({ to, name }) {
try {
const response = await emailService.send({
to,
subject: 'Welcome!',
body: `Welcome to the platform, ${name}!`,
});
return { success: true, messageId: response.id };
} catch (error) {
// Check for a specific, retriable error type
if (error.code === 'RATELIMIT_EXCEEDED') {
return {
success: false,
error: {
code: 'TRANSIENT_ERROR',
message: 'Email service rate limit exceeded.',
}
};
}
// Return a generic permanent error for everything else
return {
success: false,
error: {
code: 'PERMANENT_ERROR',
message: error.message
}
};
}
}
The ultimate goal is to build a library of custom actions that represent your unique business operations. To make this library powerful, every action must be designed for maximum reusability.
By embracing idempotency, finding the right granularity, handling errors gracefully, and designing for reusability, you elevate your use of the .do platform. You move from simply automating tasks to engineering resilient, scalable, and maintainable "business as code."
The next time you write new Action(), take a moment to consider these principles. A little thought upfront will pay massive dividends as your agentic workflows grow in complexity and importance.