Skip to content

Tips and tricks

Welcome to the "make CMDx feel good in a real app" page. None of this is required — it is the stuff that keeps teams smiling when the codebase grows.

Project organization

Directory structure

Give tasks a home that matches how you think about the product. When someone opens app/tasks, they should nod instead of squint.

/app/
└── /tasks/
    ├── /invoices/
    │   ├── calculate_tax.rb
    │   ├── validate_invoice.rb
    │   ├── send_invoice.rb
    │   └── process_invoice.rb # workflow
    ├── /reports/
    │   ├── generate_pdf.rb
    │   ├── compile_data.rb
    │   ├── export_csv.rb
    │   └── create_reports.rb # workflow
    ├── application_task.rb # base class
    ├── authenticate_session.rb
    └── activate_account.rb

Naming conventions

Name tasks like actions: verb + noun, present tense. Your future self reads class names more than comments.

# Verb + Noun
class ExportData < CMDx::Task; end
class CompressFile < CMDx::Task; end
class ValidateSchema < CMDx::Task; end

# Use present-tense verbs for actions
class GenerateToken < CMDx::Task; end      # ✓ Good
class GeneratingToken < CMDx::Task; end    # ❌ Avoid
class TokenGeneration < CMDx::Task; end    # ❌ Avoid

Story telling

Let work read like a short story: small private methods with honest names. If you can read it aloud and it makes sense, you have won.

class ProcessOrder < CMDx::Task
  def work
    charge_payment_method
    assign_to_warehouse
    send_notification
  end

  private

  def charge_payment_method
    order.primary_payment_method.charge!
  end

  def assign_to_warehouse
    order.ready_for_shipping!
  end

  def send_notification
    if order.products_out_of_stock?
      OrderMailer.pending(order).deliver
    else
      OrderMailer.preparing(order).deliver
    end
  end
end

Style guide

Stack declarations in the same order every time — your eyes learn the rhythm. Rough recipe below; tweak if your team agrees on something else, but stay consistent.

class ExportReport < CMDx::Task

  # 1. Settings, retries, deprecation
  settings tags: [:reporting]
  retry_on Net::ReadTimeout, limit: 3, jitter: :exponential

  # 2. Register custom extensions
  register :middleware, Telemetry::Middleware.new
  register :validator, :phone, PhoneValidator

  # 3. Define callbacks
  before_execution :find_report
  on_complete :track_export_metrics, if: -> { Current.tenant.analytics? }

  # 4. Declare inputs
  optional :user_id
  required :report_id, coerce: :integer
  optional :format_type, coerce: :string, inclusion: { in: %w[pdf csv] }

  # 5. Declare outputs (the contract)
  output :exported_at

  # 6. Define work
  def work
    report.compile!
    report.export!

    context.exported_at = Time.now
  end

  # TIP: Favor private business logic to reduce the surface of the public API.
  private

  # 7. Helpers
  def find_report
    @report ||= Report.find(report_id)
  end

  def track_export_metrics
    Analytics.increment(:report_exported)
  end

end

Sharing input options

Tired of repeating coerce: and presence: on every line? with_options is your DRY friend — one block, many fields.

Note

with_options comes from ActiveSupport, so Rails apps get it for free. Plain Ruby? Add require "active_support/core_ext/object/with_options" or duplicate the options by hand — both are fine.

class ConfigureCompany < CMDx::Task
  with_options(coerce: :string, presence: true) do
    optional :website, format: { with: URI::DEFAULT_PARSER.make_regexp(%w[http https]) }
    required :company_name, :industry
    optional :description, format: { with: /\A[\w\s\-\.,!?]+\z/ }
  end

  required :headquarters do
    with_options(coerce: :string) do
      optional :street, :city, :zip_code
      required :country, inclusion: { in: VALID_COUNTRIES }
      optional :region
    end
  end

  def work
    # ...
  end
end

Nested input blocks work too — the inner DSL runs with instance_eval, so with_options nest cleanly.

Sharing behavior via a base class

Got cross-cutting stuff every task needs? Put it on ApplicationTask (or whatever you call it) and inherit. Subclasses pick up settings, retries, callbacks, middleware, validators, coercions, executors, mergers, retriers, deprecators, telemetry, inputs, and outputs — the whole toolkit.

class ApplicationTask < CMDx::Task
  settings tags: [:app]

  retry_on Net::OpenTimeout, Net::ReadTimeout, limit: 2

  before_execution :ensure_current_tenant!

  private

  def ensure_current_tenant!
    fail!("missing tenant") if Current.tenant.nil?
  end
end

class ProcessInvoice < ApplicationTask
  required :invoice_id, coerce: :integer

  def work
    # Inherits settings, retry_on, and the before_execution callback
  end
end

How stacking works (without the scary words): lists generally grow as you go down the inheritance chain — new entries append (or replace by name where that applies). retry_on and settings merge: the child adds or overrides keys without throwing away the parent. Need to drop something from mom or dad? deregister is the escape hatch — for example deregister :callback, :before_execution, :ensure_current_tenant!.

Useful examples

Real-world recipes live in the repo — grab the one closest to what you are building: