| name | avo-engine-internals |
| description | The Rails-engine plumbing you touch when writing custom Ruby that runs inside Avo — engine path helpers (`avo.` vs `main_app.`), `Avo::Current`, `Avo::ExecutionContext`, reserved model names / route conflicts, `Avo::Services::EncryptionService`, and calling your app's helpers from inside Avo. Use when the user wants to link from the admin back to their main app, fix `undefined method 'posts_path'` (or a route resolving to the wrong page) inside Avo, access the current user / request / params / context / tenant inside an Avo block, know which variables (`record`, `resource`, `view`, `current_user`) are available in a lambda or why `record` is nil, use one of their app's view helpers inside an Avo field, fix a model named `resource` / `chart` / `search` / `home` / `attachment` that breaks the admin or causes routing conflicts, keep a reserved-name model while renaming its Avo resource (`--model-class`), fix a `resources :resources` route helper clash, or encrypt/decrypt a value to pass safely through Avo params. Set the tenant for the current admin request. |
| allowed-tools | Read, Edit, Write, Glob, Grep, Bash, WebFetch |
| metadata | {"requires-gem":"none — Community"} |
Avo Engine Internals
Avo is a Ruby on Rails engine that runs isolated and side-by-side with the host app. The moment you write custom Ruby that executes inside Avo — a computed field, an action's handle, a controller override, a breadcrumb, a menu block, a card — you're on the engine's turf, and a handful of primitives govern how that code sees routes, request state, and app helpers. This skill owns those primitives:
- Path helpers —
avo. vs main_app. prefixes for isolated engine routing.
Avo::Current — per-request state (user, params, request, context, view_context, locale, tenant).
Avo::ExecutionContext — how your blocks receive record / resource / view / current_user and delegate to the view.
- Reserved model names — names that collide with Avo's internal controllers/routes, and the escape hatches.
Avo::Services::EncryptionService — encrypt/decrypt values passed through params.
These are cross-cutting: many other Avo features lean on them, but this is where they're documented. When another concern (a controller, a multitenant setup, a custom card) trips over one of these, it's this skill's territory. License: Community — none of this needs a paid gem.
Docs — fetch on demand with WebFetch; prefer the raw .md (clean, no HTML):
When this applies
Reach for this skill when a symptom or request points at the engine plumbing rather than a specific feature:
| Symptom / request | Section |
|---|
"Link from the admin back to my main app", undefined method 'posts_path' inside Avo, a link resolves to the wrong page | Path helpers |
| "Access the current user / request / params / context / tenant inside an Avo block", "set the tenant for this request" | Avo::Current |
"What variables are available in this block?", "why is record nil in my lambda?", "use record / resource / view in a proc" | Avo::ExecutionContext |
"Use my app's view helper (link_to, a custom formatter) inside an Avo field" | ExecutionContext + Using your app's helpers |
Model named resource / chart / search / home / attachment breaks the admin, routing conflicts, resources :resources clash | Reserved names |
| "Encrypt a value to pass through Avo params", "decrypt what select-all sent" | EncryptionService |
Path helpers (main_app vs avo)
Rails engines have isolated routes. Code running inside Avo resolves path helpers in Avo's route set by default, so a bare posts_path either raises undefined method or — worse, silently — resolves to a different route than you meant. Always prefix:
avo.root_path
avo.resources_users_path
avo.resources_user_path(user)
avo.resource_path(resource: UserResource, record: @user)
main_app.root_path
main_app.posts_path
main_app.post_path(record)
This applies anywhere your code runs inside the engine: Avo::ResourcesController / Avo::ToolsController overrides, breadcrumbs configured in config/initializers/avo.rb, fields, actions, cards, custom tools, ejected partials.
| You want to link to… | Prefix |
|---|
| Avo pages (resources, tools, dashboards) | avo. |
| Your main application routes | main_app. |
Inside an ExecutionContext block (fields, actions, most DSL lambdas) main_app and avo are already in scope as accessors — you can write main_app.post_path(record) directly without a receiver. See below.
Avo::Current
Avo::Current is Avo's request-scoped state, built on ActiveSupport::CurrentAttributes. Avo populates it at the start of each request; read it from anywhere in your custom Ruby.
| Attribute | What it is |
|---|
Avo::Current.user | The authenticated user — whatever your current_user_method returns. |
Avo::Current.params | Equivalent to request.params (falls back to {} when there's no rack input). |
Avo::Current.request | The Rails request (an empty ActionDispatch::Request if none). |
Avo::Current.context | The context hash you configured in the initializer, evaluated in Avo::ApplicationController. |
Avo::Current.view_context | An ActionView context — call any helper/variable available in your partials, e.g. Avo::Current.view_context.link_to "Avo", "https://avohq.io". |
Avo::Current.locale | The app locale for the request. |
Avo::Current.tenant / Avo::Current.tenant_id | Writable. Avo leaves these for you to set — assign the current tenant early (e.g. in an Avo::ApplicationController override) and read it back later in the request. |
Avo::Current.tenant = current_account
Avo::Current.tenant_id = current_account.id
Avo::Current.user
For a full multitenancy setup (scoping records, switching tenants, per-tenant menus) use the avo-multitenancy skill — Avo::Current.tenant is the primitive it builds on.
Avo::ExecutionContext
Most Avo DSL blocks — computed fields, visible/hide lambdas, action handle context, dynamic options — don't run in a vacuum. Avo wraps them in an Avo::ExecutionContext: an object that holds request state and instance_execs your block against it, so the right variables are simply in scope.
field :full_name, as: :text do
"#{record.first_name} #{record.last_name}"
end
Always in scope (defaulted from Avo::Current): context, current_user, params, request, view_context, locale, main_app, avo.
Provided per-block, depending on what's being evaluated: record, resource, view, and any other variable Avo (or you) passes in. This is why record exists in a field block but may be nil or absent in a block that isn't tied to a record — the variable is only there if that call site passed it.
Two behaviors worth knowing:
delegate_missing_to :view_context. Any method the block calls that isn't a known accessor is forwarded to view_context. That means view helpers — link_to, content_tag, image_tag, and your app's own view helpers — are callable directly inside the block, no receiver needed.
- Only callables are executed.
ExecutionContext runs the target only if it respond_to?(:call); a non-callable value (a plain string, symbol, boolean) is returned as-is. So visible: false and visible: -> { view.show? } both work — the lambda gets the context, the literal doesn't.
You can drive it yourself when needed:
Avo::ExecutionContext.new(
target: -> { "#{record.first_name} #{record.last_name}" },
record: User.first,
view: :index
).handle
include: — mix in modules
Pass include: an array of modules to make their methods available inside the block:
Avo::ExecutionContext.new(
target: -> { sanitize "<script>alert('be careful')</script>#{record.name}" },
record: record,
include: [ActionView::Helpers::SanitizeHelper]
).handle
Using your app's helpers inside Avo
Three ways, from most to least direct:
-
View helpers, directly. Because of delegate_missing_to :view_context, anything defined in app/helpers (or standard view helpers) is callable straight from an ExecutionContext block:
field :name, as: :text, format_using: -> { link_to value, main_app.post_path(record) }
-
The helpers object. Inside an ExecutionContext block, helpers returns an object with every module from app/helpers mixed in — handy when you want an explicit receiver:
field :name, as: :text, format_using: -> { helpers.simple_name(value) }
-
view_context.controller, from outside a block (e.g. plain resource code or a controller). Include the helper module in the Avo controller, then reach it through the controller:
class Avo::ProductsController < Avo::ResourcesController
include ApplicationHelper
end
field :copyright, as: :text do
view_context.controller.render_copyright_info
end
For controller overrides in general, see the avo-controllers skill.
Reserved names
Avo dynamically maps models to internal controllers and routes, so a handful of model names collide with Avo's own controllers and will override built-in functionality or break routing. Avoid these model names:
action appearance_settings application array association attachment
base base_application chart debug home media_library
private resource search
If you don't already have such a model, rename it (user_resource instead of resource, advanced_search instead of search, graph instead of chart).
Keep the model, rename the Avo resource — when the model already exists and you can't rename it, generate the Avo resource under a different name but point it at the real model class:
bin/rails generate avo:resource user_resource --model-class resource
Generates Avo::Resources::UserResource and Avo::UserResourcesController, both backed by the existing Resource model — no collision.
Route helper clash with resources :resources. A host-app route like resources :resources defines a resources_path helper that overrides Avo's internal resources_path and can break parts of the admin. Keep the URL, rename the helper:
resources :resources, as: 'articles'
EncryptionService
Avo::Services::EncryptionService encrypts and decrypts values so they can travel safely through params. Avo uses it internally (e.g. select-all serializes the query, encrypted, into the URL), and you can call it anywhere.
token = Avo::Services::EncryptionService.encrypt(message: "Secret string", purpose: :demo)
Avo::Services::EncryptionService.decrypt(message: token, purpose: :demo)
token = Avo::Services::EncryptionService.encrypt(message: Course::Link.first, purpose: :demo, serializer: Marshal)
Avo::Services::EncryptionService.decrypt(message: token, purpose: :demo, serializer: Marshal)
message: (required) — the object to encrypt.
purpose: (required) — any symbol; it must match between encrypt and decrypt or verification fails.
- Extra kwargs (e.g.
serializer:) pass straight through to ActiveSupport::MessageEncryptor; use serializer: Marshal for non-string objects.
- It derives the key from
Rails.application.secret_key_base, so a secret_key_base must be defined — via ENV["SECRET_KEY_BASE"], Rails.application.credentials.secret_key_base, or Rails.application.secrets.secret_key_base.
Gotchas
- Missing
avo. / main_app. prefix is the #1 "extending Avo" footgun. A bare path helper inside the engine raises undefined method — or, when both route sets define the same name, silently resolves to the wrong route with no error. Always prefix.
- Reserved model names collide with Avo's controllers. A model named
resource, chart, search, home, attachment, association, etc. overrides Avo's internal controllers/routes. Keep the model via --model-class (renaming only the Avo resource), and rename a conflicting resources :resources route helper with as:.
record (or any per-block variable) only exists if the call site passed it. ExecutionContext accessors like context/current_user/request default from Avo::Current, but record/resource/view are supplied per block. Referencing one where it wasn't passed gives nil (or NameError) — that's not a bug, it's the wrong block.
ExecutionContext runs the target only if it respond_to?(:call). A non-callable value is returned untouched. If you expected your literal to be "evaluated," it wasn't — wrap it in a lambda.
- Delegation is to
view_context, not controller. Missing methods on an ExecutionContext go to view_context. To reach controller-level helpers you included in an Avo controller, go through view_context.controller explicitly.
EncryptionService needs matching purpose: and a secret_key_base. A mismatched purpose: between encrypt and decrypt fails verification; a missing secret_key_base raises. Use serializer: Marshal for anything that isn't a plain string.
Report
When done, tell the user:
- Which primitive you used and where (file + line): a path-helper prefix, an
Avo::Current read/write, an ExecutionContext block, a reserved-name fix, or an EncryptionService call.
- For path helpers: which prefix (
avo. / main_app.) and why that route set.
- For reserved names: the collision, and whether you renamed the model, used
--model-class, and/or added as: to a route.
- For
EncryptionService: the purpose: used and whether a serializer: was needed — and remind them encrypt/decrypt must share both.
- Any follow-up the user still owns: defining
secret_key_base, wiring tenant scoping (avo-multitenancy), or a controller include (avo-controllers). Cross-link avo-multitenancy, avo-controllers, and avo-custom-ui when the work spills into their territory.