{"id":32166929,"url":"https://github.com/msramos/ex_pipeline","last_synced_at":"2026-02-19T07:02:47.217Z","repository":{"id":50699402,"uuid":"519876013","full_name":"msramos/ex_pipeline","owner":"msramos","description":"An opinionated library to build features as pipelines.","archived":false,"fork":false,"pushed_at":"2022-11-29T19:37:35.000Z","size":50,"stargazers_count":33,"open_issues_count":1,"forks_count":2,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-10-31T22:16:05.317Z","etag":null,"topics":["elixir","elixir-library"],"latest_commit_sha":null,"homepage":"","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/msramos.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-07-31T20:04:19.000Z","updated_at":"2024-06-16T22:00:34.000Z","dependencies_parsed_at":"2023-01-23T13:15:59.574Z","dependency_job_id":null,"html_url":"https://github.com/msramos/ex_pipeline","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/msramos/ex_pipeline","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/msramos%2Fex_pipeline","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/msramos%2Fex_pipeline/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/msramos%2Fex_pipeline/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/msramos%2Fex_pipeline/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/msramos","download_url":"https://codeload.github.com/msramos/ex_pipeline/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/msramos%2Fex_pipeline/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29605803,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-19T06:47:36.664Z","status":"ssl_error","status_checked_at":"2026-02-19T06:45:47.551Z","response_time":117,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"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":["elixir","elixir-library"],"created_at":"2025-10-21T15:14:22.682Z","updated_at":"2026-02-19T07:02:47.211Z","avatar_url":"https://github.com/msramos.png","language":"Elixir","readme":"# ExPipeline\n\n![Build Status](https://github.com/msramos/ex_pipeline/actions/workflows/ci.yml/badge.svg?branch=main)\n[![Module Version](https://img.shields.io/hexpm/v/ex_pipeline.svg)](https://hex.pm/packages/ex_pipeline)\n[![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/ex_pipeline/)\n[![License](https://img.shields.io/hexpm/l/ex_pipeline.svg)](https://github.com/msramos/ex_pipeline/blob/main/LICENSE)\n\nExPipeline is an opinionated library to build better pipelines.\n\nA pipeline is set of functions that must be executed in a specific order to transform an initial state into a desired\nstate. For example, a \"login pipeline\" uses the request body as its initial state and generates an authentication token.\n\nIt allows a feature to expressed as a set of functions, like the following snippet:\n\n```elixir\ndefmodule MyFeature do\n  use Pipeline\n\n  def parse_step(value, options) do\n    ...\n  end\n\n  def fetch_address_step(value, options) do\n     ...\n  end\n\n  def final_step(value, options) do\n    ...\n  end\n\n  def reporter_async_hook(%Pipeline.State{} = state, options) do\n    ...\n  end\nend\n```\n\nLater on, you can execute this feature by calling the generated `execute/2` function or the `Pipeline.execute/3`\nfunction:\n\n```elixir\nMyFeature.execute(some_value, some_options)\n# or\nPipeline.execute(MyPipeline, some_value, some_options)\n```\n\nThese functions will return an ok/error tuple, so you can execute them with a `case` block , for example:\n\n```elixir\ncase MyFeature.execute(params, options) do\n  {:ok, succesful_result} -\u003e\n    ...\n\n  {:error, error_description} -\u003e\n    ...\nend\n```\n\n## Creating Pipelines\n\nTo create a pipeline, the target module **must** `use Pipeline`, and the functions must follow some patterns.\n\n* Functions that are part of the pipeline must end with `_step`, `_hook` or `_async_hook`.\n* They must accepts two parameters\n\n### Steps\n\nEach step modify a _state_. The result of one step is given to the next step, until the last step. Then the result\nis evaluated and returned.\n\n* Steps are executed in the same order that they are declared.\n* The first parameter is whatever was passed to the pipeline, and each step transforms this value to the next value.\n* The second parameter is an optional and immutable keyword list that is passed to all steps.\n* A step **must** return an on/error tuple - `{:ok, any}` or `{:error, any}`.\n* If one step fails, the next steps are not executed.\n\n### Hooks and Async Hooks\n\nHooks and async hooks are executed after all steps have completed, regardless of their result.\n\n* Async hooks are functions whose name end with `_async_hook` and hooks are functions whose name end with `_hook`.\n* Both types of hooks  **must** accept two parameters. The difference is that hooks receive the final `Pipeline.State`\nstruct with the execution result. Hooks return are ignored.\n  * The first parameter is the last version of the `Pipeline.State` struct from the evaluation of the last step.\n  * The second parameter is the same optional and immutable keyword list that is passed to all step.\n* After all steps are executed, the pipeline will launch all __async hooks__ on isolated processes, and run them in\n  parallel.\n* After all steps are executed, the pipeline will execute all __hooks__, in the same order that they were declared.\n\n\n## Why?\n\nAs features get more complex with time, Elixir pipes and `with` blocks can become harder to understand. Also, functions\nthat are added to them over time don't really have a spec to follow.\n\nLet's take this simple checkout code as example:\n\n```elixir\nwith %Payment{} = payment \u003c- fetch_payment_information(params),\n     {:ok, user} \u003c- Session.get(conn, :user),\n     address when !is_nil(address) \u003c- fetch_address(user, params),\n     {:ok, order} \u003c- create_order(user, payment, address) do\n  conn\n  |\u003e put_flash(:info, \"Order completed!\")\n  |\u003e render(\"checkout.html\")\nelse\n  {:error, :payment_failed} -\u003e\n    handle_error(conn, \"Payment Error\")\n\n  %Store.OrderError{message: message} -\u003e\n    handle_error(conn, \"Order Error\")\n\n  error -\u003e\n    handle_error(conn, \"Unprocessable order\")\nend\n```\n\nWe can make it look better by applying some code styles and get somethig like this:\n\n```elixir\noptions = %{conn: conn}\n\nwith {:ok, payment} \u003c- fetch_payment_information(params, options),\n     {:ok, user} \u003c- fetch_user(conn),\n     {:ok, address} \u003c- fetch_address(%{user: user, params: params}, options),\n     {:ok, order} \u003c- create_order(%{user: user, address: address, payment: payment}, options)\n  do\n  conn\n  |\u003e put_flash(:info, \"Order completed!\")\n  |\u003e redirect(to: Routes.order_path(conn, order))\nelse\n  {:error, error_description} -\u003e\n    conn\n    |\u003e put_flash(:error, parse_error(error_description))\n    |\u003e render(\"checkout.html\")\nend\n```\n\nThis is definitely easier to understand, but since the code style is not enforced, it may not look like this for too\nlong, specially if it's something that's being actively maintained.\n\nUsing `ex_pipeline`, we can express this `with` block like this:\n\n```elixir\ncase Checkout.execute(params, conn: conn) do\n  {:ok, order} -\u003e\n    conn\n    |\u003e put_flash(:info, \"Order completed!\")\n    |\u003e redirect(to: Routes.order_path(conn, order))\n\n  {:error, error_description} -\u003e\n    conn\n    |\u003e put_flash(:error, parse_error(error_description))\n    |\u003e render(\"checkout.html\")\nend\n```\n\nInside `Checkout`, all functions will look the same, and any modifications must also follow the same pattern.\n\n## Installation\n\nAdd the [Hex package](https://hex.pm/packages/ex_pipeline) by adding `ex_pipeline` to your list of dependencies in\n`mix.exs`:\n\n```elixir\ndef deps do\n  [\n    {:ex_pipeline, \"~\u003e 0.2.0\"}\n  ]\nend\n```\n\nThen make sure the `ex_pipeline` application is being loaded.\n\n## Code of Conduct\n\nThis project uses Contributor Covenant version 2.1. Check [CODE_OF_CONDUCT.md](/CODE_OF_CONDUCT.md) file for more information.\n\n## License\n\n`ex_pipeline` source code is released under Apache License 2.0.\n\nCheck [NOTICE](/NOTICE) and [LICENSE](/LICENSE) files for more information.","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmsramos%2Fex_pipeline","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmsramos%2Fex_pipeline","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmsramos%2Fex_pipeline/lists"}