In the world of automation, everything is perfect... until it isn't. A momentary network blip, a third-party API hitting its rate limit, a temporary database lock—these are the small, unavoidable gremlins that can bring a complex agentic workflow to a screeching halt. For developers, this often means wrapping every other line in a try/catch block and writing convoluted, manual retry loops that are a nightmare to maintain.
What if there was a better way? What if you could build resilience into the very foundation of your automation?
This is the core principle behind action.do. Instead of building fragile chains of function calls, you build robust systems from "atomic actions"—self-contained, observable, and inherently resilient units of work. Today, we'll dive into how action.do provides a masterclass in error handling and retries, turning your brittle scripts into bulletproof automation.
Imagine you're building a workflow to process new user sign-ups. A key step is sending a welcome email. A naive implementation might look like a simple function call to an email service.
// A simple, but fragile, function
async function sendWelcomeEmail(to: string) {
// What happens if this API call fails?
await emailProvider.send({ to, subject: "Welcome!", body: "..." });
}
This works fine in a perfect world. But what happens when emailProvider.send fails? The entire workflow might crash. You could wrap it in a try/catch, but what then? Do you log the error? Do you retry? How many times? How long do you wait between retries?
Suddenly, your simple function is buried in complex logic for handling transient failures, making it hard to read, test, and reuse.
action.do introduces the concept of an atomic action: the smallest, indivisible unit of work in a workflow. As our FAQs state, it's a self-contained function (like sending an email) with clear inputs and outputs, designed to be reliable, reusable, and observable.
By wrapping your logic in an Action, you're not just creating a function; you're creating a managed, enterprise-grade component. You define the what (the core business logic), and action.do provides the how (the structured execution, observability, and error handling).
Let's revisit our send-email example, now defined as a proper atomic action:
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)
console.log(`Sending email to ${input.to}...`);
const messageId = `msg_${Date.now()}`;
return {
messageId,
status: 'sent',
};
},
});
This is our baseline. Now, let's make it bulletproof.
The true power of action.do shines when you declaratively define your error handling strategy. Instead of writing manual retry loops, you simply tell the Action how it should behave in the face of failure.
Let's upgrade our sendEmail action to automatically retry on transient errors using an exponential backoff strategy—the gold standard for dealing with temporary API outages.
import { Action, BackoffStrategy } from '@do-co/core';
// Input and Output interfaces remain the same...
const sendCriticalEmail = new Action<SendEmailInput, SendEmailOutput>({
name: 'send-critical-email',
description: 'Sends a critical transactional email with automatic retries.',
// -- The magic happens here! --
retries: 3, // Attempt the action up to 4 times total (1 initial + 3 retries)
backoffStrategy: BackoffStrategy.Exponential, // Wait 1s, 2s, 4s between retries
handler: async (input, context) => {
// The handler is clean and focuses ONLY on the business logic.
// The action.do engine handles the retry loop.
console.log(`Attempt #${context.attempt}: Sending email to ${input.to}...`);
// Simulate a potential failure on the first two attempts
if (context.attempt <= 2) {
throw new Error("Email service API is temporarily unavailable.");
}
// On the third attempt, it succeeds.
console.log('Success! Email sent.');
const messageId = `msg_${Date.now()}`;
return { messageId, status: 'sent' };
},
});
Look at the difference. The handler code remains simple and focused on its primary job. The complex, error-prone retry logic is handled entirely by the action.do framework. This declarative approach offers tremendous benefits:
What happens if all retries fail? This is where action.do proves its worth as part of a larger agentic workflow. Because each action is an observable, managed component, a failure is not just an unhandled exception—it's a structured event.
When an action like send-critical-email ultimately fails, the action.do engine knows:
This structured failure information enables higher-level services like workflow.do or agent.do to take intelligent next steps, such as:
By building your automation on a foundation of atomic actions, you shift from writing fragile scripts to composing resilient systems. The ability to define granular, declarative error handling and retry policies for each indivisible unit of work is the key to creating robust, scalable, and automated business processes that you can trust.
Stop wrestling with endless try/catch blocks. Start building with resilience from the ground up.
Execute. Measure. Automate.