In today's fast-paced digital landscape, business processes are growing more complex. From onboarding a new customer to generating a quarterly financial report, these workflows often involve multiple steps, systems, and teams. Translating this intricate business logic into reliable, scalable, and maintainable software is one of the biggest challenges for modern engineering teams.
Enter Services-as-Software.
The core philosophy of service.do is to empower you to define any business capability as a robust, versioned software component. We believe in Business-as-Code—transforming operational logic into simple, reusable APIs.
But what does one of these services actually look like? Let's dissect the fundamental anatomy of a .do service: the Inputs, the Outputs, and the powerful Context object that ties it all together.
At the heart of every .do service is a simple piece of code that defines its behavior. There's no complex framework to learn or servers to configure. You define what the service needs, what it produces, and the logic to get from one to the other.
Let's explore a customer-onboarding service as our guide.
import { Service, type Context } from '@do/sdk';
// Define the input and output schemas for your service
interface OnboardingInput {
name: string;
email: string;
plan: 'free' | 'pro' | 'enterprise';
}
interface OnboardingOutput {
customerId: string;
welcomeEmailSent: boolean;
status: 'complete' | 'failed';
}
// Create a new Service instance with your business logic
export default new Service<OnboardingInput, OnboardingOutput>({
name: 'customer-onboarding',
description: 'Onboards a new customer, creating an account and sending a welcome email.',
async run(input: OnboardingInput, context: Context): Promise<OnboardingOutput> {
console.log(`Starting onboarding for ${input.email} on plan ${input.plan}`);
// Business logic goes here.
// e.g., call user.create, email.send, billing.setup agents
const customerId = `cust_${context.invocationId}`;
return {
customerId,
welcomeEmailSent: true,
status: 'complete',
};
},
});
This snippet contains everything the service.do platform needs to deploy a fully functional, scalable API service. Let's break it down.
Before any business process can start, it needs information. A .do service formalizes this with a strongly-typed Input schema.
interface OnboardingInput {
name: string;
email: string;
plan: 'free' | 'pro' | 'enterprise';
}
This OnboardingInput interface acts as a strict contract. It declares that to run the customer-onboarding service, you must provide a name, an email, and a specified plan.
Why is this important?
The Input defines the public-facing API for your business logic. It's the front door to your service.
Once a process is complete, you need to know the result. The Output schema defines the shape of the data the service will return upon successful completion.
interface OnboardingOutput {
customerId: string;
welcomeEmailSent: boolean;
status: 'complete' | 'failed';
}
Just like the Input, the Output schema provides a reliable contract. Any system that calls this service knows exactly what to expect in return: a new customer ID, a boolean confirming the welcome email status, and a final completion status.
This predictability is key for building complex systems. You can chain services together with confidence, knowing that the output of one service perfectly matches the expected input of the next. This is a cornerstone of building a powerful Agentic Workflow.
If Inputs and Outputs are the contract, the async run() method is the fulfillment of that contract. This is where your business logic resides.
async run(input: OnboardingInput, context: Context): Promise<OnboardingOutput> {
console.log(`Starting onboarding for ${input.email} on plan ${input.plan}`);
// Business logic goes here.
// e.g., call user.create, email.send, billing.setup agents
const customerId = `cust_${context.invocationId}`;
return {
customerId,
welcomeEmailSent: true,
status: 'complete',
};
},
Inside this function, you have access to the validated input data and can perform any actions necessary. This could include:
The beauty of the .do platform is its serverless nature. You don't worry about servers, scaling, or infrastructure. You simply write the logic that delivers business value, and we handle the rest.
You may have noticed the second argument to the run method: context: Context.
The Context object is your bridge to the service.do platform. It's not part of your business data (like Inputs and Outputs) but is instead a collection of metadata and utilities provided by the execution environment.
In our example, we use context.invocationId to generate a unique customer ID. This ID is guaranteed to be unique for every single run of the service, perfect for tracing, logging, and creating idempotent operations.
The Context object can provide access to:
It's the "magic" that elevates your simple business logic into a managed, secure, and observable component of your software ecosystem.
By combining these four elements—Inputs, Outputs, the run method, and the Context object—you have everything you need to model a business process as software.
This structured approach transforms messy, implicit business workflows into explicit, manageable, and powerful API Services.
Ready to turn your business operations into scalable software? Visit service.do to learn more and deploy your first service in minutes.