In the world of automated business processes, a single point of failure can trigger a cascade of problems. A network glitch, a temporary API outage, or an unexpected edge case can leave your systems in an inconsistent state, causing everything from duplicate user notifications to incorrect billing. The antidote to this fragility isn't just more try...catch blocks; it's a fundamental shift in how we design our automations.
Building resilient, reliable systems requires a design philosophy centered on granularity and predictability. This is where the concept of the atomic action becomes your most powerful tool. With action.do, you can build robust workflows from the ground up by focusing on two critical pillars: idempotency and sophisticated error handling.
This guide will show you how to leverage action.do to move beyond brittle scripts and create truly resilient, agentic workflows.
Imagine a standard user registration workflow:
If this is built as one long script, what happens if step 3 fails because your email provider's API is momentarily down? If you simply re-run the entire script, you risk double-charging the user and creating a duplicate CRM entry. This is the classic challenge of monolithic workflow automation. The entire process lacks the granularity to recover gracefully.
An atomic action is the smallest, indivisible unit of work in a business process. As we define it at action.do, it’s a single, self-contained task like 'send-invoice,' 'update-crm-record,' or 'verify-user-identity.'
Instead of one giant script, you break the process down:
When you isolate each step into a discrete task execution unit, you gain precise control over failures. If send-welcome-email fails, you can retry only that action without affecting the others. This is the first step toward resilience.
Idempotency is a core principle of distributed systems. It means that performing an operation multiple times produces the same result as performing it once. For our user registration workflow, this means retrying the charge-subscription action won't charge the customer a second time.
With action.do, you can bake idempotency directly into your atomic actions. Each action is essentially a serverless function with a handler, giving you the perfect place to implement this logic.
Let’s enhance our send-welcome-email example to make it idempotent. We can pass a unique runId with our inputs to track a specific execution attempt.
import { Do } from '@do-inc/sdk';
const platform = new Do({ apiKey: 'YOUR_API_KEY' });
// A database or cache to track executed runs
const executionLog = new Map<string, any>();
const sendWelcomeEmail = platform.action('send-welcome-email-idempotent', {
description: 'Sends a welcome email, ensuring it is only sent once per run.',
handler: async (inputs: { email: string, name: string, runId: string }) => {
// 1. Idempotency Check: Has this runId been processed already?
if (executionLog.has(inputs.runId)) {
console.log(`Idempotency key ${inputs.runId} already processed. Skipping.`);
return executionLog.get(inputs.runId); // Return the original result
}
// 2. Business Logic: Perform the action
console.log(`Sending welcome email to ${inputs.name} at ${inputs.email}`);
const result = { success: true, messageId: `msg-${Date.now()}` };
// 3. Log the execution and result against the idempotency key
executionLog.set(inputs.runId, result);
return result;
},
});
// Now, even if we call run() twice with the same runId,
// the email will only be sent once.
const uniqueRunId = 'user-signup-alex-12345';
await sendWelcomeEmail.run({ email: 'alex@example.com', name: 'Alex', runId: uniqueRunId });
await sendWelcomeEmail.run({ email: 'alex@example.com', name: 'Alex', runId: uniqueRunId });
// Console Log:
// > Sending welcome email to Alex at alex@example.com
// > Idempotency key user-signup-alex-12345 already processed. Skipping.
By adding this simple check, you've made your action resilient to network retries and duplicate triggers.
A simple throw Error is often not enough information for an orchestrator to make an intelligent decision. Should it retry immediately? Wait a few minutes? Or mark the job as permanently failed?
Because every action.do is a function that accepts inputs and returns outputs, you can design your error responses to be as structured as your success responses. This empowers your agentic workflow to handle failures gracefully.
Consider an action that calls an external API. The handler is the perfect place to interpret different failure modes.
import { Do } from '@do-inc/sdk';
import { callThirdPartyApi } from './api-client'; // A hypothetical API client
const platform = new Do({ apiKey: 'YOUR_API_KEY' });
const updateUserCrm = platform.action('update-user-crm', {
description: 'Updates a user record in the external CRM.',
handler: async (inputs: { userId: string, crmData: object }) => {
try {
const response = await callThirdPartyApi(inputs.userId, inputs.crmData);
return { success: true, crmId: response.id };
} catch (error: any) {
// Give the orchestrator structured, actionable error information
if (error.statusCode === 429 || error.statusCode >= 500) {
// Rate limiting or server error - this is likely temporary
return {
success: false,
error: 'service_unavailable',
message: 'CRM API is temporarily unavailable.',
retryable: true
};
} else if (error.statusCode === 400) {
// Bad request, e.g., invalid data - this is not going to fix itself
return {
success: false,
error: 'invalid_input',
message: error.message,
retryable: false
};
} else {
// An unknown error
return { success: false, error: 'unknown_error', retryable: false };
}
}
},
});
With this pattern, the system orchestrating your business process can use the retryable flag to decide its next move. A retryable error might trigger a backoff-and-retry sequence, while a non-retryable error could route the task to a dead-letter queue for manual review.
Once you have a library of idempotent, robust atomic actions, building a complex and reliable workflow becomes dramatically simpler. Your orchestration layer—whether it’s custom code or a state machine—is no longer responsible for the messy details of task execution. Its job is simplified to:
This modular approach, where resilient building blocks are composed into a larger service, is the essence of creating Services-as-Software. You build reliable components first, then assemble them into a reliable whole.
Ready to stop worrying about automations that break in the middle of the night? Start building your first atomic action and lay the foundation for truly resilient workflows.
Q: What is an 'atomic action' in the context of .do?
A: An atomic action is the smallest, indivisible unit of work in a business process. Think of it as a single, self-contained task like 'send an invoice,' 'update a CRM record,' or 'verify user identity.' These actions are the fundamental building blocks for creating services on the .do platform.
Q: How is an action.do different from a full workflow?
A: An action.do represents a single step, while a workflow orchestrates multiple actions in a sequence or based on specific logic. You compose workflows by connecting various action.do agents together to model a complete business process and deliver a Service-as-Software.
Q: Can actions accept inputs and produce outputs?
A: Yes. Every action is designed like a function. It accepts a structured set of inputs (e.g., customer details, order ID) and returns a structured output (e.g., a confirmation status, a user ID). This makes them predictable, reliable, and easy to integrate into larger systems.