Raw data is the starting point, not the destination. A new user signup with just an email address is a lead, but an incomplete one. To unlock its true value, you need enrichment: finding the user's name, their company, their location, and other critical data points. The traditional approach often involves building monolithic services that are difficult to update and a nightmare to maintain. When a new data source comes online or a business rule changes, developers face a high-risk, time-consuming redeployment.
There's a better way. By treating business logic as a set of atomic, composable functions, you can build data pipelines that are flexible, reusable, and incredibly powerful. This is the core philosophy behind function.do: encapsulating business logic into discrete, reusable functions that can be composed into automated services.
Let's walk through a practical guide to building a dynamic data enrichment pipeline using this modern, composable approach.
Imagine we have a simple input: a new user's email address. Our goal is to create a complete, enriched profile by orchestrating a series of simple, single-purpose functions:
Instead of writing one large, tangled piece of code, we will define each step as an atomic function.do.
Each function.do is an independent Agent that encapsulates a single piece of business logic. They are simple to write, easy to test, and infinitely reusable.
First, a simple function to ensure the email address is valid. This is a foundational utility that can be reused across countless services.
import { Agent, property } from '@do-sdk/core';
export class EmailValidator extends Agent {
  @property()
  async validate(email: string): Promise<{ isValid: boolean }> {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return { isValid: emailRegex.test(email) };
  }
}
Next, a function that queries our internal user database. This Agent isolates the logic for interacting with our own data stores.
import { Agent, property } from '@do-sdk/core';
// Mock User record structure
interface UserRecord {
  name: string;
  country: string;
}
export class UserLookup extends Agent {
  @property()
  async findByEmail(email: string): Promise<UserRecord | null> {
    // In a real scenario, this would query your database
    // const user = await db.users.find({ where: { email } });
    const mockDb = { "hello@example.com": { name: "Jane Doe", country: "USA" } };
    return mockDb[email] || null;
  }
}
This function calls a third-party API (like Clearbit or Hunter) to fetch company information. It neatly encapsulates the external API call, including authentication and error handling (simplified here for brevity).
import { Agent, property } from '@do-sdk/core';
interface CompanyInfo {
  companyName: string;
  industry: string;
}
export class CompanyEnricher extends Agent {
  @property()
  async enrichFromEmail(email: string): Promise<CompanyInfo | null> {
    // This would call a third-party API, e.g., fetch(`https://api.someprovider.com?email=${email}`)
    const domain = email.split('@')[1];
    const mockApi = { "example.com": { companyName: "Example Inc.", industry: "Technology" } };
    return mockApi[domain] || null;
  }
}
Finally, a pure business logic function. It takes a completed profile and applies a set of rules to generate a lead score. This logic is now a quantifiable, versioned asset.
import { Agent, property } from '@do-sdk/core';
export class LeadScorer extends Agent {
  @property()
  async calculateScore(profile: { country: string; industry: string }): Promise<{ score: number }> {
    let score = 50; // Base score
    if (profile.country === "USA") score += 10;
    if (profile.industry === "Technology") score += 20;
    return { score };
  }
}
With our atomic functions defined, we can now create an orchestrator—a function.do that composes the others into a seamless pipeline. The power of the .do platform is that any function can easily and natively invoke others.
This orchestrator function defines the flow of our entire data enrichment service.
import { Agent, property } from '@do-sdk/core';
// In a real environment, the .do platform makes other agents discoverable and callable.
// We'll represent that concept here for clarity.
declare const agents: {
  emailValidator: EmailValidator;
  userLookup: UserLookup;
  companyEnricher: CompanyEnricher;
  leadScorer: LeadScorer;
};
export class ProfileEnrichmentPipeline extends Agent {
  
  @property()
  async run(email: string): Promise<any> {
    // 1. Validate - A simple guard clause
    const { isValid } = await agents.emailValidator.validate(email);
    if (!isValid) {
      throw new Error('Invalid email provided.');
    }
    // 2. Enrich - Run lookups in parallel for performance
    const [internalUser, companyInfo] = await Promise.all([
      agents.userLookup.findByEmail(email),
      agents.companyEnricher.enrichFromEmail(email)
    ]);
    if (!internalUser) {
      return { status: 'Failed', message: 'User not found.' };
    }
    // 3. Combine - Create the unified profile
    const enrichedProfile = { ...internalUser, ...companyInfo };
    // 4. Score - Apply final business logic
    const { score } = await agents.leadScorer.calculateScore(enrichedProfile);
    // 5. Return the final, enriched result
    return {
      ...enrichedProfile,
      leadScore: score,
      status: 'Success'
    };
  }
}
By building our pipeline this way, we've gained several massive advantages:
You've successfully transformed a complex process into a clear, manageable, and scalable service built from simple, reusable blocks. This is the future of building business logic: atomic functions, deployed as discoverable API endpoints, ready to be composed into infinite possibilities.
Ready to stop building brittle monoliths? Encapsulate your business logic on function.do and start composing powerful, automated services today.