Skip to main content

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]("deploy-form") { 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:

FieldTypeDescription
valuesTThe parsed form values as your case class
userIdUserIdWho submitted the form
metadataMCustom 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 TypeSlack Input
StringPlain text input
Int, LongNumber input (integer)
Double, Float, BigDecimalNumber input (decimal)
BooleanCheckbox
LocalDateDate picker
LocalTimeTime picker
InstantDatetime picker
EmailEmail input
UrlURL input
UserIdUser select
List[UserId]Multi-user select
ChannelIdChannel select
List[ChannelId]Multi-channel select
ConversationIdConversation select
List[ConversationId]Multi-conversation select
RichTextBlockRich 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]("deploy-form") { 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.