At its core, an action.do is a beautifully simple concept: a single, repeatable task encapsulated into a powerful, API-callable building block. The handler function is the heart of this block, containing the specific logic you want to execute. But to build truly robust, scalable, and reusable automations, we need to look beyond the handler.
A resilient atomic action isn't just about what it does; it's about how it defines its requirements, how it accesses its environment, and how it behaves when things inevitably go wrong.
This deep dive will explore three critical components that elevate a simple action into a cornerstone of your agentic workflows:
The inputs object is the front door to your action. It's a formal declaration of the data your action needs to do its job. Thinking of it as a strict API contract is the first step toward creating modular, predictable automations.
Let's look at the send-welcome-email example:
import { action } from '@do-sdk/core';
export const sendWelcomeEmail = action({
name: 'send-welcome-email',
description: 'Sends a welcome email to a new user.',
inputs: {
to: { type: 'string', required: true, description: 'The recipient email address.' },
name: { type: 'string', required: true, description: 'The name of the new user.' }
},
handler: async ({ inputs, context }) => {
// ... handler logic
},
});
By defining to and name with their type and required status, you gain several powerful advantages:
Key takeaway: Treat your inputs schema as a non-negotiable contract. It's your first line of defense and your best tool for creating clear, reusable business-as-code components.
If inputs are the specific data for a single run of an action, context is the data about the environment in which the action is running. This separation is crucial for security and portability.
What kind of information belongs in context?
Let's enhance our sendWelcomeEmail action to use an API key from the context:
// ... inside the action definition
handler: async ({ inputs, context }) => {
const { to, name } = inputs;
// Safely access secrets from the context
const mailerApiKey = context.secrets.MAILER_API_KEY;
if (!mailerApiKey) {
throw new Error('Mailer API key is not configured in the context.');
}
// Email sending logic would use mailerApiKey
console.log(`Sending welcome email to ${name} at ${to} using configured mailer.`);
return { success: true, messageId: 'xyz-123' };
},
By abstracting secrets into the context, your action's logic becomes cleaner and more secure. It doesn't need to know how to get the key, only that it will be there. This makes testing easier (you can inject a mock context) and your actions more portable across different environments.
"Atomic" means an action either succeeds completely or fails completely—there are no partial states. In a distributed system, network calls can fail, APIs can be down, and data can be invalid. Your action's handler must account for this reality.
Proper error handling within an action is what signals failure to the wider workflow, allowing the orchestrator to make intelligent decisions like:
The mechanism is simple: use a try...catch block and throw an error to signal failure.
// ... inside the action definition
handler: async ({ inputs, context }) => {
const { to, name } = inputs;
const mailerApiKey = context.secrets.MAILER_API_KEY;
try {
//
// Hypothetical email sending logic
// const mailer = new MailerClient(mailerApiKey);
// const result = await mailer.send({ to, name, ... });
//
console.log(`Sending welcome email to ${name} at ${to}`);
// A simulated failure
if (name === 'Faily McFailface') {
throw new Error('Simulated API connection timeout.');
}
return { success: true, messageId: 'xyz-123' };
} catch (error) {
console.error(`Failed to send welcome email to ${to}. Reason: ${error.message}`);
// Re-throwing the error is crucial. This tells the action.do
// platform that the execution has failed.
throw new Error(`Email sending failed: ${error.message}`);
}
},
When you throw an error, the action.do platform catches it, marks the run as "failed," and logs the error message. This explicit failure is far more valuable than silently failing or returning a { success: false } payload, as it plugs directly into the orchestration and monitoring layer.
By mastering inputs, context, and error handling, you transform your actions from simple scripts into enterprise-grade, resilient components.
These three pillars are what make action.do more than just a function runner. They provide the structure needed to truly practice business-as-code, building complex, agentic workflows from simple, atomic, and powerful blocks.
Q: What is an 'atomic action' in the context of action.do?
A: An atomic action is the smallest, indivisible unit of work in a workflow. It represents a single, well-defined task, like 'send an email' or 'create a user record', ensuring that it either completes successfully or fails entirely, without partial states.
Q: How is an action.do different from a full workflow?
A: An action.do represents a single task. A workflow is a collection of one or more actions orchestrated to achieve a larger business process. Actions are the building blocks; workflows are the blueprints that connect them.
Q: Can I reuse actions across different workflows?
A: Absolutely. Actions are designed to be modular and reusable. You can define an action once, like 'generate-report', and call it from any number of different workflows, promoting DRY (Don't Repeat Yourself) principles in your automations.
Q: What kind of logic can I put inside an action's handler?
A: The handler can contain any Node.js/TypeScript logic. This includes making API calls to third-party services, performing data transformations, interacting with databases, or executing any custom business logic required to complete the task.