Forms
Forms (Slack modals) let you collect structured input from users. ChatOps4s derives the entire form definition from a case class — fields, input types, labels, and optionality are all inferred automatically.
Defining a Form
case class DeployForm(service: String, version: String, dryRun: Boolean) derives FormDef
derives FormDef generates a form with:
- A text input for
service - A text input for
version - A checkbox for
dryRun - Field labels derived from the field names ("Service", "Version", "Dry Run")
- Fields wrapped in
Option[T]become optional; all others are required
Registering and Opening a Form
deployForm <- slack.registerForm[DeployForm, String] { submission =>
val form = submission.values
slack.send(channel, s"Deploying ${form.service} ${form.version}").void
}
_ <- slack.registerCommand[String]("deploy-form", "Open deployment form") { cmd =>
slack
.openForm(cmd.triggerId, deployForm, "Deploy Service")
.as(CommandResponse.Silent)
}
registerForm returns a FormId[T, M] that you pass to openForm. You can open forms from:
- A slash command handler (using
cmd.triggerId) - A button click handler (using
click.triggerId)
Form Submission Context
The handler receives a FormSubmission[T, M] with:
| Field | Type | Description |
|---|---|---|
values | T | The parsed form values as your case class |
userId | UserId | Who submitted the form |
metadata | M | Custom metadata (set when opening the form, String by default) |
Supported Field Types
FormDef derivation supports these Scala types, each mapped to a Slack input element:
| Scala Type | Slack Input |
|---|---|
String | Plain text input |
Int, Long | Number input (integer) |
Double, Float, BigDecimal | Number input (decimal) |
Boolean | Checkbox |
LocalDate | Date picker |
LocalTime | Time picker |
Instant | Datetime picker |
Email | Email input |
Url | URL input |
UserId | User select |
List[UserId] | Multi-user select |
ChannelId | Channel select |
List[ChannelId] | Multi-channel select |
ConversationId | Conversation select |
List[ConversationId] | Multi-conversation select |
RichTextBlock | Rich text input |
Wrap any type in Option[T] to make it optional.
Initial Values
Pre-fill form fields using InitialValues:
def withInitialValues(
slack: SlackGateway[IO],
triggerId: chatops4s.slack.api.TriggerId,
formId: FormId[DeployForm, String],
): IO[Unit] = {
val initial = InitialValues
.of[DeployForm]
.set(_.service, "api-gateway")
.set(_.version, "1.0.0")
.set(_.dryRun, true)
slack.openForm(triggerId, formId, "Deploy Service", initialValues = initial)
}
The .set method uses a field selector lambda for type-safe access — the compiler ensures you're setting a value of the correct type for each field.
Form Metadata
You can attach metadata when opening a form, and read it back in the submission handler. By default metadata is a String, but you can use any type that has a MetadataCodec (including all Circe-encodable types):
def withMetadata(
slack: SlackGateway[IO] & SlackSetup[IO],
channel: String,
): IO[Unit] = {
for {
deployForm <- slack.registerForm[DeployForm, String] { submission =>
val meta = submission.metadata // your metadata string
val form = submission.values
slack.send(channel, s"[$meta] Deploying ${form.service}").void
}
_ <- slack.registerCommand[String]("deploy-form", "Open deployment form") { cmd =>
slack
.openForm(cmd.triggerId, deployForm, "Deploy", s"${cmd.channelId}:requested")
.as(CommandResponse.Silent)
}
} yield ()
}
This is useful for passing context (like which message triggered the form) through the form lifecycle.
Custom Field Types
For field types beyond the built-in list, provide a FieldCodec instance. The most common case is a static select menu mapped to a custom enum:
enum Environment { case Staging, Production }
given FieldCodec[Environment] = FieldCodec.staticSelect(
List(
BlockOption(PlainTextObject("Staging"), "staging") -> Environment.Staging,
BlockOption(PlainTextObject("Production"), "production") -> Environment.Production,
),
)
case class DeployFormWithEnv(service: String, environment: Environment) derives FormDef
The same pattern works for FieldCodec.radioButtons, FieldCodec.checkboxes, FieldCodec.multiStaticSelect, and FieldCodec.externalSelect.
AllInputs Example
The AllInputs example demonstrates every supported field type in a single form, including pre-filled initial values. It's a good reference for seeing all the available input types in action.