You've grasped the core concept of action.do: encapsulating a single, repeatable task into a powerful, API-callable atomic action. You've seen the "Hello, World!" of automation—a simple action to send a welcome email. It's clean, simple, and powerful.
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 },
name: { type: 'string', required: true }
},
handler: async ({ inputs, context }) => {
// Basic email sending logic
console.log(`Sending welcome email to ${name} at ${to}`);
return { success: true };
},
});
But what happens when you move from simple scripts to mission-critical business processes? How do you handle secrets, manage transient network errors, or validate complex data?
This is where action.do shines. It provides the advanced constructs needed to build robust, production-ready automations. Let's move beyond the basics and explore the configurations that transform a simple task into a resilient component of your agentic workflow.
Hardcoding credentials in your action handler is a major security risk. action.do solves this by providing a secure context object, which is automatically injected into your handler. This is the right way to manage API keys, database connection strings, and other sensitive information.
The Problem: Your send-welcome-email action needs an API key for your email service provider.
The Solution: Store the key as a secret in the .do environment and access it via context.secrets.
import { action } from '@do-sdk/core';
// Assume an email client is available
import { emailClient } from '../lib/email';
export const sendWelcomeEmail_v2 = action({
name: 'send-welcome-email-secure',
description: 'Sends a welcome email using a secure API key.',
inputs: {
to: { type: 'string', required: true },
name: { type: 'string', required: true }
},
handler: async ({ inputs, context }) => {
const { to, name } = inputs;
// Access the secret securely from the context
const apiKey = context.secrets.EMAIL_SERVICE_API_KEY;
// Initialize the client or use the key in your API call
const messageId = await emailClient.send({
apiKey,
to,
subject: `Welcome, ${name}!`,
body: 'We are so glad to have you on board.'
});
return { success: true, messageId };
},
});
By decoupling your logic from your secrets, your atomic action becomes more secure, portable, and easier to manage across different environments (dev, staging, prod).
In any distributed system, failure is inevitable. A third-party API might be down, or a network glitch might interrupt a database connection. A truly robust automation doesn't give up on the first try.
The Problem: Your action to generate-report calls an external analytics service that sometimes fails with a temporary server error (e.g., 503 Service Unavailable).
The Solution: Define a retry policy directly in your action configuration. Let action.do handle the complexity of backoff strategies and error evaluation for you.
import { action } from '@do-sdk/core';
import { analyticsApi } from '../lib/analytics';
export const generateReport = action({
name: 'generate-report',
description: 'Generates a monthly sales report from the analytics API.',
inputs: {
month: { type: 'string', required: true },
},
// Advanced configuration: Add a retry policy
retry: {
maxAttempts: 3,
backoff: 'exponential', // 'exponential' or 'linear'
// Only retry on specific, transient errors
shouldRetry: (error) => error.statusCode >= 500,
},
handler: async ({ inputs, context }) => {
const { month } = inputs;
// This API call will now be automatically retried on failure
const reportUrl = await analyticsApi.generate({
month,
apiKey: context.secrets.ANALYTICS_API_KEY
});
return { success: true, reportUrl };
},
});
This simple configuration adds immense resilience to your task automation, ensuring that temporary hiccups don't derail your entire workflow. This is a core principle for building reliable agentic workflows.
As your automations grow, so does the complexity of the data they handle. The inputs schema is more than just documentation; it's a powerful validation engine.
The Problem: An action to create-user-in-crm requires a structured user object with specific fields and values. Passing invalid data could corrupt your CRM records.
The Solution: Use a detailed object schema with nested properties, enums, and other constraints. action.do will automatically validate incoming data against this schema before your handler is ever executed, preventing bad data from entering your business logic.
import { action } from '@do-sdk/core';
import { crmClient } from '../lib/crm';
export const createUserInCrm = action({
name: 'create-user-in-crm',
description: 'Creates a new user record in the CRM with validation.',
inputs: {
user: {
type: 'object',
required: true,
properties: {
email: { type: 'string', format: 'email' },
firstName: { type: 'string', minLength: 1 },
lastName: { type: 'string' }, // optional
role: { type: 'string', enum: ['admin', 'editor', 'viewer'] }
}
}
},
handler: async ({ inputs, context }) => {
// You can trust that inputs.user is well-formed here.
const { user } = inputs;
const crmId = await crmClient.createUser({
credentials: context.secrets.CRM_CREDENTIALS,
userData: user,
});
return { success: true, crmId };
},
});
This "business-as-code" approach ensures data integrity at the edge, simplifying your handler logic and making your entire system more predictable and robust.
To manage and debug workflows effectively, you need visibility. console.log isn't enough. The context object provides essential metadata for building observable systems.
The Problem: When an action fails within a complex, multi-step workflow, it's hard to trace its execution and correlate logs.
The Solution: Use the structured logger and unique identifiers provided in the context.
import { action } from '@do-sdk/core';
export const processPayment = action({
name: 'process-payment',
description: 'Processes a payment via a payment gateway.',
inputs: {
amount: { type: 'number' },
source: { type: 'string' }
},
handler: async ({ inputs, context }) => {
// Use the structured logger instead of console.log
context.logger.info('Starting payment processing...');
// Log with additional context for better debugging
context.logger.debug({
amount: inputs.amount,
invocation: context.invocationId
});
try {
// payment processing logic...
context.logger.info('Payment processed successfully.');
return { success: true, transactionId: 'txn-123...' };
} catch (error) {
// The error log will be automatically correlated via invocationId
context.logger.error('Payment processing failed.', { error });
throw error;
}
},
});
By using these context properties, you create a clear audit trail for every single atomic action, making debugging and monitoring your workflow automation a systematic process, not a guessing game.
As you can see, action.do is designed with production scale in mind. By leveraging advanced configurations for secrets, retries, validation, and logging, you move beyond simple task automation. You start building a library of enterprise-grade, reusable components.
These features are the key to implementing a true business-as-code strategy, where your critical processes are defined as code that is as robust, testable, and reliable as your main application.
Ready to build more powerful and resilient agentic workflows? Explore the action.do documentation and start transforming your automations from brittle scripts into powerful, atomic building blocks.