In the world of business automation, the promise is a seamless flow of logic, where tasks execute flawlessly from start to finish. But reality is messy. Networks flicker, APIs time out, and invalid data finds its way into the system. In an agentic workflow, where an AI is making decisions based on the outcome of its tools, a single unhandled error can derail an entire process.
At action.do, we believe in encapsulating logic to execute flawlessly. The core of this philosophy is the atomic action: a reliable, single-purpose function that forms the bedrock of your automations. But reliability isn't just about successful executions; it's also about how gracefully an action fails.
An action that fails silently or throws a cryptic error is a black box. An action that fails with clear, structured information is a valuable signal. This post dives into essential strategies for handling errors within your atomic actions to build truly resilient and intelligent agentic workflows.
Treating business logic as code means applying software engineering best practices, and error handling is paramount. For atomic actions, it's critical for several reasons:
Let's explore practical strategies using our process-payment action as an example. The goal is to evolve it from a simple function to a robust, production-ready building block.
This is the most fundamental error-handling pattern. Any logic that can fail, especially calls to external services like APIs or databases, should be wrapped in a try...catch block. This ensures that an unexpected exception doesn't crash your entire action.
The key is not just to catch the error, but to do something meaningful with it.
import { Do } from '@do-sdk/core';
const processPayment = Do.action('process-payment', {
inputs: { /* ... */ },
handler: async ({ inputs, context }) => {
try {
// Connect to a payment provider like Stripe
console.log(`Processing payment of $${inputs.amount} for ${inputs.customerId}`);
// const paymentResult = await stripe.charges.create(...); // This might fail!
// Return a structured success result
return { success: true, transactionId: `txn_${context.runId}` };
} catch (error) {
// Log the full error for debugging and observability
context.logger.error('Payment processing failed', {
error: error.message,
customerId: inputs.customerId
});
// Return a structured error that the calling workflow can understand
return {
success: false,
error: "PaymentGatewayError",
message: "The payment provider was unable to process the charge."
};
}
},
});
Here, we catch any potential failure, log the technical details for our own observability, and return a clean, structured error object. The calling agent or workflow now knows precisely why the action failed.
An action shouldn't even attempt its core logic if the data it receives is invalid. The action.do platform handles basic type validation for you based on your inputs definition. However, you should always add your own business logic validation.
For a payment action, an amount of -50 might be a valid number, but it's not a valid payment amount.
// Inside your action's handler...
handler: async ({ inputs, context }) => {
// 1. Business logic validation
if (inputs.amount <= 0) {
return {
success: false,
error: "InvalidInputError",
message: "Payment amount must be a positive number."
};
}
// 2. Proceed to core logic inside a try...catch block
try {
// ... payment gateway logic here
} catch (error) {
// ... error handling
}
}
By validating upfront, you prevent unnecessary API calls and provide immediate, clear feedback about invalid data. This is more efficient and easier to debug than discovering the data error deep inside a third-party library.
Idempotency is the property of an operation that allows it to be performed multiple times with the same input, yielding the same result without creating additional side effects.
Imagine an action fails due to a temporary network timeout after the payment was successfully processed but before your action received the success response. If the workflow automatically retries the action, you risk charging the customer twice.
To prevent this, design your action to be idempotent. A common pattern is to use a unique identifier.
// A more robust handler with an idempotency key
handler: async ({ inputs, context }) => {
// inputs now include: { customerId, amount, orderId }
// Check if a transaction for this orderId already exists
const existingTx = await db.findTransactionByOrderId(inputs.orderId);
if (existingTx) {
// The action was already successful, just return the existing result
return { success: true, transactionId: existingTx.id, status: 'already_processed' };
}
try {
// Proceed with payment processing, passing the orderId as an idempotency key
// const paymentResult = await stripe.charges.create({ ..., idempotency_key: inputs.orderId });
// ...
} catch (error) {
// ...
}
}
While the orchestration logic for when to retry belongs in a workflow.do, the action itself must be built to handle retries safely.
Not all failures are created equal. A workflow needs to know the difference between a temporary glitch and a permanent failure.
You can signal the error type in your structured response.
// Inside your catch block...
catch (error) {
context.logger.error('Payment processing failed', { error });
// Check the type of error from the payment gateway
if (error.type === 'StripeCardError') {
// This is a permanent failure related to the card itself
return {
success: false,
error: "PermanentFailure",
code: "CardDeclined",
message: error.message
};
}
// Assume other errors (like network issues) are transient
return {
success: false,
error: "TransientFailure",
code: "GatewayUnavailable",
message: "Could not connect to the payment provider. Please try again later."
};
}
This level of detail empowers your workflow.do to implement intelligent retry strategies, like retrying TransientFailure three times with exponential backoff while immediately stopping on a PermanentFailure.
By embracing these error-handling strategies, you transform a simple function into a resilient, observable, and composable atomic action. You're not just writing code; you're codifying a piece of your business logic, complete with the intelligence to handle the inevitable bumps in the road.
This is the essence of Business As Code. It's about building systems that are not only powerful in their success paths but also predictable and graceful in their failure modes.
Ready to build your first resilient action? Get started on the .do platform and turn your business logic into flawless, automated workflows.