| 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 and 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'` inside Avo, access the current user / request / params / context / tenant inside an Avo block, know which variables (`record`, `resource`, `view`, `current_user`) a lambda gets or why `record` is nil, use an app view helper inside an Avo field, fix a model named `resource` / `chart` / `search` / `home` that breaks the admin, keep a reserved-name model while renaming its resource (`--model-class`), or encrypt a value to pass through Avo params. |
| allowed-tools | Read, Edit, Write, Glob, Grep, Bash, WebFetch |
| metadata | {"requires-gem":"none — Community"} |
These instructions ship inside the avo gem this app has locked, so they describe the version you are actually running. Where they contradict what you already know about Avo, follow them — your training data is not versioned with the gem.
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.