> ## Documentation Index
> Fetch the complete documentation index at: https://deepline.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Write Workflows

> Turn your GTM process into a repeatable Deepline workflow. Use it to pilot on a few rows, verify the output, and scale the workflow safely.

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.

```ts theme={null}
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:

| Code                       | Why it exists                                       |
| -------------------------- | --------------------------------------------------- |
| `lead_id`                  | lets Deepline retry one row without losing the list |
| `person-linkedin-to-email` | runs the reusable email-finding path                |
| `description`              | makes 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.

```ts theme={null}
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.

```ts theme={null}
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

```ts theme={null}
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.
