In the world of software automation, complexity is the enemy. Grand, monolithic processes are difficult to build, impossible to debug, and fragile in production. The modern approach, and the core philosophy behind the .do ecosystem, is to build complex systems from simple, solid components.
Think of it like building with LEGO bricks. Each brick is simple, predictable, and performs a single function. By itself, it's just a block. But when you combine them, you can create anything you can imagine.
In our world, the action.do is that fundamental LEGO brick. It's an atomic action—the smallest, indivisible unit of work in your agentic workflow. It might send an email, update a database record, or call a third-party API. Because these actions are the foundation of your Services-as-Software, ensuring each one is flawless isn't just a good practice; it's essential.
When an entire automated service relies on a chain of actions, a single faulty link can cause the whole process to fail. Testing at the atomic level provides a level of quality assurance that is impossible to achieve when only testing the entire workflow.
Pinpoint Accuracy in Debugging: When a workflow fails, where do you start looking? If you know each action.do is individually tested and proven, you can immediately rule out huge portions of your logic. Testing in isolation means you can find and fix bugs faster, reducing downtime and frustration.
Confidence to Compose and Scale: Well-tested actions are trustworthy. You can confidently reuse a send-welcome-email action across dozens of different onboarding workflows, knowing it will perform reliably every single time. This confidence allows you to build more complex and valuable services because you trust the foundation they're built on.
Encapsulation and Reliability: Atomic actions are stateless and designed to do one thing well. Testing validates this design. It forces you to define clear inputs and expected outputs, ensuring your action doesn't have unintended side effects. This makes your entire system more predictable and robust.
Let's take the concept and make it concrete. Imagine we have the sendWelcomeEmail action from our documentation. It's designed to take a user's details and use an external service to send an email.
import { action } from '@do-sdk/core';
// Define an action to send a welcome email
const sendWelcomeEmail = action.create({
id: 'send-welcome-email',
description: 'Sends a welcome email to a new user.',
execute: async ({ email, name }) => {
// Your email sending logic via an external API
console.log(`Sending welcome email to ${name} at ${email}...`);
return { success: true, messageId: 'xyz-123' };
}
});
How do we test this without actually sending an email every time we run our tests? We use unit testing and mocking.
A unit test focuses on a single "unit" of code—our action.do—in isolation. Mocking is the technique of replacing external dependencies (like an email API) with a "mock" or fake version that we control during the test.
Here’s what a test file for our action might look like using a framework like Jest:
// __tests__/sendWelcomeEmail.test.ts
import { sendWelcomeEmail } from '../actions/sendWelcomeEmail';
import { emailService } from '../services/emailService'; // Our mocked email service
// Tell the test framework to use a mock version of our email service
jest.mock('../services/emailService');
describe('Action: send-welcome-email', () => {
// Test 1: The "Happy Path"
it('should call the email service and return a success response', async () => {
// Arrange: Set up the test conditions
const input = { email: 'jane.doe@example.com', name: 'Jane Doe' };
const mockApiResponse = { messageId: 'xyz-123' };
(emailService.send as jest.Mock).mockResolvedValue(mockApiResponse);
// Act: Execute the action
const result = await sendWelcomeEmail.execute(input);
// Assert: Verify the outcome
expect(emailService.send).toHaveBeenCalledWith({ to: 'jane.doe@example.com' });
expect(result).toEqual({ success: true, messageId: 'xyz-123' });
});
// Test 2: An "Edge Case" (e.g., failed API call)
it('should handle errors from the email service gracefully', async () => {
// Arrange: Simulate a failure from the external API
const input = { email: 'test@fail.com', name: 'Test Fail' };
(emailService.send as jest.Mock).mockRejectedValue(new Error('API Limit Reached'));
// Act & Assert: Expect the action's execution to fail (reject)
await expect(sendWelcomeEmail.execute(input)).rejects.toThrow('API Limit Reached');
});
});
By testing this way, we've verified three critical things:
Testing isn't just a technical task; it's a core component of treating your Business as Code. Every action.do you create and test is a verified, enterprise-grade capability. When you compose these actions into a workflow.do, you are not just scripting a process—you are engineering a reliable, automated service.
This rigorous, bottom-up approach to quality is what separates fragile scripts from robust, valuable Services-as-Software. When you can trust every brick in the wall, you can have confidence in the entire structure.
Ready to build your own reliable automations? Start by defining your core atomic actions, write tests to make them bulletproof, and compose them into powerful workflows that deliver real value.