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] { 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] { 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.