You've grasped the core idea behind action.do: encapsulate a single piece of business logic into a flawless, executable component. It’s the fundamental "Business As Code" principle that lets you build powerful automations. Basic actions, like the processPayment example, are easy to create and immediately useful.
But as your systems grow and your AI agents become more autonomous, the real power lies in how you design these actions for resilience, scale, and clarity. Moving beyond simple, happy-path functions is what separates a fragile script from an enterprise-grade agentic workflow.
This post will explore advanced concepts that will elevate your atomic action design, ensuring your automations are robust, maintainable, and truly intelligent.
In a distributed system—especially one orchestrated by an AI agent that might retry tasks—things can and will run more than once. A network blip, a temporary API outage, or a workflow crash can trigger a retry. If your action isn't idempotent, this can lead to duplicate charges, multiple emails, or corrupted data.
Idempotency means that performing the same operation multiple times has the same effect as performing it once.
For an atomic action, this means designing it to handle repeat calls gracefully. The best practice is to use an idempotencyKey passed in the inputs. The action's first step should be to check if an operation with this key has already been successfully completed.
Let's enhance our processPayment action:
import { Do, a } from '@do-sdk/core';
import { db } from './your-database-client'; // pseudocode
const processPayment = Do.action('process-payment-idempotent', {
inputs: {
customerId: a.string(),
amount: a.number(),
idempotencyKey: a.string(), // A unique key for this specific request
},
handler: async ({ inputs, context }) => {
// 1. Check if this operation was already completed
const existingTx = await db.findTransaction({ key: inputs.idempotencyKey });
if (existingTx && existingTx.status === 'success') {
console.log(`Idempotency key hit. Returning existing transaction: ${existingTx.transactionId}`);
return { success: true, transactionId: existingTx.transactionId };
}
// 2. If not, proceed with the core logic
console.log(`Processing new payment of $${inputs.amount} for ${inputs.customerId}`);
// ... payment gateway logic here
const transactionId = `txn_${context.runId}`;
// 3. Store the result with the idempotency key *before* returning
await db.saveTransaction({
key: inputs.idempotencyKey,
transactionId,
status: 'success',
});
return { success: true, transactionId };
},
});
By building for idempotency, you make your agentic workflows resilient to the unpredictable nature of distributed computing.
A simple throw new Error() is a black box. It tells the orchestrating workflow that something failed, but not what or why. Was it a temporary network issue that can be retried? Or a permanent validation error that requires aborting the process?
Advanced atomic actions don't just fail; they provide structured, actionable error information. This allows the parent workflow.do to make intelligent decisions.
Define a clear contract for your action's output, for both success and failure.
const processPayment = Do.action('process-payment-robust-errors', {
// ... inputs remain the same
handler: async ({ inputs, context }) => {
try {
// payment gateway logic...
// Let's simulate a failure from the provider
const paymentResponse = { success: false, code: 'card_declined' };
if (!paymentResponse.success) {
// This is a permanent failure. Don't retry.
return {
success: false,
errorType: 'permanent',
errorCode: paymentResponse.code,
message: 'The payment card was declined.'
};
}
return { success: true, transactionId: `txn_${context.runId}` };
} catch (error) {
if (error.type === 'api_connection_error') {
// This is a network issue. Retrying might work.
return {
success: false,
errorType: 'transient',
errorCode: 'gateway_unavailable',
message: 'Could not connect to the payment gateway.'
};
}
// For all other unexpected errors
return { success: false, errorType: 'unknown', message: error.message };
}
},
});
Now, the orchestrating workflow.do can inspect the output and decide its next move:
As stated in our FAQs, actions shouldn't call other actions. That's the job of a workflow.do. This enforces a clean separation of concerns and is the key to building complex systems that are easy to understand and debug.
An advanced action is designed with this composition in mind. It does one thing and does it perfectly, without any knowledge of the larger process it's part of.
Consider an "order processing" scenario.
The Naive Approach (One Giant Action):
A single processNewOrder action that:
This is a monolithic, brittle design. If the email service fails, did the payment go through? It's hard to test, debug, and reuse.
The Composable action.do Approach:
Create four distinct, atomic actions:
A workflow.do then orchestrates them, passing the output of one as the input to the next. This Services-as-Software model gives you:
The handler's context object is your gateway to the action.do platform's ecosystem, and a key differentiator from standard serverless functions. While our basic example showed context.runId, it holds much more. Advanced action design leverages this context for enterprise-grade features.
// Inside your action handler
const stripeApiKey = context.secrets.get('STRIPE_API_KEY');
const stripe = new Stripe(stripeApiKey);
context.logger.info('Starting payment processing', { customer: inputs.customerId });
// ... logic ...
context.logger.warn('A transient error occurred.', { errorType: 'transient' });
Designing atomic actions is about more than just writing a function. It's about architecting self-contained, reliable, and observable units of business logic. By embracing advanced concepts like idempotency, structured error handling, composability, and context-awareness, you transform simple scripts into the robust building blocks required for sophisticated AI-powered agentic workflows.
Ready to build your first advanced atomic action? Get started on action.do and turn your business logic into flawless, scalable, and intelligent components.