# Input and action widgets

> Source: https://docs.clonepartner.com/reference/widgets/inputs-and-actions/

<!-- sources: src/dashboard/register-widget-types.ts, src/dashboard-parser.ts, src/job-config-schema.ts, ui/src/components/dashboard/widgets/DashboardFormWidget.vue, ui/src/components/dashboard/widgets/DashboardActionWidget.vue, references/envoy/dashboards.md -->

# Input and action widgets

Widgets that make a dashboard interactive: `form` and `field_mapping` persist typed configuration onto the Job, `action` dispatches server-executed side effects, `task_progress` reads run status, and `explorer` embeds a curated data browser. Shared keys (`id`, `title`, `width`, `scope`, `refresh_interval_seconds`) are on the [Widgets overview](/reference/widgets/).

## `form`

Typed fields whose values persist as one object under a `config_key` in the Job's configuration. Tasks read the saved object in JSONata as `job_config.<config_key>`.

| Key | Type | Required | Description |
|---|---|---|---|
| `config_key` | string | Yes | `job_config` key the value is stored under. |
| `fields` | array | Yes | Non-empty array of form fields. |
| `validation_expr` | string | No | JSONata run against the value before persisting; a non-empty `[{ field, message }]` result rejects the save. |
| `transform_expr` | string | No | JSONata that reshapes the value just before it is written. |

Form fields use the same shape as job template `job_config` form entries — `key` (required), `label`, `type` (`text` | `number` | `boolean` | `select` | `multi_select`), `required`, `default`, `format` (`email` | `url`), inline `options` or a connector-backed `options_source`, `depends_on`, and `hide_if_expr`. The full field and option-source tables are in the [job_config schema](/reference/job-config/).

```yaml
- id: sync_options
  type: form
  config_key: sync_options
  fields:
    - { key: batch_size, label: Batch size, type: number, required: true, default: 100 }
    - { key: notify, type: boolean, default: false }
    - { key: owner_email, type: text, format: email }
```

Prefer declaring the canonical schema in the job template's top-level `job_config:` block; a dashboard `form` is then a customer-facing editor for the same persisted keys.

## `field_mapping`

A two-column mapping editor (left = source field, right = target field). The persisted value is an array of `{ source_field, target_field, transform? }` under `config_key`.

| Key | Type | Required | Description |
|---|---|---|---|
| `config_key` | string | Yes | `job_config` key the array is stored under. |
| `source` / `target` | object | Yes | An option source per side (`options_source`, `label_expr`, `value_expr`, `search`, and related keys — see [job_config schema](/reference/job-config/)). |
| `allow_transform` | boolean | No | Enables the per-row JSONata `transform` editor. Operator-only: hidden on the public share, but operator-set values survive customer edits. |
| `validation_expr` | string | No | JSONata run against the value before persisting. |
| `cardinality` | string | No | `one_to_one` (default, unique targets) or `many_to_one`. |
| `extra_columns` | array | No | Typed per-row fields: `{ key (required), label?, type: text \| select \| boolean, options?, required? }`. |
| `status_expr` | string | No | JSONata per row → `{ label, tone }` badge, evaluated with `{ row, job_config, shared }`. |
| `row_action` | string | No | `action` widget ID rendered as a per-row button; the row is merged into that action's JSONata context. |

```yaml
- id: meeting_map
  type: field_mapping
  title: Salesforce → HubSpot meeting fields
  config_key: meeting_field_map_sf_to_hs
  allow_transform: true
  source:
    options_source:
      connector: salesforce
      method: proxy_list
      args: { resource: describe, query: { object: arguments.sf_object } }
    label_expr: $.label
    value_expr: $.name
    search: { on_frontend: true }
  target:
    options_source:
      connector: hubspot
      method: proxy_list
      args: { resource: properties, query: { objectType: meetings } }
    label_expr: $.label
    value_expr: $.name
    search: { on_frontend: true }
```

Missing `config_key` fails with `<path>.config_key is required for field_mapping`; missing sides fail with `<path>.source and .target are required for field_mapping`.

## `action`

A declarative button that runs entirely server-side. Viewers submit form values only; the server resolves the action from the stored YAML. Every action opens a confirmation dialog before running.

| Key | Type | Required | Description |
|---|---|---|---|
| `label` | string | Yes | Button text. |
| `action` | object | Yes | The dispatch spec (see below). |
| `form` | object | No | `{ fields: [...] }` — form fields rendered inside the confirmation dialog, same shape as `form` fields. |
| `allow_public` | boolean | No | Allow execution from the public share (additionally gated server-side). |
| `cooldown_seconds` | number | No | Minimum delay between runs of this widget. |
| `confirm_message` | string | No | Text shown in the confirmation dialog (fallback: `description`). |
| `on_success` | object | No | `{ refresh?: string[], message?: string }` — widget IDs to refresh and a toast message. |

