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

# Forms

> Create structured data collection forms for workflow triggers via Slack

## Overview

Forms enable structured data collection that can trigger [workflows](/docs/guides/configuration/workflows). A form defines a set of typed fields that users fill out via Slack. When submitted, the form data is available to workflow actions via CEL expressions.

Forms are useful for **Access requests**: Enable users to request access to perform specific actions on resources with time-bound parameters for policy suspensions.

## Form Structure

A form consists of:

* **Name**: A unique name within your organization
* **Description**: Optional description shown to users when filling out the form
* **Fields**: One or more typed fields for data collection

### Field Types

**Text and numeric inputs:**

* `string` — Plain text input
* `number` — Numeric input
* `email` — Email address input
* `url` — URL input

**Date and time inputs:**

* `date` — Date picker
* `time` — Time picker
* `timestamp` — Combined date and time picker

**Selection inputs** (require options configuration — see [below](#selection-field-options)):

* `select` — Single-select dropdown
* `multi_select` — Multi-select dropdown
* `checkbox` — Checkbox group
* `radio` — Radio button group

### Field IDs

Each field has an `id` that must match the pattern `field_[_a-zA-Z0-9]+`. This ID is used to reference field values in workflow actions via `trigger.form_submission.submission.field_<id>`.

### Selection Field Options

The `select`, `multi_select`, `checkbox`, and `radio` field types all require an options configuration. All four types support **static options**. Only `select` and `multi_select` also support a **dynamic options source**. A field must have one or the other, not both.

#### Static Options

Provide a fixed list of options directly in the field configuration. Each option has a `label` (displayed to the user) and a `value` (submitted with the form).

#### Dynamic Options

Dynamic sources allow you to populate `select` and `multi_select` inputs by calling a `List` endpoint on Formal's API. This is useful when the set of options changes over time (e.g., listing resources, users, or datastores). `checkbox` and `radio` fields do not support dynamic sources and must use static options.

A dynamic source has the following configuration:

| Parameter           | Description                                                                                                                                                                         |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **App**             | The service name (e.g., `core.v1.ResourceService`)                                                                                                                                  |
| **Command Name**    | The method name without the `List` prefix (e.g., `Resources` for the `ListResources` method)                                                                                        |
| **Machine User ID** | The machine user used to authenticate options retrieval                                                                                                                             |
| **Input**           | The JSON request body. Use `${{ query }}` to interpolate the user's search text into the request. You can also pass `searchFields` to control which fields are searched             |
| **Transform**       | A CEL expression evaluated against the response body (available via the `body` variable) when the response is a 200. Should return a list of objects with `value` and `text` fields |

The `searchFields` parameter in the input controls which fields the `search` term is matched against. For example, `"searchFields": ["name", "technology", "hostname"]` restricts matching to those three fields on the target resource.

## Filling Out Forms

### Slack Integration

Forms can be filled out via Slack using the "Fill out form" shortcut. A [Slack integration](/docs/guides/integrations/chats) needs to be linked to your Formal organization.

#### Slash Command

Type `/formal form` to open a modal where you can select and fill out a form.

#### Submission Flow

1. User opens the form shortcut in Slack
2. User selects a form from the searchable dropdown
3. User fills out the fields and submits
4. A confirmation message with the submitted values is sent to the user
5. Any workflows with a matching `form-submission` trigger are executed

### Desktop App

Forms can also be filled out via the Formal desktop app.

#### Submission Flow

1. User opens the desktop app
2. User runs `formal form fill` and selects a form from the shown list
3. User fills out the fields and submits
4. A confirmation message with the submitted values is shown to the user, along with a command to submit the same payload again without needing to re-select the form
5. Any workflows with a matching `form-submission` trigger are executed

You can also display forms and their fields using `formal form list -o [jsonl|yaml]` for programmatic use.

## Using Forms in Workflows

Forms connect to workflows via the `form-submission` trigger type. See the [workflows documentation](/docs/guides/configuration/workflows#form-submission) for trigger details.

Form data will not be saved in the Formal Control Plane unless there is at least one workflow with the corresponding `form-submission` trigger type.

### Example: Access Request with Approval

```yaml theme={null}
trigger:
  type: form-submission
  name: access_request
  args:
    id: form_abc123

actions:
  - type: ask-in-chat
    name: ask_approval
    args:
      message: "${{ trigger.form_submission.submitter_email }} is requesting access to ${{ trigger.form_submission.submission.field_resource }}. Reason: ${{ trigger.form_submission.submission.field_reason }}. Approve?"
      recipient_channel: "security-approvals"
      integration: slack
```

### Referencing Form Data in CEL

Form submission data is available under `trigger.form_submission`:

| Expression                                      | Description                                       |
| ----------------------------------------------- | ------------------------------------------------- |
| `trigger.form_submission.form_id`               | The ID of the submitted form                      |
| `trigger.form_submission.form_name`             | The name of the submitted form                    |
| `trigger.form_submission.submitter_email`       | Email of the user in Slack who submitted the form |
| `trigger.form_submission.submission.field_<id>` | Value of a specific field by its ID               |

## Terraform Example

```hcl theme={null}
resource "formal_form" "access_request" {
  name        = "access-request-form"
  description = "Form for requesting database access"

  field {
    id   = "field_reason"
    name = "Reason"
    type = "string"
  }

  field {
    id   = "field_resource"
    name = "Resource"
    type = "select"

    config {
      option {
        label = "Production"
        value = "production"
      }
      option {
        label = "Staging"
        value = "staging"
      }
    }
  }

  field {
    id   = "field_duration"
    name = "Duration (hours)"
    type = "number"
  }
}
```

For a dynamic options source (e.g., fetching resources from Formal):

```hcl theme={null}
resource "formal_form" "dynamic_access_request" {
  name        = "dynamic-access-request"
  description = "Access request with dynamic resource selection"

  field {
    id   = "field_resource"
    name = "Resource"
    type = "select"

    config {
      options_source {
        app             = "core.v1.ResourceService"
        machine_user_id = formal_user.machine.id
        transform       = "body.resources.map(r, {\"value\": r.id, \"text\": r.name})"

        command {
          name = "Resources"
        }

        input = {
          search       = "${{ query }}"
          limit        = "25"
          searchFields = jsonencode(["name", "technology", "hostname"])
        }
      }
    }
  }

  field {
    id   = "field_reason"
    name = "Reason"
    type = "string"
  }
}
```

See the [formal\_form resource documentation](https://registry.terraform.io/providers/formalco/formal/latest/docs/resources/form) for the full schema reference.

## Next Steps

<CardGroup cols={2}>
  <Card title="Workflows" icon="diagram-project" href="/docs/guides/configuration/workflows">
    Learn about workflow triggers, actions, and chaining
  </Card>

  <Card title="Slack Integration" icon="slack" href="/docs/guides/integrations/chats">
    Configure your Slack integration for forms
  </Card>
</CardGroup>
