---
title: "Create Fields Batch"
method: POST
path: "/fields/batch"
tags: ["Fields"]
---

# Create Fields Batch

`POST /fields/batch`

## Request

- Base URL: `https://be.graph8.com/api/v1`
- URL: `https://be.graph8.com/api/v1/fields/batch`
- Auth: HTTP bearer

Create many custom columns (contacts or companies) in ONE request.

Backed by a single DB transaction with ONE view rebuild for the whole batch
— the efficient path for setting up a list with dozens of columns at once.
The single-column `POST /fields` is unchanged; this is purely additive.

Validation is fail-fast: data_types, duplicate titles, and every `enrichment`
block are checked BEFORE any column is created, so a bad request creates
nothing. Column creation itself is atomic (all columns or none).

Per-column `enrichment` blocks are applied AFTER the columns commit; if a
companion enrichment config fails for one column, that column still exists
and its `results[]` entry reports `ok=false` with the error — the rest of
the batch is unaffected (mirrors the single endpoint's column-then-config
ordering, but per-field instead of failing the whole request).

## Headers

- `Authorization` string, nullable
- `X-Target-Org-Id` string, nullable

## Request body

- CreateFieldsBatchRequest: Create MANY custom columns in one request. All columns are created in a single DB transaction with a SINGLE view rebuild — at 40-50 columns this replaces 40-50 view recreations with one.
  - `fields` BatchFieldItem[], required: Columns to create (1-100).
    - `title` string, required: Column title, e.g. 'Job Level'
    - `data_type` string: Column data type: 'text', 'int' (whole number), 'numeric' (decimal) or 'boolean' (yes/no). Unknown values 400.
    - `enrichment` CreateFieldEnrichmentInput: Optional enrichment-pipeline block attached at column-create time. When supplied, `POST /fields` creates a companion `enrichment_configs` row in the same request so the new column is immediately runnable via `POST /enrichment/waterfall/enrich`. Without this block the column exists but has no enrichment plan — calling /waterfall/enrich on it returns 422 (`waterfall_config_missing`). Currently only `type='waterfall'` is supported; AI / formula enrichment will follow once the rest of the agent surface lands.
      - `type` string: Pipeline kind. Only 'waterfall' is supported today.
      - `name` string, nullable: Pipeline name. Defaults to `<column title> Enrichment` if omitted.
      - `field_to_enrich` string, nullable: Target field identifier. Defaults to the new column's slug.
      - `target_field` string, nullable: Kind of data this column holds — CONTACT_DIRECT_PHONE, CONTACT_MOBILE_PHONE, CONTACT_WORK_EMAIL, CONTACT_LINKEDIN_URL, COMPANY_PHONE, ... or a friendly synonym ('phone', 'mobile', 'email', 'linkedin'). When set, the pipeline is built from the server's per-column TEMPLATE so each provider runs its CORRECT action for that data kind (e.g. leadmagic `mobile_finder` for a phone, NOT `email_finder`) — the same actions the web UI's 'Add column -> Waterfall' picks. Combine with `providers[]` to keep only those providers from the template. Mutually exclusive with `steps`. When omitted, the legacy `providers[]` email-finder shortcut is unchanged.
      - `skip_existing_values` boolean: If true, skip rows where the target field is already populated.
      - `providers` string[], nullable: Ordered list of provider names. Without `target_field`, server maps each to its default email-finder action (legacy). With `target_field`, filters the column template to these providers (correct per-field action preserved). Mutually exclusive with `steps`.
      - `steps` WaterfallStepInput[], nullable: Full caller-supplied step list. Persisted verbatim. Mutually exclusive with `providers`.
        - `id` string, nullable: Step identifier. Auto-generated (`step_<provider>_<uuid8>`) if omitted.
        - `provider` string, required: Provider name: graph8, hunter, prospeo, dropcontact, apollo, ...
        - `action` string, required: Provider action: lookup_person, find_email, enrich_person, ...
        - `action_display_name` string, nullable: Human-readable label shown in UI. Auto-titled from `action` if omitted.
        - `output_field` string, nullable: Dotted path the worker writes into (e.g. 'contact.work_email').
        - `input_mapping` object: Maps provider input names to graph8 column identifiers (CONTACT_*).
        - `output_mapping` object: Optional output field remap (deprecated — prefer `output_field`).
        - `config` object: Provider-specific options (e.g. apollo `reveal_phone_numbers=true`).
        - `order` integer: 0-indexed execution order.
        - `enabled` boolean
        - `is_system_provider` boolean: True = use graph8-managed credentials + charge credits. False = use org BYOK.
        - `stop_on_success` boolean
        - `confidence_threshold` number
        - `max_retries` integer
      - `email_verification` EmailVerificationInput: Email verification config attached to a waterfall pipeline. Mirrors the canonical `EmailVerificationConfigDto` in `enrichment/interfaces/fastapi/waterfall_dtos.py` so the dev-API and the UI persist an identical settings shape (the CF worker reads `settings. email_verification.*` regardless of which surface saved the pipeline).
        - `enabled` boolean: Enable post-enrichment email verification
        - `provider` 'zerobounce' | 'hunter' | 'icypeas' | 'leadmagic': Verification provider. Charges credits when use_system_credentials=True.
        - `use_system_credentials` boolean: Use graph8-managed API keys (charges credits). False = use org's BYOK keys.
        - `accept_catchall` boolean: Treat catch-all responses as valid (domains that accept any address).
        - `valid_statuses` string[]: Verification statuses to accept as valid. Typical: ['valid'] or ['valid','catch-all'].
  - `entity` string: Target entity for ALL columns: 'contacts' or 'companies'
  - `list_id` integer, nullable: Shared list scope for all columns. REQUIRED if ANY field carries an `enrichment` block — waterfall pipelines are list-scoped.

