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

# Field mapping

> Map source-platform custom fields to Ekso process fields with a migration.fields.yaml file.

Source platforms have custom fields. Ekso has process fields. The migrator bridges the two with a `migration.fields.yaml` file you author by hand.

This page is the canonical format reference. Per-source pages link here for examples specific to each source.

## What this solves

Every issue tracker has its own custom-field model:

* **Jira** uses `customfield_10026` numeric IDs that vary per tenant.
* **Linear** has a single first-class `estimate` decimal.
* **Azure DevOps** uses `Custom.<FieldName>` reference names.
* **Zendesk** has typed `ticket_field` IDs.
* **Gemini** has named custom fields.

Without a field map, every source custom field falls through to `DataItem.Meta` losslessly — the data is preserved, but not searchable or filterable as a real field. With a field map, each source field maps to an Ekso process field that *is* searchable, filterable, and editable like any other field.

## How auto-create works

When you run `apply --field-map migration.fields.yaml`, the Apply layer's `ProcessFieldApplier` runs **before any item writes**:

1. It reads your YAML.
2. For each entry, it queries the destination `DataProcess` (the one you passed to `--process`) for fields with that name.
3. If the field exists, it's a no-op.
4. If the field doesn't exist, it calls `POST /api/field` to create it on the process. The field's `kind` (text / decimal / picker / toggle) and any picker values come from the YAML.
5. If field creation fails (permission denied, validation error), `apply` exits with code `6` *before* writing any items.

This means after `apply` succeeds, your destination process has every field your YAML promised, populated with the source-platform values. You can audit and remove fields post-migration via the admin UI.

## YAML format

```yaml theme={null}
<source>:
  <source-field-id>: { ekso: <EksoFieldName>, kind: <kind>, ...kind-specific }
```

Top-level keys are source names: `jira`, `linear`, `devops`, `zendesk`, `gemini`. The migrator reads only the section matching the source you're migrating.

## Process mapping (`_process`)

Most sources use a Type / Issue Type / Work Item Type to distinguish Bugs from Stories from Tasks. Ekso's equivalent is the [Process](/data-model/data-process). The optional `_process` key under each source maps inbound type names to Ekso process slugs:

```yaml theme={null}
jira:
  _process:
    Bug:      Defect
    Story:    Task
    Task:     Task
    Epic:     Enhancement
    Sub-task: Task

devops:
  _process:
    Bug:        Defect
    Task:       Task
    User Story: New Feature
    Issue:      Task
```

When `apply` walks the cache, each item's source type is looked up in `_process` to pick the destination process. If a type isn't listed (or the source has no type concept at all, like Linear), the item falls back to the default process passed on the command line as `--process`.

<Note>
  **Pre-flight validation.** Before any items are written, `apply` verifies that **every** process slug it could need actually exists on the destination Ekso tenant — both the `--process` default and every value under any `_process` block. If any one is missing, `apply` aborts at the start with exit code `6` (validation). Half-applied migrations against a half-complete process catalogue are worse than no migration.
</Note>

### Field kinds

| `kind`    | Stores                      | Example source fields                     |
| --------- | --------------------------- | ----------------------------------------- |
| `text`    | Free-form string            | Jira sprint name, Linear project label    |
| `decimal` | Numeric value               | Story points, estimates, custom durations |
| `picker`  | One value from a fixed list | Severity, Risk, Phase                     |
| `toggle`  | True / false                | "Has-PR-attached", "Customer-facing"      |

### `text` example

```yaml theme={null}
jira:
  customfield_10018: { ekso: Sprint, kind: text }
```

The Apply layer creates a text field named `Sprint` on the destination process if it doesn't exist, then writes the source value into it on every item.

### `decimal` example

```yaml theme={null}
jira:
  customfield_10026: { ekso: StoryPoints, kind: decimal }

linear:
  estimate: { ekso: StoryPoints, kind: decimal }

devops:
  Microsoft.VSTS.Scheduling.StoryPoints: { ekso: StoryPoints, kind: decimal }
```

All three sources can map their estimate field to the same Ekso `StoryPoints` field — handy when you're consolidating multiple sources into one Ekso tenant.

### `picker` example

Picker fields are the most expressive. Map source values to Ekso picker values explicitly:

```yaml theme={null}
jira:
  customfield_10031:
    ekso: Severity
    kind: picker
    picker:
      Critical: P0
      High:     P1
      Medium:   P2
      Low:      P3
```

The Apply layer creates a `Severity` picker field on the process with the four destination values (`P0`, `P1`, `P2`, `P3`), then maps each Jira severity to its Ekso counterpart on every item. Source values not in the `picker` mapping fall back to `DataItem.Meta`.

### `toggle` example

```yaml theme={null}
zendesk:
  has_attachments: { ekso: HasAttachments, kind: toggle }
```

Zendesk's `has_attachments` boolean becomes an Ekso toggle field. Source values of `true` / `1` / `"yes"` / `"true"` (case-insensitive) become Ekso `true`; everything else becomes `false`.

## Worked examples per source

### Jira

```yaml theme={null}
jira:
  customfield_10026: { ekso: StoryPoints, kind: decimal }
  customfield_10018: { ekso: Sprint,      kind: text }
  customfield_10031:
    ekso: Severity
    kind: picker
    picker:
      Critical: P0
      High:     P1
      Medium:   P2
      Low:      P3
```

