User onboarding. It’s the critical first impression your product makes. Yet, it’s often a fragile, multi-step process cobbled together with scripts, manual tasks, and hope. A user signs up, and a cascade of events needs to happen: create a database record, send a welcome email, add them to your CRM, and maybe ping the sales team on Slack. When this chain breaks—and it will—you're left scrambling to fix a monolithic script or figuring out which manual step was missed.
There's a better way. Instead of building a fragile chain, what if you could build with resilient, Lego-like blocks?
This is the core philosophy behind the .do platform. We break down complex processes into their smallest, most fundamental parts: Atomic Actions. In this post, we'll show you how to leverage this "business as code" approach to build a robust, scalable, and easy-to-maintain user onboarding workflow in less time than it takes to brew a pot of coffee.
Before we build, let's understand the core concept. An atomic action is the smallest, indivisible unit of work in a system. It’s a self-contained, executable task designed to do one thing perfectly.
You don't build a complex, monolithic "onboarding script." Instead, you define a series of small, independent, and reusable actions. Then, you compose them into a powerful agentic workflow that orchestrates these actions to achieve a larger business goal. This approach transforms brittle scripts into a resilient, observable, and scalable system.
Let's start by defining the building blocks for our onboarding workflow. Using the .do TypeScript SDK, we can easily turn each distinct task into a programmable atomic action.
This is the classic first touchpoint. We'll define an action that takes a user's name and email and sends them a warm welcome.
import { Action } from '@do-co/agent';
// Define a new atomic action to send a welcome email
const sendWelcomeEmail = new Action('send-welcome-email', {
title: 'Send Welcome Email',
description: 'Sends a standardized welcome email to a new user.',
input: {
to: { type: 'string', required: true },
name: { type: 'string', required: true },
},
async handler({ to, name }) {
console.log(`Sending email to ${to}...`);
// Actual email sending logic (e.g., using an SMTP service) would go here
const message = `Welcome to the platform, ${name}!`;
console.log(message);
return { success: true, messageId: `msg_${Date.now()}` };
},
});
Notice how this action is completely self-contained. It clearly defines its inputs (to, name) and has a single responsibility. It doesn't know or care about where the user data came from or what happens next.
Next, we need to get this new lead into our CRM for the sales and marketing teams.
// Define an action to add a user to the CRM (e.g., Salesforce, HubSpot)
const addUserToCrm = new Action('add-user-to-crm', {
title: 'Add User to CRM',
description: 'Creates a new contact record in the company CRM.',
input: {
email: { type: 'string', required: true },
firstName: { type: 'string', required: true },
userId: { type: 'string', required: true },
},
async handler({ email, firstName, userId }) {
console.log(`Adding ${email} to CRM with internal ID ${userId}...`);
// API call to your CRM service would go here
const crmId = `crm_${Date.now()}`;
return { success: true, crmId };
},
});
Finally, let's give the sales team a heads-up about the new signup directly in their Slack channel.
// Define an action to post a message to a Slack channel
const notifySalesOnSlack = new Action('notify-sales-on-slack', {
title: 'Notify Sales on Slack',
description: 'Posts a message to the #new-signups channel in Slack.',
input: {
userName: { type: 'string', required: true },
userEmail: { type: 'string', required: true },
},
async handler({ userName, userEmail }) {
const slackMessage = `🎉 New User Signup! Please welcome ${userName} (${userEmail}).`;
console.log(`Sending to Slack: "${slackMessage}"`);
// API call to the Slack Webhook API would go here
return { success: true, messageSent: true };
},
});
With just these three simple files, we have created the robust, reusable building blocks for our entire onboarding process. Each one can be tested, versioned, and improved independently.
Now for the magic. Actions are the building blocks; the workflow is the blueprint that puts them together. On the .do platform, you can visually or programmatically define a workflow that chains these actions.
A simplified agentic workflow triggered by a "New User Signup" event might look like this:
This orchestration is now your official onboarding process. If the welcome email fails, the .do platform can automatically retry it without affecting the CRM or Slack notifications. If you want to add a fourth step, like "Provision a Sandbox Account," you simply define a new atomic action and add it to the workflow. You never have to touch the existing, working actions.
This isn't just a different way to write a script; it's a fundamentally more resilient approach to task automation.
By breaking down complexity into its atomic parts, you build systems that are stronger, more flexible, and easier to understand. This is the future of building agentic systems and complex business automation.
Ready to stop wrestling with fragile scripts? Define your first atomic action and start building bulletproof workflows today on the .do platform.
What is an atomic action in the .do platform?
An atomic action is the smallest, indivisible unit of work in a workflow. It's a self-contained, executable task—like sending an email, querying a database, or calling an external API. Each action is designed to do one thing well.
How are actions different from workflows?
Actions are the individual steps, while workflows are the orchestration of multiple actions in a specific sequence or logic. You build complex workflows by composing simple, reusable actions together.
Can I create my own custom actions?
Absolutely. The .do SDK allows you to define custom actions with specific inputs, outputs, and business logic. This transforms your unique business operations into reusable, programmable building blocks for any workflow.