Skip to content

Paper Trail Whatdunnit

Paper Trail answers two questions when a row changes: who did it (whodunnit) and what code path was responsible (whatdunnit in controller_info).

For a normal browser request, Rails sets that up for you. For a CMDx task — a webhook handler, a nightly job, a workflow step — you are the code path. Stamping the task class name on each write makes audit logs honest.

Setup

Middleware sets whatdunnit for the duration of the task and restores the previous value when the task finishes. That way nested tasks do not leak their name into the parent's writes.

class CmdxPaperTrailMiddleware
  def call(task)
    PaperTrail.request.controller_info ||= {}
    previous = PaperTrail.request.controller_info[:whatdunnit]
    PaperTrail.request.controller_info[:whatdunnit] = task.class.name

    yield
  ensure
    PaperTrail.request.controller_info[:whatdunnit] = previous
  end
end

Usage

Register the middleware on the task (or on ApplicationTask if every task should be attributed).

class CancelSubscription < CMDx::Task
  register :middleware, CmdxPaperTrailMiddleware.new

  required :subscription_id, coerce: :integer
  required :reason,          coerce: :string

  def work
    subscription = Subscription.find(subscription_id)
    subscription.update!(canceled_at: Time.current, cancel_reason: reason)
  end
end

What to expect

After the task runs, version rows can point at the task class:

SELECT whodunnit, object_changes FROM versions WHERE item_id = 42;
-- whodunnit | object_changes
-- ----------+----------------------------------------------------
-- (NULL)    | { "canceled_at": [null, "2026-05-13T..."], ... }

SELECT controller_info->>'whatdunnit' FROM versions WHERE item_id = 42;
-- "CancelSubscription"

Set whodunnit separately (for example current_user.id) if you also need a human actor on the version.

Notes

Restore in ensure

RegisterUser calling SendWelcomeEmail should not leave SendWelcomeEmail on the user row's version. Saving and restoring whatdunnit in ensure keeps parent and child tasks from stepping on each other.