# Workflow Composition

<!-- BEGIN GENERATED: guide-map -->
## Guide map

Read only the section needed for the next decision. Line ranges are exact
for this generated runtime target.

| Section | What it covers | Lines |
| --- | --- | --- |
| [Purpose](#purpose) | Scope, ownership, and the central operating question. | 27-44 |
| [Use This When](#use-this-when) | Tasks and conditions that belong in this guide. | 45-57 |
| [Do Not Use This For](#do-not-use-this-for) | Boundaries and the correct routes for excluded work. | 58-69 |
| [Mental Model](#mental-model) | Concepts and state boundaries needed for correct operation. | 70-87 |
| [Preflight](#preflight) | State, authority, inputs, and readiness to verify before execution. | 88-118 |
| [Risk And Side Effects](#risk-and-side-effects) | Effects, approvals, cost, privacy, and retry boundaries. | 119-155 |
| [Canonical Operating Pattern](#canonical-operating-pattern) | The normal end-to-end execution sequence. | 156-178 |
| [Play Pattern Selection](#play-pattern-selection) | Operating guidance and contracts for this section. | 179-191 |
| [Core SDK Surfaces](#core-sdk-surfaces) | Exact source-backed callable contracts owned by this guide. | 192-199 |
| [Data Contracts](#data-contracts) | Required identities, fields, evidence, handles, and durable shapes. | 200-266 |
| [Examples](#examples) | Representative usage patterns and code. | 267-326 |
| [Verification](#verification) | Evidence required before reporting success. | 327-342 |
| [Troubleshooting](#troubleshooting) | Failure diagnosis and corrective routes. | 343-354 |
| [Related References](#related-references) | Adjacent boundaries and when to cross them. | 355-373 |
<!-- END GENERATED: guide-map -->

## Purpose

Use this guide to design or refactor a multi-step GTM Workflow Play before the
mechanics are owned by an existing workflow root. It answers: what should the
play do, which SDK capability owns each step, what data moves between steps,
what must be visible in the final Play/report, and where human approval is
required.

Composition is a planning and architecture reference. It should make the
repeatable operating contract obvious before code is written: source universe,
suppression, enrichment, qualification, outputs, metrics, reports, side-effect
gates, and snapshot visibility.

Once a workflow root exists, `workflow_operations.md` owns running, repairing,
replaying, and run-local reporting for that root. `workflow_schedules.md` owns
canonical Play revision/output sync and scheduling. `workflow_sources.md` owns
source windows, checkpoints, and object memory.

## Use This When

- The task spans discovery, CRM, enrichment, qualification, reporting, and
  optional sequence/source activation.
- The user asks what play/workflow to build or how an existing play should be
  structured.
- A one-off SDK flow is becoming recurring, row-heavy, resumable, or
  operator-reviewed.
- You need to choose the durable step, data, output, and approval shape before
  writing `workflow.py`.
- The frontend Play should show real steps, run metrics, outputs, credits,
  artifacts, and side-effect ids instead of only a final table.

## Do Not Use This For

- Running, repairing, replaying, syncing, or inspecting a specific generated
  workflow root; use `workflow_operations.md`.
- Source window/checkpoint/cursor semantics; use `workflow_sources.md`.
- Job/artifact paging and handle semantics; use `artifacts_jobs.md`.
- Final Play UI build and Svelte/custom view details; use `workflow_reports.md`.
- CRM schemas, object associations, active customer/deal suppression fields, or
  provider readiness; use `crm.md`.
- Exact audience discovery, enrichment, LinkedIn, web, host, sender, sequence,
  publish, send, or purchase mechanics; use the narrower provider references.

## Mental Model

Growth capabilities are the provider/runtime truth. Typed SDK calls such as
`vf.sources.*`, `vf.crm.*`, `vf.linkedin.*`, `vf.web.*`, `vf.clay.*`, and
`vf.host.crunchbase.*` own provider execution, policy, credits, batching, and
artifacts. Workflow acquisition is local checkpointed object memory around
those calls. Workflow transforms remain normal Python.

Workflow composition answers “what steps should exist and what data should flow
between them?” It does not replace the child guides that own exact SDK
signatures, source freshness, report rendering, or sequence activation.

Design plays around evidence and operator review. Every important provider job,
artifact, row table, metric, credit-bearing call, report output, and side-effect
result should be recorded so the Play UI can explain what happened after the
run. The generated Play UI/report is an output of the workflow, not a substitute
for durable local run state or synced workspace run-output history.

## Preflight

Before designing or changing a workflow play:

1. Resolve the exact workspace and local workflow root if one exists. If
   multiple roots could match, list their paths, names, latest run ids, and
   one-line purpose before choosing.
2. Inspect existing `config.json`, `workflow.py`, reports, and final tables
   before reusing prior assumptions.
3. Name the source universe and open the owning reference: CRM objects,
   provider audience discovery, places, web, LinkedIn, host/browser, or an
   existing artifact.
4. Identify required suppression before expensive work: active customer,
   associated deal, already-in-sequence audience, personal/education domains,
   missing identity, geography, company type, budget, or any customer-specific
   exclusion.
5. Decide the output contract up front: final table, audit tables, report/Play
   UI views, artifact files, sidecars, metric ids, and snapshot fields.
   The report source must retain every analyzed row. Qualification, rejection,
   parking, and ranking are explicit row fields and filters, not destructive
   top-N truncation. An operational preview or budget limit never defines final
   business membership.
6. Decide the run mode boundary: preview must be safe for review; batch and
   production may perform declared side effects only when the workflow policy
   and approval gates allow them.
7. Estimate provider jobs, enrichment volume, credits, and paging limits before
   launching broad acquisition.

Stop if the play depends on a CRM property, provider field, sequence source, or
report output that has not been verified against the relevant SDK/source doc.

## Risk And Side Effects

| Operation class | Examples | Composition rule |
| --- | --- | --- |
| Local transform | RowSet shaping, joins, scoring, suppression, CSV export | Safe after input shape is sampled. Record counts and reasons. |
| Local workflow write | `ctx.save_rows`, `ctx.replace_rows`, `ctx.save_output`, metrics | Expected in workflow runs. Keep table names, keys, and output kinds explicit. |
| Platform read | CRM schema/object reads, sequence audience reads, artifact inspection | Print workspace, object/source ids, counts, and sample rows when relevant. |
| Provider or credit job | source jobs, enrichment, qualification, web/LinkedIn/provider work | Preview inputs, limits, and expected fields before start; preserve `job_id`, `artifact_id`, credits, and row counts. |
| Report/Play output | local report bundle, Play UI, CSV/JSON sidecars | Build for review and snapshot visibility. Do not confuse report rendering with sequence activation. |
| Sequence/source staging | `ctx.stage_rows_to_sequence_source`, artifact/source staging | Batch/production only, declared as `sequence_stage`, deduped against existing audience, and verified by source run/import ids. |
| Customer-visible mutation | sequence publish, sends, LinkedIn actions, sender/channel changes | Keep as explicit top-level calls in the owning guide. Require immediate human confirmation. |
| Billing or purchase | mailbox/domain purchase, plan changes, large unclear spend | Require immediate human confirmation and print price/quantity/workspace before call. |

Preview mode may spend credits when the user asked for real provider evidence,
but it must not stage, publish, send, purchase, or perform customer-visible
mutations. Batch and production may stage or promote rows only when the step
declares the effect, the workflow policy allows it, and idempotency/claim gates
protect reruns.

Workflow step effects use one closed vocabulary:

| Effect | Use it for |
| --- | --- |
| `provider_job` | Provider-backed acquisition or lookup calls, including immediate provider reads. |
| `artifact_job` | Artifact-backed jobs and checkpointed artifact collection. |
| `credit_usage` | Calls that can consume workspace credits. |
| `enrichment` | Contact, company, or row enrichment and qualification. |
| `sequence_stage` | Reversible sequence/source audience staging. |
| `sequence_publish` | Publishing a sequence or staged audience. |
| `direct_send` | Email or LinkedIn communication. |
| `purchase` | Mailbox, domain, plan, or other purchase operations. |
| `browser_write` | Browser-backed external writes such as posting. |

Declare the applicable effects on `@workflow.step(effects=(...))`, or call
`ctx.require_effects(...)` before provider work. Names such as `source_job`,
`send`, `sender_setup`, and `domain_purchase` are not workflow effects.

## Canonical Operating Pattern

1. State the business goal, final operator decision, final table, and final
   report/Play UI output.
2. Pick the source universe and source-of-truth fields. Open the owning source
   and CRM/provider references before writing acquisition code.
3. Put suppression and routing before people discovery, enrichment,
   qualification, personalization, or activation whenever possible.
4. Define stable row identities: contact, company, source object, artifact,
   sequence audience, and side-effect ids.
5. Use deterministic Python for crisp joins, grouping, thresholds, dedupe,
   sampling, and column mapping. Do not replace semantic ICP/title/company
   decisions with brittle local heuristics.
6. Use typed SDK calls for provider discovery, CRM, enrichment, qualification,
   LinkedIn, web, host, sequence, sender, and billing work.
7. Record every material step as rows, outputs, metrics, operations, jobs,
   artifacts, and audit tables so the Play UI can show actual inner workflow
   steps, not just the final table.
8. Render a report/Play UI and use local snapshot or dry-run sync for review.
   Real Play sync or registration is a reviewable platform write.
9. Stage/publish/send/purchase only through explicit approval-gated steps owned
   by the sequence/sender/billing references.

## Play Pattern Selection

| Intent | Source/acquisition | Transform and evidence | Output and gate | Read next |
| --- | --- | --- | --- | --- |
| Places/reviews research | Places provider | Reviews, ratings, geos, domains | Report, optional contact enrichment | `places.md`, `workflow_reports.md` |
| Company evidence report | Domains, web search, host/browser evidence | Page extracts, screenshots, evidence URLs, scores | Report with evidence gallery | `web.md`, `host_browser.md`, `workflow_reports.md` |
| CRM segment expansion | CRM company/contact objects | Suppression, association joins, people discovery | Final rows, audit CSVs, optional sequence stage | `crm.md`, `audience_discovery.md`, `sequences.md` |
| LinkedIn engagement research | Posts, profiles, companies | Evidence, identity resolution, enrichment, qualification | Outreach prep or report | `linkedin.md`, `enrichment.md`, `row_operations.md` |
| Signal-based SDR list | Jobs, funding, product news, incubators, CRM usage | Software/company/geography checks, suppression, people search, email enrichment | Final contact table and signal report | `audience_discovery.md`, `web.md`, `crm.md`, `enrichment.md` |
| Clay/Crunchbase artifact play | Public API job or provider export | Local artifact join, canonical ids, audit rows | Report or staged artifact | `clay.md`, `crunchbase.md`, `artifacts_jobs.md` |
| Copy/personalization review | Source rows plus CRM/profile/company context | Variable coverage, personalization, copy checks | Sequence copy/source review | `copy.md`, `sequence_authoring.md` |
| Activation-safe sequence prep | Artifact/source rows | Dedupe against target audience, sandbox/verify, mapping | Sequence source run with confirmation gate | `artifacts_jobs.md`, `sequences.md`, `senders.md` |

## Core SDK Surfaces

<!-- BEGIN GENERATED: core-sdk-surfaces -->
| Function | Use When | Inputs | Output And Handles To Preserve | Side Effect | Source |
| --- | --- | --- | --- | --- | --- |
| `vf.workflows.define` | Define a durable Python workflow whose steps can compose local logic and typed SDK calls. | <strong>Required</strong>: `name` (str; required): Human-readable workflow name used in the local run ledger.; `store` (SqliteLocalStore; required): Workflow-local store returned by vf.local_store.sqlite(...).<br><strong>Optional</strong>: none | `workflow` payload returned by the SDK operation. Inspect the documented function reference and `raw`/dictionary fields when debugging provider-specific data.<br><strong>Common next calls</strong>: `vf.local_store.sqlite` | local_file_write | installed SDK: `verbiflow/workflows.py:407-440` |
<!-- END GENERATED: core-sdk-surfaces -->

## Data Contracts

Keep contracts explicit and generic. A workflow can use customer-specific
columns, but the composition should name durable identifiers, fields, outputs,
and categories before implementation:

| Contract | Required design decision |
| --- | --- |
| Play identity | Slug/name, owner, purpose, workspace/org, local root label, report path, and expected run cadence. |
| Source contract | Source universe, source-of-truth fields, source window, freshness/checkpoint semantics, provider/artifact/job ids. |
| Row identity | Contact, company, source object keys, redirect/canonical domain rules, duplicate and merge policy. |
| Suppression/audit | Suppression inputs, stage where suppression happens, excluded-row audit tables, reason codes. |
| Enrichment | Required input fields for each enrichment call and output fields needed downstream. |
| Qualification | Deterministic checks vs semantic checks, model/provider ownership, rubric/id, evidence fields, pass/fail reason. |
| Metrics and credits | Dynamic metrics as `{label, value, unit, scope}`; provider job counts; credit usage or usage handles; rows in/out at each step. |
| Outputs | Final table, table previews, CSV/JSON sidecars, status report, Play UI output, artifacts, links, diagnostics. |
| Side effects | Sequence/source ids, sender/channel ids, approval keys, idempotency keys, source run/import ids, publish/send/purchase gates. |
| Stable ownership | Canonical account key, fixed roster keys, assignment namespace/version, explicit owner-migration policy, and the rule that contacts inherit account ownership. |
| Snapshot visibility | Actual step names, run mode/status, active/draft/final state, output ids, job/artifact ids, and report/Play UI URL. |

Typical row fields:

- contact identifiers: `contact_email`, `contact_linkedin_url`, or equivalent
  stable profile URL;
- contact variables: `contact_first_name`, `contact_last_name`, `contact_job_title`;
- company identifiers: `company_domain`, `company_linkedin_url`, `company_name`;
- provenance: source window, source job/artifact ids, evidence URLs, and
  suppression reasons;
- activation ids: sequence id, source node id, ingress id, source run id, and
  lead import id.

Names are not enough for runtime identity. `contact_first_name` and
`contact_last_name` are useful variables, but they cannot satisfy enrollment
identity by themselves.

For recurring account distribution, assign the company/account before expanding
to contacts. Use `ctx.assign_stable_groups(...)` with a versioned namespace.
Existing account owners never move on an ordinary rerun; only new accounts are
placed, and all contacts at that account inherit the stored owner. Changing the
roster is an explicit migration, not a hashing side effect.

For sequence staging, use the sequence references for exact payloads. Artifact
staging must use full mapping payloads when the SDK cannot infer the artifact
schema. The current value type vocabulary is `string`, `number`, `boolean`,
`date`, or `enum`.

```python
column_mapping = {
    "contact_email": {
        "source_column_name": "email",
        "source_column_index": 0,
        "category": "contact_identifier",
        "label": "Email",
        "order": 0,
        "value_type": "string",
    },
    "contact_first_name": {
        "source_column_name": "first_name",
        "source_column_index": 1,
        "category": "contact_identifier",
        "label": "First Name",
        "order": 1,
        "value_type": "string",
    },
}
```

## Examples

Use local Python for deterministic shaping over already-acquired rows. Start
provider jobs only after the source/audience/enrichment references confirm the
right input fields and limits.

```python
import verbiflow as vf

raw_rows = [
    {"email": "ada@example.com", "company_linkedin_url": "https://www.linkedin.com/company/example/"},
    {"email": "grace@example.com", "company_linkedin_url": "https://www.linkedin.com/company/example/"},
]
signups = vf.from_records(raw_rows, label="recent_signups")
accounts = (
    signups
    .derive("company_domain", vf.email_domain("email"))
    .filter(~vf.is_personal_domain("company_domain"))
    .group_by("company_domain")
    .agg(user_count=vf.count())
    .filter(vf.col("user_count") >= 2)
)
print("company preview:", accounts.sample(10).to_list())

company_urls = sorted(
    {
        str(row["company_linkedin_url"])
        for row in signups.to_list()
        if row.get("company_linkedin_url")
    }
)
print("company URLs ready for people discovery:", len(company_urls))
```

If the same flow must recur, create an editable workflow root, use
`workflow_operations.md` for run/repair/report, then hand canonical Play sync
and scheduling to `workflow_schedules.md`:

```python
store = vf.local_store.sqlite("./state/workflow.sqlite")
workflow = vf.workflows.define("company-expansion", store=store)

@workflow.step()
def load_inputs(ctx: vf.workflows.WorkflowContext) -> None:
    ctx.save_rows(
        "companies",
        [{"company_domain": "example.com", "company_name": "Example"}],
        key="company_domain",
    )
    ctx.record_metric("candidate_companies", value=1, label="Candidate companies")
    ctx.save_output("status_report", {"message": "Loaded one candidate company."})
```

For parallel provider/qualification chunks, start all provider jobs first, then
wait with a bounded concurrency primitive. `pipelined=True` in a workflow does
not parallelize arbitrary local Python loops. `vf.jobs.wait_many(...)` returns
job states; inspect every `status`, `finished`, and `failed` value before
consuming artifacts. Use `vf.jobs.result(item).wait(require_success=True)` when
downstream logic requires fail-fast success semantics.

## Verification

- The composition has a named final table, audit tables, report/Play UI output,
  and diagnostic output.
- Every source, enrichment, qualification, and provider step records row counts,
  dynamic metrics, credits/usage where available, `job_id`, and `artifact_id`.
- Every step that should appear in the Play UI is a workflow step or recorded
  operation, not hidden inside an unreported helper.
- Source freshness/checkpoint assumptions route to `workflow_sources.md`.
- CRM schema/suppression assumptions route to `crm.md`.
- Sequence/source/billing actions route to explicit approval-gated steps and
  preserve sequence/source/import ids.
- Workspace Play sync is described as a platform write, not as provider
  execution or sequence activation.
- Preview, batch, and production modes have distinct side-effect policies.

## Troubleshooting

| Symptom | First check | Fix |
| --- | --- | --- |
| Provider job starts with zero inputs | Inspect the table feeding the job and required provider fields. | Add the missing source/enrichment step or fail fast before job start. |
| Final Play shows only high-level steps | Check whether inner steps are separate workflow steps or recorded operations. | Split important units into steps or call `ctx.record_metric`, `ctx.save_output`, `ctx.record_job`, and `ctx.record_artifact`. |
| Counts differ between report, Play history, and sequence | Compare final table rows, staged rows, source run/import ids, and existing audience suppression counts. | Make the final table and staging input explicit; run Play sync or confirm the daemon has synced completed evidence after staging. |
| Old rows survive after source contract changes | Check whether the workflow used `save_rows` against a stable key after changing source fields. | Reset/replay the affected step or use `replace_rows` for run-exact derived outputs. |
| Batch run did not stage rows | Check step `effects`, workflow policy, approval/claim result, existing audience table, and stageable count. | Run with an allowed `sequence_stage` policy only after preview and approval; do not hide staging behind env flags. |
| Credit usage is missing from the Play | Check provider job outputs and Growth observed usage handles. | Record job ids, artifact ids, provider counts, and explicit metric outputs for credits/usage. |
| Parallel loop is still serial | Look for `for chunk: start job; wait` patterns. | Start all jobs first, then use bounded `vf.jobs.wait_many(...)` or workflow step concurrency. |

## Related References

| If you are doing this | Read next | Why |
| --- | --- | --- |
| Choosing the right workflow child guide | `workflows.md` | Parent router for workflow operation, source, composition, queues, and reports. |
| Running or repairing an existing root | `workflow_operations.md` | Owns root resolution, run/resume/retry/reset, report build, and activation boundaries. |
| Syncing or scheduling the composed Play | `workflow_schedules.md` | Owns canonical revision/output sync and later/recurring execution. |
| Designing source windows or checkpointing | `workflow_sources.md` | Owns recurring source freshness, object memory, cursor keys, and replay behavior. |
| Building the final Play UI/report | `workflow_reports.md` | Owns tables, filters, row drawers, charts/maps/graphs, Svelte Play app, and report bundles. |
| Preserving job/artifact handles | `artifacts_jobs.md` | Owns `PendingJob`, `JobResult`, waiting, paging, artifacts, and exports. |
| Shaping already-acquired rows | `row_operations.md` | Owns deterministic RowSet/CSV transforms and row-level qualification. |
| Discovering people/accounts/signals | `audience_discovery.md` | Owns provider-backed audience discovery and source strategy. |
| Reading CRM fields and associations | `crm.md` | Owns schema, properties, company/contact/deal associations, and suppression facts. |
| Enriching emails/domains/profiles | `enrichment.md` | Owns canonical enrichment inputs, resolver fields, and output identity fields. |
| Designing sequence sources/copy | `sequence_authoring.md` | Owns sequence source/copy structure before activation. |
| Staging, publishing, or sending sequences | `sequences.md` | Owns sequence staging, promotion, publish/send semantics, and confirmation gates. |
| Sender, mailbox, or sending-domain actions | `mailboxes.md` | Owns sender readiness, mailbox/domain purchase, and their approval requirements. |
| Subscription, credits, checkout, or billing reconciliation | `billing.md` | Owns the authoritative billing view and billing actions. |
| Browser-backed provider evidence | `host_browser.md`; use `web.md` for explicit URL page capture | Owns runtime-appropriate saved/managed sessions, rendered DOM evidence, screenshots, and provider-session actions without exposing raw credentials. |
