Building a subscription service is a cornerstone of modern digital business. But anyone who's tried knows the truth: it's surprisingly complex. You're not just charging a card; you're managing customer lifecycles, handling failed payments, processing upgrades, and reacting to webhooks—all while ensuring data integrity. A single bug in a monolithic script can lead to billing errors, frustrated customers, and a maintenance nightmare.
There's a better way. Instead of building a fragile, all-in-one system, what if you could assemble your service from a series of small, robust, and independent building blocks?
This is the philosophy behind action.do: defining your business logic as a set of atomic, single-responsibility actions. Let's explore how to build a complete Stripe subscription service using this powerful Business-as-Code approach.
Imagine a single function, createNewSubscription(user, plan). Inside, it tries to:
What happens if step 4 fails due to a database connection issue? The customer now has a subscription in Stripe and is being billed, but your application doesn't know about it. This is a classic state inconsistency problem that is difficult to debug and resolve.
With action.do, we break down the complex process into granular, self-contained tasks. Each Atomic Action is like a transaction: it either succeeds completely or fails entirely, leaving no messy partial states. It has one job and does it flawlessly.
This approach transforms our fragile script into a resilient, maintainable, and scalable workflow.
The first building block is creating a customer in Stripe. We define an action named create-stripe-customer. Its single responsibility is to take user details and create a corresponding customer object in Stripe.
import { Dō } from '@do-sdk/core';
const dō = new Dō({ apiKey: 'YOUR_API_KEY' });
// This action is defined once in your .do repository
// and executed from anywhere in your application.
const customerResult = await dō.action.execute({
name: 'create-stripe-customer',
input: {
email: 'jane.doe@example.com',
name: 'Jane Doe',
localUserId: 'user-12345',
},
});
// The action returns a clear success status and the new ID
// { success: true, transactionId: '...', output: { stripeCustomerId: 'cus_XXXX' } }
console.log(customerResult);
This action is now a reusable component in your system. Whether a user signs up via your web app or is created by an admin, you call the same, reliable action.
With a Stripe Customer ID, we can now create the subscription. We define a new action, create-stripe-subscription, that takes the customer ID and a price ID.
const subscriptionResult = await dō.action.execute({
name: 'create-stripe-subscription',
input: {
// Use the output from the previous action
stripeCustomerId: customerResult.output.stripeCustomerId,
stripePriceId: 'price_1L2m3n4o5pABC...', // Your plan's price ID from Stripe
},
});
// { success: true, transactionId: '...', output: { subscriptionId: 'sub_YYYY' } }
console.log(subscriptionResult);
By separating these two steps, we gain resilience. If the subscription creation fails (e.g., due to a declined card), the customer object in Stripe still exists. The user can try again with a different payment method without having to re-enter all their information.
Stripe communicates state changes—like successful payments or subscription cancellations—via webhooks. These are a perfect fit for agentic workflows driven by atomic actions. Instead of a messy if/else block in your webhook handler, you can execute a specific action for each event type.
Let's define an action to handle a successful invoice payment. Its job is to find the relevant user in our database and update their subscription period.
// Inside your API endpoint that receives the Stripe 'invoice.paid' webhook
const event = request.body; // The event payload from Stripe
const handlePaymentResult = await dō.action.execute({
name: 'handle-invoice-paid',
input: {
stripeCustomerId: event.data.object.customer,
subscriptionEndsAt: event.data.object.period_end,
},
});
// A successful execution confirms the webhook was processed.
console.log(handlePaymentResult);
// { success: true, transactionId: 'txn_ghi_789' }
Similarly, you can have an action for customer.subscription.deleted or invoice.payment_failed. Each action is simple, focused, and easy to test. If handle-invoice-paid fails, action.do's built-in logging and retry mechanisms ensure you can process it again without losing the event.
While action.do executes individual tasks, its true power is realized when these actions are chained together in a workflow.do. You can visually or programmatically define a sequence:
This is Workflow Automation at its best. The entire business process is explicitly defined, monitored, and made resilient, turning complex backend logic into a clear sequence of manageable steps.
Ready to stop wrestling with complex integrations and start building flawless, automated workflows? Define your first atomic action today.
Q: What is an 'atomic action' in the context of .do?
A: An atomic action is a self-contained, single-responsibility task that either completes successfully or fails entirely, without partial states. Think of it as the smallest indivisible unit of work in your workflow, like 'send-email', 'create-user', or 'charge-card'.
Q: Can actions be chained together?
A: Absolutely. While action.do executes a single action, it's designed to be a fundamental building block within a larger workflow.do. You compose complex processes by sequencing these atomic actions to create robust Services-as-Software.
Q: How is action.do different from a serverless function?
A: While similar, action.do is designed specifically for agentic workflows. It provides built-in orchestration, state management, logging, and security context that serverless functions require you to build and manage yourself. It's a higher-level abstraction for business logic.