Execution flow
When a user triggers an action in Avo, the following flow occurs:
Record selection phase:
- This phase can be bypassed by setting
self.standalone = true
- For bulk actions on the index page, Avo collects all the records selected by the user
- For actions on the show page or row controls, Avo uses that record as the target of the action
- This phase can be bypassed by setting
The action is initiated by the user through the index page (bulk actions), show page (single record actions), or resource controls (custom action buttons)
Form display phase (optional):
- This phase can be bypassed by setting
self.no_confirmation = true
- By default, a modal is displayed where the user can confirm or cancel the action
- If the action has defined fields, they will be shown in the modal for the user to fill out
- The user can then choose to run the action or cancel it
- If the user cancels, the execution stops here
- This phase can be bypassed by setting
Action execution:
- The
handle
method processes selected records, form values, current user, and resource details - Your custom business logic is executed within the
handle
method - User feedback is configured (
succeed
,warn
,inform
,error
, orsilent
) - Response type is configured (
redirect_to
,reload
,keep_modal_open
, and more)
- The
The handle
method
The handle
method is where you define what happens when your action is executed. This is the core of your action's business logic and receives the following arguments:
query
Contains the selected record(s). Single records are automatically wrapped in an array for consistencyfields
Contains the values submitted through the action's form fieldscurrent_user
The currently authenticated userresource
The Avo resource instance that triggered the action
# app/avo/actions/toggle_inactive.rb
class Avo::Actions::ToggleInactive < Avo::BaseAction
self.name = "Toggle Inactive"
def fields
field :notify_user, as: :boolean
field :message, as: :textarea
end
def handle(query:, fields:, current_user:, resource:, **args)
query.each do |record|
# Toggle the inactive status
record.update!(inactive: !record.inactive)
# Send notification if requested
if fields[:notify_user]
# Assuming there's a notify method
record.notify(fields[:message])
end
end
succeed "Successfully toggled status for #{query.count}"
end
end
Feedback notifications
After an action runs, you can respond to the user with different types of notifications or no feedback at all. The default feedback is an Action ran successfully
message of type inform
.
-> succeed
Displays a green success alert to indicate successful completion.
-> warn
Displays an orange warning alert for cautionary messages.
-> inform
Displays a blue info alert for general information.
-> error
Displays a red error alert to indicate failure or errors.
-> silent
You may want to run an action and show no notification when it's done. That is useful for redirect scenarios. You can use the silent
response for that.
# app/avo/actions/toggle_inactive.rb
class Avo::Actions::ToggleInactive < Avo::BaseAction
def handle(**args)
redirect_to "/admin/some-tool"
silent
end
end
INFO
You can show multiple notifications at once by calling multiple feedback methods (succeed
, warn
, inform
, error
) in your action's handle
method. Each notification will be displayed in sequence.
# app/avo/actions/toggle_inactive.rb
class Avo::Actions::ToggleInactive < Avo::BaseAction
def handle(**args)
succeed "Success response ✌️"
warn "Warning response ✌️"
inform "Info response ✌️"
error "Error response ✌️"
end
end

