{"id":13879039,"url":"https://github.com/beehiiv/has_state_machine","last_synced_at":"2026-03-07T02:40:46.992Z","repository":{"id":41824021,"uuid":"292153080","full_name":"beehiiv/has_state_machine","owner":"beehiiv","description":"Simple and flexible state machines for your ActiveRecord classes","archived":false,"fork":false,"pushed_at":"2025-04-24T21:56:46.000Z","size":104,"stargazers_count":12,"open_issues_count":0,"forks_count":1,"subscribers_count":8,"default_branch":"main","last_synced_at":"2026-01-13T06:48:18.124Z","etag":null,"topics":["activerecord","ruby","state-machine"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/beehiiv.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"MIT-LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2020-09-02T02:00:44.000Z","updated_at":"2025-09-02T14:25:33.000Z","dependencies_parsed_at":"2025-07-16T23:26:18.389Z","dependency_job_id":"97b0398e-757e-4b8c-8299-2c4c0c3faf8e","html_url":"https://github.com/beehiiv/has_state_machine","commit_stats":{"total_commits":44,"total_committers":4,"mean_commits":11.0,"dds":0.4772727272727273,"last_synced_commit":"7a20ea321bd376569215a3d7c5ac6783825f0a23"},"previous_names":["encampment/has_state_machine"],"tags_count":13,"template":false,"template_full_name":null,"purl":"pkg:github/beehiiv/has_state_machine","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/beehiiv%2Fhas_state_machine","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/beehiiv%2Fhas_state_machine/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/beehiiv%2Fhas_state_machine/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/beehiiv%2Fhas_state_machine/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/beehiiv","download_url":"https://codeload.github.com/beehiiv/has_state_machine/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/beehiiv%2Fhas_state_machine/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30206138,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-06T19:07:06.838Z","status":"online","status_checked_at":"2026-03-07T02:00:06.765Z","response_time":53,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["activerecord","ruby","state-machine"],"created_at":"2024-08-06T08:02:07.707Z","updated_at":"2026-03-07T02:40:46.967Z","avatar_url":"https://github.com/beehiiv.png","language":"Ruby","funding_links":[],"categories":["Ruby"],"sub_categories":[],"readme":"# HasStateMachine\n\n[![CI](https://github.com/beehiiv/has_state_machine/actions/workflows/ci.yml/badge.svg)](https://github.com/beehiiv/has_state_machine/actions/workflows/ci.yml)\n[![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)\n\nHasStateMachine uses ruby classes to make creating a finite state machine for your ActiveRecord models a breeze.\n\n## Contents\n\n- [HasStateMachine](#hasstatemachine)\n  - [Contents](#contents)\n  - [Installation](#installation)\n  - [Usage](#basic-usage)\n    - [Basic Usage](#basic-usage)\n    - [Validations \u0026 Error Handling](#validations-and-error-handling)\n    - [Advanced Usage](#advanced-usage)\n  - [Contributing](#contributing)\n  - [License](#license)\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'has_state_machine'\n```\n\nAnd then execute:\n\n```bash\n$ bundle\n```\n\nOr install it yourself as:\n\n```bash\n$ gem install has_state_machine\n```\n\n## Basic Usage\n\nYou must first use the `has_state_machine` macro to define your state machine at\na high level. This includes defining the possible states for your object as well\nas some optional configuration should you want to change the default behavior of\nthe state machine (more on this later).\n\n```ruby\n# By default, it is assumed that the \"state\" of the object is\n# stored in a string column named \"status\".\nclass Post \u003c ApplicationRecord\n  has_state_machine states: %i[draft published archived]\nend\n```\n\nNow you must define the classes for the states in your state machine. By default,\n`HasStateMachine` assumes that these will be under the `Workflow` namespace following\nthe pattern of `Workflow::#{ObjectClass}::#{State}`. The state classes must inherit\nfrom `HasStateMachine::State`.\n\n```ruby\nmodule Workflow\n  class Post::Draft \u003c HasStateMachine::State\n    # Define the possible transitions from the \"draft\" state\n    state_options transitions_to: %i[published archived]\n  end\nend\n\nmodule Workflow\n  class Post::Published \u003c HasStateMachine::State\n    state_options transitions_to: %i[archived]\n\n    # Custom validations can be added to the state to ensure a transition is \"valid\"\n    validate :title_exists?\n\n    def title_exists?\n      return if object.title.present?\n\n      # Errors get added to the ActiveRecord object\n      errors.add(:title, \"can't be blank\")\n    end\n  end\nend\n\nmodule Workflow\n  class Post::Archived \u003c HasStateMachine::State\n    # There are callbacks for running logic before and after\n    # a transition occurs.\n    before_transition do\n      Rails.logger.info \"== Post is being archived ==\\n\"\n    end\n\n    after_transition do\n      Rails.logger.info \"== Post has been archived ==\\n\"\n\n      # You can access the previous state of the object in\n      # after_transition callbacks as well.\n      Rails.logger.info \"== Transitioned from #{previous_state} ==\\n\"\n    end\n  end\nend\n```\n\nSome examples:\n\n```ruby\npost = Post.create(status: \"draft\")\n\npost.status.transition_to(:published) # =\u003e false\npost.status                           # =\u003e \"draft\"\n\npost.title = \"Foobar\"\npost.status.transition_to(:published) # =\u003e true\npost.status                           # =\u003e \"published\"\n\npost.status.transition_to(:archived)\n# == Post is being archived ==\n# == Post has been archived ==\n# == Transitioned from published ==\n# =\u003e true\n```\n\nIf you'd like to check that an object can be transitioned into a new state, use the `can_transition?` method. This checks to see if the provided argument is in the `transitions_to` array defined on the object's current state. (This does not run any validations that may be defined on the new state)\n\nExample:\n```ruby\npost = Post.create(status: \"draft\")\n\npost.status.can_transition?(:published) # =\u003e true\npost.status.can_transition?(:other_state) # =\u003e false\n```\n\n### Validations and Error Handling\n\nYou can define custom validations on a given state to determine whether an object in that state or a transition to that state is valid.\n\nBy default, validations defined on the state will be run as part of the object validations if the object is in that state.\n\n```ruby\npost = Post.create(status: \"published\", title: \"Title\")\n\npost.valid?\n# =\u003e true\n\npost.title = nil\npost.valid?\n# =\u003e false\n```\n\nIf you wish to change this behavior and not have the state validations run on the object, you can specify that with the `state_validations_on_object` option when defining your state machine.\n\n```ruby\nclass Post \u003c ApplicationRecord\n  has_state_machine states: %i[draft published, archived], state_validations_on_object: false\nend\n\npost = Post.create(status: \"published\", title: \"Title\")\n\npost.valid?\n# =\u003e true\n\npost.title = nil\npost.valid?\n# =\u003e true\n```\n\nBy default, when attempting to transition an object to another state, it checks:\n  * Validations defined on the object\n  * That the new state is one of the allowed transitions from the current state\n  * Any validations defined on the new state\n\nIf any are found to be invalid, the transition will fail. Any errors from validations on the new state will be added to the object.\n\n```ruby\npost = Post.create(status: \"draft\")\n\npost.title = nil\npost.status.transition_to(:published)\n# =\u003e false\n\npost.errors.full_messages\n# =\u003e [\"Title can't be blank\"]\n```\n\nIf you wish to bypass this behavior and skip validations during a transition, you can do that:\n\n```ruby\npost = Post.create(status: \"draft\")\n\npost.title = nil\npost.status.transition_to(:published, skip_validations: true)\n# =\u003e true\n```\n\n### Advanced Usage\n\n#### Transactional Transitions\n\nThere may be a situation where you want to manually rollback a state change in one of the provided transition callbacks. To do this, add the `transactional: true` option to the `state_options` declaration. This results in the transition being wrapped in a transaction. You can then use the `rollback_transition` method in your callback when you want to trigger a rollback of the transaction. This will allow you to prevent the transition from persisting if something further down the line fails.\n\n```ruby\nmodule Workflow\n  class Post::Archived \u003c HasStateMachine::State\n    state_options transactional: true\n\n    after_transition do\n      rollback_transition unless notified_watchers?\n    end\n\n    private\n\n    def notified_watchers?\n      #...\n    end\n  end\nend\n```\n\n#### Transient Transition Variables\n\nSometimes you may may want to pass additional arguments to a state transition for additional context in your transition callbacks. To do this, add the `transients` option to the `state_options` declaration. This allows you to define any additional attributes you want to be able to pass along during a state transition to that state.\n\n```ruby\nmodule Workflow\n  class Post::Archived \u003c HasStateMachine::State\n    state_options transients: %i[user]\n\n    after_transition do\n      puts \"== Post archived by #{user.name} ==\"\n    end\n  end\nend\n\ncurrent_user = User.create(name: \"John Doe\")\npost = Post.create(status: \"published\")\n\npost.status.transition_to(:archived, user: current_user)\n# == Post archived by John Doe ==\n# =\u003e true\n```\n\n## Contributing\n\nAnyone is encouraged to help improve this project. Here are a few ways you can help:\n\n- [Report bugs](https://github.com/encampment/has_state_machine/issues)\n- Fix bugs and [submit pull requests](https://github.com/encampment/has_state_machine/pulls)\n- Write, clarify, or fix documentation\n- Suggest or add new features\n\nTo get started with development:\n\n```\ngit clone https://github.com/encampment/has_state_machine.git\ncd has_state_machine\nbundle install\nbundle exec rake test\n```\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbeehiiv%2Fhas_state_machine","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbeehiiv%2Fhas_state_machine","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbeehiiv%2Fhas_state_machine/lists"}