## Response `201`

Successful Response

- ApiResponseCreateFieldsBatchResponse
  - `data` CreateFieldsBatchResponse, required
    - `entity` string, required
    - `list_id` integer, nullable
    - `total_requested` integer, required
    - `total_created` integer, required: Columns returned by this call, INCLUDING ones that already existed (unchanged meaning). `total_already_existed` says how many of them were existing columns.
    - `results` BatchFieldResult[], required
      - `title` string, required
      - `id` integer, nullable
      - `name` string, nullable
      - `ok` boolean
      - `error` string, nullable: Set when the column was created but its companion enrichment config failed.
      - `enrichment` CreateFieldEnrichmentResponse: Echo of the companion enrichment config created by `enrichment` block.
        - `column_id` string, required: column_id slug to pass to /enrichment/waterfall/enrich
        - `config_id` string, required
        - `name` string, required
        - `type` string, required
        - `providers` string[]
        - `email_verification` object, nullable
      - `is_global` boolean: True when this column was created org-GLOBAL (no list_id was supplied) and will therefore appear on EVERY list in the org. Pass `list_id` to scope columns to one list.
      - `list_id` integer, nullable: The list this column is scoped to, or null when it is org-global.
      - `already_existed` boolean: True when this column already existed (same name, same scope), so the existing column was returned instead of a new one.
      - `existing_data_type` string, nullable: When `already_existed` is true: the existing column's data type.
      - `warnings` string[]: Non-blocking notices for this column (e.g. same name exists org-wide).
    - `total_already_existed` integer: How many requested columns already existed and were returned as-is.
  - `pagination` PaginationMeta
    - `page` integer, required: Current page number (1-indexed)
    - `limit` integer, required: Items per page
    - `total` integer, required: Total number of items
    - `has_next` boolean, required: Whether there are more pages
    - `next_cursor` string, nullable: Opaque cursor for the next page; null when there are no more items. Pass it back as the `cursor` query param to fetch the following page without computing page numbers.
    - `total_available` integer, nullable: Search endpoints only: how many records really match, when known. Can be larger than `total`, which is capped at the 10,000 results that can be paged. For company search this is a distinct-company estimate. Null when unknown; absent outside search.
    - `total_is_lower_bound` boolean, nullable: Search endpoints only: true when more records match than `total` shows (the result set is larger than the 10,000 results that can be paged). Absent outside search.
    - `notes` string[], nullable: Search endpoints only: plain-language notes about the result, e.g. that more records match than can be paged, or that a reversed `between` range was put in order.

## Other responses

- `422`: Validation Error

---

[API](https://skmtc.dev/graph8/apis/graph8-developer-api.md) · [All operations](https://skmtc.dev/graph8/apis/graph8-developer-api/llms.txt) · [OpenAPI document](https://skmtc.dev/graph8/apis/graph8-developer-api/revisions/89fc11143b70?raw)