Response types
After an action completes, you can control how the UI responds through various response types. These powerful responses give you fine-grained control over the user experience by allowing you to:
- Navigate: Reload pages or redirect users to different parts of your application
- Manipulate UI: Control modals, update specific page elements, or refresh table rows
- Handle Files: Trigger file downloads and handle data exports
- Show Feedback: Combine with notification messages for clear user communication
You can use these responses individually or combine them to create sophisticated interaction flows. Here are all the available action responses:
-> reload
The reload
response triggers a full-page reload. This is the default behavior if no other response type is specified.
def handle(query:, **args)
query.each do |project|
project.update active: false
end
succeed 'Done!'
reload # This is optional since reload is the default behavior
end
-> redirect_to
redirect_to
will execute a redirect to a new path of your app. It accept allow_other_host
, status
and any other arguments.
Example: redirect_to path, allow_other_host: true, status: 303
def handle(query:, **args)
query.each do |project|
project.update active: false
end
succeed 'Done!'
redirect_to avo.resources_users_path
end
-> download
download
will start a file download to your specified path
and filename
.
You need to set self.may_download_file
to true for the download response to work like below. That's required because we can't respond with a file download (send_data) when making a Turbo request.
If you find another way, please let us know 😅.
class Avo::Actions::DownloadFile < Avo::BaseAction
self.name = "Download file"
self.may_download_file = true
def handle(query:, **args)
filename = "projects.csv"
report_data = []
query.each do |project|
report_data << project.generate_report_data
end
succeed 'Done!'
if report_data.present? and filename.present?
download report_data, filename
end
end
end
# app/avo/resources/project.rb
class Avo::Resources::Project < Avo::BaseResource
def fields
# fields here
end
def actions
action Avo::Actions::DownloadFile
end
end
-> keep_modal_open
There might be situations where you want to run an action and if it fails, respond back to the user with some feedback but still keep it open with the inputs filled in.
keep_modal_open
will tell Avo to keep the modal open.
class Avo::Actions::KeepModalOpenAction < Avo::BaseAction
self.name = "Keep Modal Open"
self.standalone = true
def fields
field :name, as: :text
field :birthday, as: :date
end
def handle(fields:, **args)
User.create fields
succeed "All good ✌️"
rescue => error
error "Something happened: #{error.message}"
keep_modal_open
end
end
-> close_modal
This type of response becomes useful when you are working with a form and need to execute an action without redirecting, ensuring that the form remains filled as it is.
close_modal
will flash all the messages gathered by action responses and will close the modal using turbo streams keeping the page still.
class Avo::Actions::CloseModal < Avo::BaseAction
self.name = "Close modal"
def handle(**args)
# do_something_here
succeed "Modal closed!!"
close_modal
# or
do_nothing
end
end
-> do_nothing
do_nothing
is an alias for close_modal
.
class Avo::Actions::CloseModal < Avo::BaseAction
self.name = "Close modal"
def handle(**args)
# do_something_here
succeed "Modal closed!!"
do_nothing
end
end
-> navigate_to_action
You may want to redirect to another action. Here's an example of how to create a multi-step process, passing arguments from one action to another. In this example the initial action prompts the user to select the fields they wish to update, and in the subsequent action, the chosen fields will be accessible for updating.
class Avo::Actions::City::PreUpdate < Avo::BaseAction
self.name = "Update"
def fields
field :name, as: :boolean
field :population, as: :boolean
end
def handle(query:, fields:, **args)
navigate_to_action Avo::Actions::City::Update,
arguments: {
cities: query.map(&:id),
render_name: fields[:name],
render_population: fields[:population]
}
end
end
class Avo::Actions::City::Update < Avo::BaseAction
self.name = "Update"
self.visible = -> { false }
def fields
field :name, as: :text if arguments[:render_name]
field :population, as: :number if arguments[:render_population]
end
def handle(fields:, **args)
City.find(arguments[:cities]).each do |city|
city.update! fields
end
succeed "City updated!"
end
end
You can see this multi-step process in action by visiting the avodemo. Select one of the records, click on the "Update" action, choose the fields to update, and then proceed to update the selected fields in the subsequent action.
-> append_to_response
Avo action responses are in the turbo_stream
format. You can use the append_to_response
method to append additional turbo stream responses to the default response.
def handle(**args)
succeed "Modal closed!!"
close_modal
append_to_response -> {
turbo_stream.set_title("Cool title ;)")
}
end
The append_to_response
method accepts a Proc or lambda function. This function is executed within the context of the action's controller response.
The block should return either a single turbo_stream
response or an array of multiple turbo_stream
responses.
append_to_response -> {
[
turbo_stream.set_title("Cool title"),
turbo_stream.set_title("Cool title 2")
]
}
append_to_response -> {
turbo_stream.set_title("Cool title")
}
-> reload_records
WARNING
This option only works on Index pages, NOT on associations.
This option leverages Turbo Stream to refresh specific table rows in response to an action. For individual records, you can use the reload_record
alias method.
def handle(query:, fields:, **args)
query.each do |record|
record.update! active: !record.active
record.notify fields[:message] if fields[:notify_user]
end
reload_records(query)
end
The reload_records
and reload_record
methods are aliases, and they accept either an array of records or a single record.
reload_records([record_1, record_2])
reload_record(record)