Skip to content
Back to skills

Create Service

ASecurity

Scaffold a service, form, policy, value, or query object following project patterns, structure, and naming. TRIGGER when the user wants to extract business logic into a service/command object or create a form/policy/value/query object. Does NOT modify existing app code beyond the new object.

  • 2 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 5, 2026
ai-agentsgorubyrails

Works with

  • cli

Security analysis

A100/100

Scanned September 5, 2026

npx -y skills add mik2win/foureyes --skill create-service --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Create Service?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Create Service
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/mik2win-create-service/badge)](https://www.skillsdirectory.com/skills/mik2win-create-service)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: create-service
description: >-
  Scaffold a service, form, policy, value, or query object following project patterns,
  structure, and naming.
  TRIGGER when the user wants to extract business logic into a service/command object or
  create a form/policy/value/query object. Does NOT modify existing app code beyond the new
  object.
allowed-tools:
  - Read
  - Grep
  - Glob
  - Write
  - Edit
---

# Create Service Object Skill

You are a senior Ruby/Rails developer. Generate a business logic object following project conventions defined in the `rails-business-logic` rule.

## 1. Parse Arguments

Parse `$ARGUMENTS` to determine:

- **Object name** (required): e.g., `OrderFulfiller`, `RegistrationForm`, `Orders::Creator`
- **`--type=` flag** (optional): `service` | `form` | `policy` | `value` | `query`. Default: `service`.

If the name contains `Form`, `Policy`, `Query`, or `Value` suffix and no `--type` is given, infer the type from the name.

## 2. Determine Target Directory and File Path

| Type | Directory | Example |
|------|-----------|---------|
| service | `app/services/` | `app/services/order_fulfiller.rb` |
| form | `app/forms/` | `app/forms/registration_form.rb` |
| policy | `app/policies/` | `app/policies/order_policy.rb` |
| value | `app/values/` | `app/values/money.rb` |
| query | `app/queries/` | `app/queries/overdue_orders_query.rb` |

For namespaced classes (e.g., `Orders::Creator`), create subdirectories: `app/services/orders/creator.rb`.

Convert CamelCase to snake_case for file names.

## 3. Check for Existing Patterns

Before generating, search the target directory for existing files to match the project's style:

- Check if there is a base class or shared module used by other objects of the same type.
- Match the style of existing objects (e.g., if services use `Result` objects, use them too).
- If an `ApplicationPolicy` base class exists, inherit from it for policy objects.

## 4. Generate the File

### Service Object

```ruby
# frozen_string_literal: true

class OrderFulfiller
  def initialize(order, notifier: OrderMailer)
    @order = order
    @notifier = notifier
  end

  def call
    ActiveRecord::Base.transaction do
      @order.update!(status: :fulfilled, fulfilled_at: Time.current)
    end

    @notifier.with(order: @order).fulfilled.deliver_later
    @order
  end
end
```

Key conventions:
- Named as a **noun** (not a verb): `OrderFulfiller`, not `FulfillOrder`
- Single `#call` method for simple services; multi-method facade for complex ones
- **Dependency injection** for collaborators (mailers, external clients, etc.)
- Wrap multi-model writes in `ActiveRecord::Base.transaction`
- Return a meaningful result: the record, a `Result` object, or similar
- Use `Time.current` / `Date.current`, never `Time.now`

For namespaced services:

```ruby
# frozen_string_literal: true

module Orders
  class Creator
    def initialize(user)
      @user = user
    end

    def call(params)
      ActiveRecord::Base.transaction do
        @user.orders.create!(params)
      end
    rescue ActiveRecord::RecordInvalid => e
      e.record
    end
  end
end
```

### Form Object

```ruby
# frozen_string_literal: true

class RegistrationForm
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :name, :string
  attribute :email, :string
  attribute :company_name, :string

  validates :name, :email, :company_name, presence: true
  validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }

  def save
    return false unless valid?

    ActiveRecord::Base.transaction do
      company = Company.create!(name: company_name)
      User.create!(name:, email:, company:)
    end
  rescue ActiveRecord::RecordInvalid => e
    errors.merge!(e.record.errors)
    false
  end
end
```

Key conventions:
- `include ActiveModel::Model` and `include ActiveModel::Attributes`
- Declare attributes with types
- Validations before `#save` / `#submit`
- `#save` returns `false` on failure (like ActiveRecord)
- Wrap multi-model writes in transaction
- Merge record errors on failure for user-friendly messages

### Policy Object

```ruby
# frozen_string_literal: true

class OrderPolicy < ApplicationPolicy
  def show?
    owner? || user.admin?
  end

  def update?
    owner? && record.editable?
  end

  def destroy?
    user.admin?
  end

  private

  def owner?
    record.user_id == user.id
  end
end
```

Key conventions:
- Inherit from `ApplicationPolicy` if it exists; otherwise create a standalone class with `attr_reader :user, :record` and an initializer
- Methods return **boolean** values
- Method names end with `?` and match controller actions: `show?`, `create?`, `update?`, `destroy?`
- Takes `user` and `record` in constructor
- Private helper methods for shared predicates (`owner?`, `admin?`)

### Value Object

```ruby
# frozen_string_literal: true

Money = Data.define(:amount, :currency) do
  def initialize(amount:, currency: 'USD')
    super(amount: BigDecimal(amount.to_s), currency:)
  end

  def +(other)
    raise ArgumentError, 'Currency mismatch' unless currency == other.currency

    Money.new(amount: amount + other.amount, currency:)
  end

  def to_s
    "#{currency} #{format('%.2f', amount)}"
  end
end
```

Key conventions:
- Use `Data.define` (Ruby 3.2+) -- immutable by default
- Equality is by value (built into `Data.define`)
- No setters, no mutation
- Override `initialize` for defaults or coercion
- Add domain-specific methods (arithmetic, formatting)

### Query Object

```ruby
# frozen_string_literal: true

class OverdueOrdersQuery
  def initialize(scope = Order.all)
    @scope = scope
  end

  def call(grace_period: 30.days)
    @scope
      .where(status: :placed)
      .where(placed_at: ...grace_period.ago)
      .where(paid_at: nil)
      .includes(:customer)
      .order(placed_at: :asc)
  end
end
```

Key conventions:
- Takes a relation (default `Model.all`), returns a relation
- Composable: `OverdueOrdersQuery.new(company.orders).call`
- Named with `Query` suffix
- Single `#call` method with keyword arguments for parameters
- Use `includes` / `preload` for eager loading when appropriate

## 5. Post-Generation

- Confirm the file was created and show its path.
- If a namespaced module directory was created, mention it.
- Do NOT create test files unless explicitly asked.
- Do NOT auto-run any commands.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.