Skip to main content
Job: encode the way your team finds, enriches, routes, and exports customers so an agent or scheduled job can run it again. Start from the business decision, not the provider.

Example: custom waterfall

You have LinkedIn URLs. You want a verified work email. You want to try your preferred path first and stop when the answer is good enough. The workflow shape:
  1. Read each lead.
  2. Run a person-to-email workflow.
  3. Verify the result.
  4. Export rows that need review.
import { definePlay } from 'deepline';

type Lead = {
  lead_id: string;
  linkedin_url: string;
  company_domain: string;
};

export default definePlay('lead-email-waterfall', async (ctx, input: { leads: Lead[] }) => {
  const rows = await ctx
    .dataset('lead_email', input.leads)
    .withColumn('email', async (lead, rowCtx) => {
      const result = await rowCtx.tools.execute({
        id: 'person_email',
        tool: 'test_rate_limit',
        input: {
          key: lead.linkedin_url,
        },
        description: 'Find a verified work email for this lead.',
      });
      return result.status === 'SUCCESS' ? `lead@${lead.company_domain}` : null;
    })
    .run({ key: 'lead_id', description: 'Find one email per lead.' });

  return { rows };
});
The technical parts are there to keep the business process reliable:
CodeWhy it exists
lead_idlets Deepline retry one row without losing the list
person-linkedin-to-emailruns the reusable email-finding path
descriptionmakes logs readable for humans and agents

Example: route by customer type

You search companies once a week. Directors and VPs should not get the same follow-up.
import { definePlay } from 'deepline';

type Account = {
  domain: string;
  seniority: 'VP' | 'Director';
};

export default definePlay('route-qualified-accounts', async (ctx, input: { accounts: Account[] }) => {
  const rows = await ctx
    .dataset('qualified_accounts', input.accounts)
    .withColumn('next_step', async (account, rowCtx) => {
      if (account.seniority === 'VP') {
        return await rowCtx.tools.execute({
          id: 'vp_research',
          tool: 'test_rate_limit',
          input: { key: account.domain },
          description: 'Research this enterprise account.',
        });
      }

      return await rowCtx.tools.execute({
        id: 'director_research',
        tool: 'test_rate_limit',
        input: { key: account.domain },
        description: 'Research this midmarket account.',
      });
    })
    .run({ key: 'domain', description: 'Route each account to the right research path.' });

  return { rows };
});
This is where custom workflows pay off. The workflow reflects your GTM motion, not a generic enrichment recipe.

Example: weekly search to Salesforce

You want new matching companies every Monday, enriched and pushed to Salesforce.
import { definePlay } from 'deepline';

type SearchInput = {
  query: string;
  owner_email: string;
};

type Account = {
  domain: string;
  owner_email: string;
};

export default definePlay('weekly-new-accounts', async (ctx, input: SearchInput) => {
  const companies: Account[] = [
    { domain: 'example.com', owner_email: input.owner_email },
    { domain: 'acme.com', owner_email: input.owner_email },
  ];

  const enriched = await ctx.dataset('new_accounts', companies)
    .withColumn('signals', (account, rowCtx) =>
      rowCtx.tools.execute({
        id: 'company_research',
        tool: 'test_rate_limit',
        input: { key: `${input.query}:${account.domain}` },
        description: 'Research the new account.',
      }),
    )
    .withColumn('salesforce', (account, rowCtx) =>
      rowCtx.tools.execute({
        id: 'create_salesforce_task',
        tool: 'test_rate_limit',
        input: {
          key: `${account.domain}:${account.owner_email}`,
        },
        description: 'Create the owner follow-up task.',
      }),
    )
    .run({ key: 'domain', description: 'Research new accounts and create Salesforce tasks.' });

  return { rows: enriched };
});

When to write a workflow

Write one when you need:
  • a custom provider order
  • different paths for different customer types
  • a weekly or daily run
  • a webhook from another system
  • repeatable output for Salesforce, CSV, or dashboards
Use a prebuilt workflow when you just need the standard answer once.

Add guardrails

import { definePlay } from 'deepline';

type Account = {
  domain: string;
};

export default definePlay('nightly-refresh', async (ctx, input: { accounts: Account[] }) => {
  const rows = await ctx
    .dataset('accounts', input.accounts)
    .withColumn('company_signal', (account, rowCtx) =>
      rowCtx.tools.execute({
        id: 'company_signal',
        tool: 'test_rate_limit',
        input: { key: account.domain },
        description: 'Refresh one account signal.',
      }),
    )
    .run({ key: 'domain' });

  return { rows };
}, {
  billing: { maxCreditsPerRun: 25 },
});
Guardrails make background runs safer because they travel with the workflow.