Overview
Spree’s promotion system is built around two extension points: Rules (eligibility conditions) and Actions (what happens when a promotion applies). While Spree ships with a comprehensive set of built-in rules and actions, you can create custom ones for business-specific logic. This guide covers:- Creating a custom promotion rule with admin UI
- Creating a custom promotion action with a calculator
- Understanding the
eligible?,actionable?, andperformcontracts
Custom Promotion Rules
Rules determine whether a promotion is eligible for a given order. Each rule implementseligible? which returns true or false.
Step 1: Create the Rule Class
Create a new class inheriting fromSpree::PromotionRule:
app/models/spree/promotion/rules/minimum_quantity.rb
Key Methods to Implement
| Method | Required | Description |
|---|---|---|
applicable?(promotable) | Yes | Returns true if this rule type can evaluate the promotable (usually promotable.is_a?(Spree::Order)) |
eligible?(promotable, options = {}) | Yes | Returns true if the promotable meets this rule’s conditions. Add messages to eligibility_errors to explain why not. |
actionable?(line_item) | No | Returns true if a specific line item should receive the promotion’s action. Defaults to true. Override this for rules that target specific items (like product or category rules). |
options hash passed to eligible? can include :user, :email, and other context from the checkout flow.
Using Preferences
Rules use Spree’s preference system for configuration. Each preference creates getter/setter methods automatically::string, :integer, :decimal, :boolean, :array.
Step 2: Register the Rule
Add your rule to the promotion configuration so it appears in the admin panel:config/initializers/spree.rb
Step 3: Create the Admin Partial
Create a form partial so admins can configure the rule’s preferences. The partial name must match the rule class name in underscore format:app/views/spree/admin/promotions/rules/_minimum_quantity.html.erb
Step 4: Add Translations
config/locales/en.yml
Step 5: Restart and Test
After restarting your application, the new rule will be available in Admin > Promotions when adding rules to a promotion.Example: Rule with actionable?
When your rule targets specific line items (not the whole order), implement actionable? so that actions like CreateItemAdjustments only discount matching items:
app/models/spree/promotion/rules/brand.rb
Custom Promotion Actions
Actions define what happens when a promotion is applied. Most actions create adjustments on orders or line items.Step 1: Create the Action Class
Discount Action (with Calculator)
For actions that create monetary adjustments, includeSpree::CalculatedAdjustments and Spree::AdjustmentSource:
app/models/spree/promotion/actions/tiered_discount.rb
Non-Discount Action
For actions that don’t create adjustments (e.g., awarding points, sending notifications):app/models/spree/promotion/actions/add_loyalty_points.rb
Key Methods to Implement
| Method | Required | Description |
|---|---|---|
perform(options = {}) | Yes | Called when the promotion is activated. options includes :order and :promotion. Return true if the action was applied. |
compute_amount(adjustable) | For discount actions | Return the adjustment amount (negative for discounts). Cap at the adjustable’s total to prevent negative amounts. |
revert(options = {}) | No | Called when a promotion is deactivated. Use to undo side effects (e.g., remove added line items). |
Available Helper Methods
When you includeSpree::AdjustmentSource, you get:
Spree::CalculatedAdjustments, you get:
Step 2: Register the Action
config/initializers/spree.rb
Step 3: Add Translations
config/locales/en.yml
Step 4: Restart and Test
After restarting, the new action will be available in Admin > Promotions when adding actions to a promotion.How Rules and Actions Work Together
Understanding how Spree evaluates promotions helps you build better custom rules and actions: Key points:match_policy: 'all'means every rule must returneligible? == truematch_policy: 'any'means at least one rule must returneligible? == true- For item-level actions (
CreateItemAdjustments),actionable?(line_item)on each rule filters which line items get the discount - When multiple promotions compete, Spree picks the best one (largest discount) and marks others as ineligible
Testing Custom Rules and Actions
spec/models/spree/promotion/rules/minimum_quantity_spec.rb
spec/models/spree/promotion/actions/tiered_discount_spec.rb
Related Documentation
- Promotions - Promotion architecture and built-in rules/actions
- Calculators - Available calculator types for promotion actions
- Adjustments - How adjustments work on orders and line items
- Events - Subscribe to promotion events