### Action kinds

`action.kind` must be one of `run_task`, `job_run`, or `connector_method`:

| Kind | Required keys | Behavior |
|---|---|---|
| `run_task` | `task` | Starts a task run for the named job task key, using the Job's connector mapping. |
| `job_run` | — | Starts a Job Run; optional `tasks` (string[]) selects a subset and `skip_deps` skips dependency resolution. Does not accept `arguments_expr` or `args` — the job runs with its existing arguments and configuration. |
| `connector_method` | `connector`, `method` | Sanctioned write path through the Job's mapped connector. |

For `run_task` and `connector_method`, `arguments_expr` (JSONata) or a literal `args` value builds the dispatch arguments. The expression context is `{ form, job, arguments, job_config, row? }` — `row` is set when an authenticated operator triggers the action from a `field_mapping` `row_action`.

```yaml
- id: invite_user
  type: action
  label: Invite
  form:
    fields:
      - key: email
        type: text
        required: true
        format: email
  action:
    kind: run_task
    task: invite-user
    arguments_expr: '{ "email": form.email, "job_id": job.id }'
  allow_public: true
  cooldown_seconds: 30
  confirm_message: Sends an invite email to this address.
  on_success:
    refresh: [user_mapping]
    message: User invited
```

Safety: the server deduplicates in-flight executions per widget, applies `cooldown_seconds`, and records audit rows in `dashboard_action_runs`.

## `task_progress`

Reads the latest run state for one job task key. No connector call.

| Key | Type | Required | Description |
|---|---|---|---|
| `task` | string | Yes | Task key within the dashboard's Job. |
| `stat_expr` | string | No | JSONata over the run and metrics producing a headline number (for example `run.item_count`). |

```yaml
- id: migrate_progress
  type: task_progress
  task: migrate_tickets
  refresh_interval_seconds: 5
  stat_expr: run.item_count
```

Returns the latest run status, `item_count`, `duration_ms`, and per-step counts from `metrics_summary`.

## `explorer`

A curated data browser: the author pins the connector, resource, and a server-enforced `base_query`; viewers get paging, filtering, sorting, column selection, and export.

| Key | Type | Required | Description |
|---|---|---|---|
| `data.connector` | string | Yes | Template connector name. |
| `data.resource` | string | Yes | Collection or table to browse. |
| `data.base_query` | object | No | Server-enforced base filter; viewer parameters are always ANDed in and can never remove it. |
| `default_sort` | object | No | Field → direction applied when the viewer has not sorted. |
| `page_size` | number | No | Positive number, capped at 100. |
| `features` | array | No | Any of `filter`, `sort`, `columns`, `export`, `pagination`; unknown values are dropped. |
| `columns` | array | No | `{ field (required), label?, format?, sortable? }`. When declared, columns are the allowlist for viewer filters and projections. |

```yaml
- id: errored_tickets
  type: explorer
  data:
    connector: mongo
    resource: zendesk_tickets
    base_query:
      filter: { severity: error }
  default_sort: { updated_at: -1 }
  page_size: 25
  features: [filter, sort, columns, export]
  columns:
    - { field: id, label: ID, sortable: true }
    - { field: subject, format: truncate }
```

Sort is always applied server-side. Viewers may sort only on columns marked `sortable: true` — mark only fields backed by an index; sort requests on other columns are ignored and `default_sort` stays in effect. Export streams all matching rows as CSV or NDJSON server-side, applying the same merged `base_query`, scope profile, and column allowlist; omitting `export` from `features` disables both the button and the export endpoints.

## Public execution gates

Interactive widgets are gated twice on the public share surface:

- Input widgets (`form`, `field_mapping`): the server kill switch `ENVOY_PUBLIC_DASHBOARD_INPUT_ENABLED` must be enabled (it defaults off) and the dashboard must set `allow_public_input: true`.
- Action widgets: the server kill switch `ENVOY_PUBLIC_DASHBOARD_ACTIONS_ENABLED=true` must be set and the widget must set `allow_public: true` on a public page. Client-supplied `row` context is ignored on the public path.

See [Authoring dashboards](/building/authoring-dashboards/) for the full public-share model.

## Related

- [job_config schema](/reference/job-config/) — the schema and stored value shapes input widgets write
- [Widgets overview](/reference/widgets/) — shared keys and data binding
- [Authoring dashboards](/building/authoring-dashboards/) — public sharing and design guidance
