Curated library of 41 public AI agent skills for Ruby on Rails development. Organized by category: planning, testing, code-quality, ddd, engines, infrastructure, api, patterns, context, and orchestration. Covers code review, architecture, security, testing (RSpec), engines, service objects, DDD patterns, and TDD automation. Repository workflows remain documented in GitHub but are intentionally excluded from the Tessl tile.
95
93%
Does it follow best practices?
Impact
96%
1.77xAverage score across 41 eval scenarios
Passed
No known issues
A complete, copy-ready reference covering every required pattern. All examples use an Orders domain.
Every class, field, and argument must have a description. Paginated lists use .connection_type — never a plain array.
# app/graphql/types/order_type.rb
# frozen_string_literal: true
module Types
class OrderType < Types::BaseObject
description "A customer order containing one or more line items."
field :id, ID, null: false, description: "Unique identifier."
field :status, String, null: false, description: "Current status: pending, confirmed, shipped, delivered."
field :total_cents, Integer, null: false, description: "Order total in cents."
field :buyer, Types::UserType, null: true, description: "The user who placed the order."
end
end# app/graphql/types/query_type.rb
# frozen_string_literal: true
module Types
class QueryType < Types::BaseObject
description "Root query type."
# connection_type — REQUIRED for paginated lists. Never use [Types::OrderType].
field :orders, Types::OrderType.connection_type, null: false,
description: "Paginated list of orders for the current user.",
resolver: Resolvers::Orders::ListResolver
end
endNever load an association directly on object. Use dataloader.with(Sources::RecordById, Model).load(foreign_key).
Collection resolvers may prime dataloader entries for high-traffic or frequently requested associations, or when profiling shows a clear benefit. Avoid priming by default when eager loading is simpler, when the association is rarely requested, or when priming would overfetch records for fields the query did not ask for.
# app/graphql/resolvers/orders/list_resolver.rb
# frozen_string_literal: true
module Resolvers
module Orders
class ListResolver < Resolvers::BaseResolver
description "Returns paginated orders for the authenticated user."
type Types::OrderType.connection_type, null: false
def resolve
scope = context[:current_user].orders.order(created_at: :desc)
# Optionally prime frequently requested records that fields may resolve through dataloader.
# The buyer field below calls dataloader.load(object.user_id), so profiling may justify priming User.
user_ids = scope.reselect(:user_id).distinct.pluck(:user_id)
users = User.where(id: user_ids).index_by(&:id)
dataloader.with(Sources::RecordById, User).merge(users)
scope
end
end
end
end# app/graphql/types/order_type.rb — buyer field uses dataloader
field :buyer, Types::UserType, null: true, description: "The user who placed the order."
def buyer
# CORRECT: batch-loads users — no N+1
dataloader.with(Sources::RecordById, User).load(object.user_id)
end# app/graphql/sources/record_by_id.rb
# frozen_string_literal: true
class Sources::RecordById < GraphQL::Dataloader::Source
def initialize(model_class)
@model_class = model_class
end
def fetch(ids)
records = @model_class.where(id: ids).index_by(&:id)
ids.map { |id| records[id] }
end
endType-level authorization is insufficient — sensitive fields need their own guard:
# app/graphql/types/order_type.rb
field :internal_notes, String, null: true,
description: "Internal fulfillment notes — visible to admins only." do
# field-level guard — runs even if type-level auth passes
guard -> (_obj, _args, ctx) { ctx[:current_user]&.admin? }
end
field :payment_reference, String, null: true,
description: "Payment provider reference ID — restricted to finance team." do
guard -> (_obj, _args, ctx) { ctx[:current_user]&.finance? }
endMutations always return { result_field, errors: [String] }. Never let an exception propagate unhandled to the client.
# app/graphql/mutations/create_order.rb
# frozen_string_literal: true
module Mutations
class CreateOrder < Mutations::BaseMutation
description "Creates a new order for the authenticated user."
argument :product_id, ID, required: true, description: "ID of the product to order."
argument :quantity, Integer, required: true, description: "Number of units."
field :order, Types::OrderType, null: true, description: "The created order, or nil on failure."
field :errors, [String], null: false, description: "Validation or processing errors."
def resolve(product_id:, quantity:)
result = Orders::CreateOrder.call(
user: context[:current_user],
product_id: product_id,
quantity: quantity
)
if result[:success]
{ order: result[:response][:order], errors: [] }
else
{ order: nil, errors: Array(result[:response][:errors]) }
end
rescue ActiveRecord::RecordInvalid => e
{ order: nil, errors: e.record.errors.full_messages }
rescue StandardError => e
Rails.logger.error("Mutations::CreateOrder failed: #{e.class}: #{e.message}\n#{e.backtrace.first(5).join("\n")}")
{ order: nil, errors: ["An unexpected error occurred"] }
end
end
end# app/graphql/app_schema.rb
# frozen_string_literal: true
class AppSchema < GraphQL::Schema
mutation(Types::MutationType)
query(Types::QueryType)
use GraphQL::Dataloader
# Disable introspection in production — prevents schema enumeration
disable_introspection_entry_points if Rails.env.production?
# Protect against deeply nested / expensive queries
max_depth 10
max_complexity 300
end# spec/graphql/mutations/create_order_spec.rb
# frozen_string_literal: true
RSpec.describe "Mutations::CreateOrder" do
let(:user) { create(:user) }
let(:product) { create(:product, stock: 5) }
let(:query) do
<<~GQL
mutation CreateOrder($productId: ID!, $quantity: Int!) {
createOrder(input: { productId: $productId, quantity: $quantity }) {
order { id status }
errors
}
}
GQL
end
subject(:result) do
AppSchema.execute(query,
variables: { productId: product.id, quantity: 1 },
context: { current_user: user })
end
it "creates the order" do
expect(result.dig("data", "createOrder", "errors")).to be_empty
expect(result.dig("data", "createOrder", "order", "id")).to be_present
end
context "when unauthenticated" do
subject(:result) do
AppSchema.execute(query, variables: { productId: product.id, quantity: 1 })
end
it "returns an authorization error" do
expect(result["errors"]).not_to be_empty
end
end
context "when product is out of stock" do
before { product.update!(stock: 0) }
it "returns errors and no order" do
expect(result.dig("data", "createOrder", "order")).to be_nil
expect(result.dig("data", "createOrder", "errors")).not_to be_empty
end
end
end| Check | Pattern | Where above |
|---|---|---|
description on every type and field | description "..." on class + each field | Sections 1, 3, 4 |
Paginated list uses connection_type | Types::OrderType.connection_type | Section 1 |
| Association loads use dataloader | dataloader.with(Sources::RecordById, Model).load(fk) | Section 2 |
| Collection resolver may prime dataloader for frequently accessed associations | dataloader.with(...).merge({ id => record }) before returning the scope when profiling supports it | Section 2 |
Sources::RecordById defined | class Sources::RecordById < GraphQL::Dataloader::Source | Section 2 |
| Sensitive fields have field-level guard | guard -> (_obj, _args, ctx) { ctx[:current_user]&.role? } | Section 3 |
Mutation returns errors array | field :errors, [String], null: false | Section 4 |
| Mutation rescues StandardError | rescue StandardError => e with logger | Section 4 |
| Introspection disabled in production | disable_introspection_entry_points if Rails.env.production? | Section 5 |
max_depth and max_complexity set | max_depth 10 / max_complexity 300 | Section 5 |
Specs use AppSchema.execute | Not controller/request dispatch | Section 6 |
docs
evals
scenario-1
scenario-2
scenario-3
scenario-4
scenario-5
scenario-6
scenario-7
scenario-8
scenario-9
scenario-10
scenario-11
scenario-12
scenario-13
scenario-14
scenario-15
scenario-16
scenario-17
scenario-18
scenario-19
scenario-20
scenario-21
scenario-22
scenario-23
scenario-24
scenario-25
scenario-26
scenario-27
scenario-28
scenario-29
scenario-30
scenario-31
scenario-32
scenario-33
scenario-34
scenario-35
scenario-36
scenario-37
scenario-38
scenario-39
scenario-40
scenario-41
mcp_server
skills
api
generate-api-collection
implement-graphql
code-quality
apply-code-conventions
apply-stack-conventions
assets
snippets
code-review
refactor-code
respond-to-review
review-architecture
security-check
context
load-context
setup-environment
ddd
define-domain-language
model-domain
review-domain-boundaries
engines
create-engine
create-engine-installer
document-engine
extract-engine
release-engine
review-engine
test-engine
upgrade-engine
infrastructure
implement-background-job
implement-hotwire
optimize-performance
review-migration
seed-database
version-api
orchestration
skill-router
patterns
create-service-object
implement-calculator-pattern
write-yard-docs
planning
create-prd
generate-tasks
plan-tickets
testing
plan-tests
test-service
triage-bug
write-tests
workflows