The depends_on field controls the execution order of actions. Actions with empty depends_on run immediately after the trigger. Subsequent actions run after their dependencies complete.
Actions can reference previous triggers and actions outputs via the following syntax in CEL expressions:
trigger.<output_field>: reference any output field of the trigger
actions.<action_name>.<output_field>: reference any output field of an action by its name
Some arguments, such as if or the api-requestallow argument, take CEL expressions by default. For arguments that take strings
instead, embed CEL expressions via the ${{}} syntax.
Triggered when a new log event matches a specified condition. Useful for reacting to specific database queries, policy violations, or access patterns.Args:
Arg
Type
Required
Description
if
string
Yes
CEL expression to filter logs. Only logs matching this condition trigger the workflow.
Triggered when a new resource is autodiscovered via cloud integration (AWS, GCP, etc.). Useful for automatically onboarding new databases.Args: NoneOutputs (available in actions):
Triggered on a recurring schedule defined by a standard 5-field cron expression (minute hour day-of-month month day-of-week). All times are evaluated in UTC. Useful for periodic checks, scheduled reports, and recurring maintenance tasks.Args:
Arg
Type
Required
Description
schedule
string
Yes
5-field cron expression (e.g., "0 9 * * 1-5" for weekdays at 9am UTC)
recovery
string
No
Controls behavior when scheduled triggers are missed during a Formal outage. One of none (default), latest, or all. See below.
Outputs (available in actions):
Output
Type
Description
trigger.scheduled_time
string
The time (UTC, RFC 3339) the cron was scheduled to run, always truncated to the minute.
trigger.execution_time
string
The actual time (UTC, RFC 3339) the trigger fired. Matches scheduled_time under normal operation.
Example:
trigger: name: "workday-check" type: "cron-schedule" args: schedule: "0 8,17 * * 1-5" # 8am and 5pm UTC on weekdaysactions: - name: "notify" type: "send-slack-message" args: text: "Workday check ran at ${{ trigger.execution_time }} (scheduled for ${{ trigger.scheduled_time }})" recipient_channel: "ops"
Recovery policy (optional): Under normal operation, cron triggers fire reliably every minute and recovery has no effect. It only matters in the unlikely event of a Formal platform outage where the workflow engine is temporarily unable to process scheduled triggers. When the platform recovers, any missed ticks are handled according to the recovery policy. If omitted, defaults to none. Missed triggers are capped at a 24-hour lookback window.
Policy
Behavior
none
Skips all missed triggers and resumes from the next future tick on the schedule.
latest
Fires a single trigger for the most recent missed tick, then resumes normally. The scheduled_time output reflects the missed tick, while execution_time reflects the actual recovery time.
all
Fires every missed tick (up to 24 hours) in chronological order, each with its own scheduled_time, then resumes normally.
Cron schedules are always evaluated in UTC. To schedule at a local time, convert manually (e.g., 9am US Eastern = "0 14 * * *" during EDT or "0 13 * * *" during EST).
Triggered when a user submits a Formal form via Slack. Useful for approval workflows, access requests, and other structured data collection scenarios.Args:
Arg
Type
Required
Description
id
string
Yes
The ID of the form to listen for submissions on
Outputs (available in actions via trigger.form_submission):
resource "formal_workflow" "request_policy_suspension_from_api_request" { name = "Request oneoff SQL queries in Slack from API requests" code = <<-YAML trigger: type: api-request name: request_query args: allow: true # write a condition on who can perform approval requests if applicable actions: - type: ask-in-chat name: ask_approver args: recipient_email: <recipient email here> message: 'Do you want to allow `${{ trigger.payload.query }}` from ${{ trigger.user.email }}?' integration: slack - type: formal-app-command name: suspend_policy depends_on: [ask_approver] if: actions.ask_approver.response == "yes" args: app: Policies command: name: PolicySuspension type: Create machine_user_id: "user_abc123" input: policy_id: "policy_1234" identity_type: user identity_id: ${{ trigger.user.id }} input_condition: ${{ trigger.payload.query }} oneoff: false expiration_minutes: 60 YAML}
resource "formal_workflow" "request_policy_suspension_from_form" { name = "Request oneoff SQL queries in Slack from API requests" # assume we have a form `form_abc123` with two fields: # one field with id `field_resource` that should be the resource name # one field with id `field_access_un` that should store the timestamp that the requester is requesting access until code = <<-YAML trigger: type: form-submission name: form_submission args: id: form_abc123 actions: - type: ask-in-chat name: ask_approval args: message: 'Approve access request to "${{trigger.form_submission.submission.field_resource }}" from ${{ trigger.form_submission.submitter_email }} until <!date^${{ int(timestamp(trigger.form_submission.submission.field_access_un)) }}^{date_short_pretty} at {time}|invalid_time>?' recipient_channel: some-slack-channel integration: slack - type: formal-app-command name: get_user_id depends_on: [ask_approval] args: app: User command: name: Users type: List machine_user_id: user_abc123 input: limit: 1 filter: field: key: "db_username" operator: "contains" value: value: ${{ trigger.form_submission.submitter_email }} "@type": "type.googleapis.com/google.protobuf.StringValue" - type: formal-app-command name: suspend_policy depends_on: [get_user_id] if: actions.ask_approval.response == "yes" && (int(timestamp(trigger.form_submission.submission.field_access_un)) > int(now) + 60) && actions.get_user_id.status_code == 200 && size(actions.get_user_id.body.users) > 0 args: app: Policies command: name: PolicySuspension type: Create machine_user_id: user_abc123 input: policy_id: policy_abc123 identity_type: user identity_id: ${{ actions.get_user_id.body.users[0].id }} input_condition: 'input.resource.name == "${{ trigger.form_submission.submission.field_resource }}"' oneoff: false expiration_minutes: ${{(int(timestamp(trigger.form_submission.submission.field_access_un)) - int(now))/60}} YAML}