---
title: "Create Company Column"
method: POST
path: "/companies/columns/create"
tags: ["Companies"]
---

# Create Company Column

`POST /companies/columns/create`

## Request

- Base URL: `https://be.graph8.com/api/v1`
- URL: `https://be.graph8.com/api/v1/companies/columns/create`
- Auth: HTTP bearer

Create a custom company column. Global if list_id is null, list-scoped otherwise.

Optionally pass an `enrichment` block to provision a companion waterfall
pipeline in the same request — STRONGLY RECOMMENDED so the UI 'Enrich'
button works out-of-the-box. Without it the column is created but isn't
enrichable until a pipeline is provisioned via
POST /enrichment/waterfall/configs.

## Headers

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

## Request body

- CreateCompanyColumnRequest: Request body for `POST /companies/columns/create`. Mirrors the shared `CreateColumnRequest` shape (`title`, `data_type`, `list_id`, `created_by`) plus an OPTIONAL `enrichment` block that, when supplied, provisions a companion waterfall pipeline in the same request — so the new column is immediately runnable via `POST /enrichment/waterfall/enrich`. Without the `enrichment` block the column is created, but it has no enrichment plan; the UI "Enrich" button (and `g8_enrich_contacts` in waterfall mode) will return 422 (`waterfall_config_missing`) until a pipeline is provisioned separately via `POST /enrichment/waterfall/configs`. Backwards compatible: callers that previously POSTed only `title` / `data_type` / `list_id` / `created_by` continue to work unchanged.
  - `title` string, required: Column title shown in the UI (e.g. 'Tier')
  - `data_type` 'text' | 'int' | 'numeric' | 'boolean': User-selectable dynamic-column data types. Every member must have a physical type in the repos' pg_type_map (contacts/companies CDP repos + enrichment DynamicColumnRepository: int→INTEGER, numeric→NUMERIC, text→TEXT, boolean→BOOLEAN) and belong to the user-owned family in lists/usecases/dynamic_column_write_contract.py — both already list these four. Write paths coerce values per type via shared.infrustructure.db.cdp.dynamic_column_values.coerce_dynamic_value.
  - `list_id` integer, nullable: If set, the column is list-scoped (only visible on that list). Required when `enrichment` is supplied — waterfall pipelines are list-scoped.
  - `created_by` string, email, required: Email of the user creating the column (for audit).
  - `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'].

## Response `201`

Successful Response

- ApiResponseCreateCompanyColumnResponse
  - `data` CreateCompanyColumnResponse, required: Response shape for `POST /companies/columns/create`. Mirrors the `Column` entity fields verbatim so existing callers reading `data.id` / `data.title` / `data.name` / `data.is_global` etc. keep working without code changes. Adds one new optional `enrichment` field that is populated when the request included an `enrichment` block.
    - `id` integer, nullable
    - `title` string, required
    - `data_type` 'text' | 'int' | 'numeric' | 'boolean': User-selectable dynamic-column data types. Every member must have a physical type in the repos' pg_type_map (contacts/companies CDP repos + enrichment DynamicColumnRepository: int→INTEGER, numeric→NUMERIC, text→TEXT, boolean→BOOLEAN) and belong to the user-owned family in lists/usecases/dynamic_column_write_contract.py — both already list these four. Write paths coerce values per type via shared.infrustructure.db.cdp.dynamic_column_values.coerce_dynamic_value.
    - `name` string, nullable
    - `object_id` string, nullable
    - `is_global` boolean: Computed: True when object_id is null/empty (global column).
    - `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
    - `already_existed` boolean: True when a column with this name already existed in this scope, so the existing column was returned. A different `data_type` is rejected with 409.
    - `existing_data_type` string, nullable: When `already_existed` is true: the existing column's data type.
    - `warnings` string[]: Non-blocking notices (e.g. a same-named org-wide column also shows on this list).
  - `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/2e7cfa0ca921?raw)