**Discovering Jira custom-field IDs.** Jira's per-project custom field IDs are visible in the URL of the field's edit page in admin (`customfield_10026`), or via the REST API:

Cloud authenticates with your email and API token against `/rest/api/3`; Data Center / Server authenticates with a Personal Access Token sent as a bearer header against `/rest/api/2`.

<Tabs>
  <Tab title="bash / zsh (macOS, Linux)">
    ```bash theme={null}
    # Cloud (email + API token):
    curl -u you@acme.com:ATATT3... \
        https://acme.atlassian.net/rest/api/3/field \
        | jq '.[] | select(.custom == true) | {id, name}'

    # Data Center / Server (Personal Access Token):
    curl -H "Authorization: Bearer <your-PAT>" \
        https://jira.mycompany.com/rest/api/2/field \
        | jq '.[] | select(.custom == true) | {id, name}'
    ```
  </Tab>

  <Tab title="PowerShell (Windows)">
    Multi-line commands are easier in Windows PowerShell ISE, and you don't need to install `jq`.

    ```powershell theme={null}
    # Cloud (email + API token):
    $cred = [Convert]::ToBase64String(
        [Text.Encoding]::ASCII.GetBytes("<your user>:<your API token>"))

    $fields = Invoke-RestMethod `
        -Uri "https://<your site>.atlassian.net/rest/api/3/field" `
        -Headers @{Authorization = "Basic $cred"}

    $fields | Where-Object { $_.custom -eq $true } |
        Select-Object id, name | Format-Table

    # Data Center / Server (Personal Access Token):
    $fields = Invoke-RestMethod `
        -Uri "https://jira.mycompany.com/rest/api/2/field" `
        -Headers @{Authorization = "Bearer <your PAT>"}

    $fields | Where-Object { $_.custom -eq $true } |
        Select-Object id, name | Format-Table
    ```
  </Tab>
</Tabs>

### Linear

```yaml theme={null}
linear:
  estimate: { ekso: Estimate, kind: decimal }
```

Linear's first-class `estimate` field is the only thing most teams need to map. Custom fields beyond that are uncommon on Linear.

### Azure DevOps

```yaml theme={null}
devops:
  Microsoft.VSTS.Scheduling.StoryPoints: { ekso: StoryPoints, kind: decimal }
  Microsoft.VSTS.Common.Severity:
    ekso: Severity
    kind: picker
    picker:
      "1 - Critical": P0
      "2 - High":     P1
      "3 - Medium":   P2
      "4 - Low":      P3
  Custom.RootCause: { ekso: RootCause, kind: text }
```

**Discovering DevOps reference names.** From the work-item form: open any work item, click the field, the reference name shows in the field properties. Or via REST:

```bash theme={null}
curl -u :PAT https://dev.azure.com/your-org/your-project/_apis/wit/fields?api-version=7.1 \
    | jq '.value[] | {referenceName, name, type}'
```

### Zendesk

```yaml theme={null}
zendesk:
  "360001234567": { ekso: AffectedComponent, kind: text }
  "360001234568":
    ekso: CustomerTier
    kind: picker
    picker:
      Free: tier-free
      Pro:  tier-pro
      Ent:  tier-ent
```

Zendesk ticket-field IDs are numeric strings — quote them in YAML so they're treated as keys, not numbers.

**Discovering Zendesk ticket field IDs:**

```bash theme={null}
curl -u you@acme.com/token:API_TOKEN \
    https://acme.zendesk.com/api/v2/ticket_fields.json \
    | jq '.ticket_fields[] | {id, title, type}'
```

### Gemini

```yaml theme={null}
gemini:
  PullRequestUrl: { ekso: PullRequestUrl, kind: text }
  Severity:
    ekso: Severity
    kind: picker
    picker:
      Critical: P0
      High:     P1
      Medium:   P2
      Low:      P3
```

Gemini custom fields are accessed by name. SQL mode reads them from `dbo.IssueCustomFields`; API mode pulls them from the issue payload.

## Without a field map

You can run `apply` without `--field-map`. Source custom fields fall through to `DataItem.Meta` losslessly. Each source field becomes a `Meta` key prefixed with the source name (e.g. `Meta.jira_customfield_10026 = "5"`). This is fine for archival migrations where the destination process schema doesn't matter. For active use, write the YAML.

## What gets created on the destination process

After `apply` runs with a field map, your destination process has:

* A new field for every entry in the YAML (created if it didn't already exist).
* Every item populated with values from the source field.

The created fields appear under the process's field list in the admin UI. You can rename, reorder, or delete them after the fact — the migration doesn't lock anything.

## Failure modes

| Situation                                           | Behaviour                                                                                 |
| --------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| YAML references a kind the migrator doesn't support | exit `2` (usage error) before any work happens                                            |
| `picker` field with no `picker:` value list         | exit `6` (validation) — picker fields need values                                         |
| Field creation fails (permission denied)            | exit `4` (forbidden), no items written                                                    |
| Source value doesn't match any picker value         | the source value is preserved in `Meta`, the item field is left null, and apply continues |
| Decimal field gets a non-numeric source value       | `Meta` fallback for that item, warn at end                                                |

## Where to next

* **[Identity resolution](/cli/migrate/identity-resolution)** — author mapping.
* **[Command reference](/cli/migrate/command-reference)** — every flag.
* **[Troubleshooting](/cli/migrate/troubleshooting)** — exit codes.
