Runic is a purely functional workflow composition tool useful for dataflow parallel data pipelines, rule based expert systems, and low-code workflow engine functionality.
Runic is uniquely designed to be purely functional and lazily evaluated using a performant multigraph DAG & label indexed model. This design enables Runic to not assume runtime topology and support any user desired process model be it 1-node, distributed, or using any kind of process(s) for the rich Elixir / Erlang / OTP ecosystem.
Runic nodes are typically struct wrappers around a function to produce values that flow downward to next runnable nodes. Values are wrapped in %Fact{} structs and causal reactions are represented by labeled edges for memory.
mix test- Run all testsmix test test/specific_test.exs- Run specific test filemix test test/specific_test.exs:123- Run specific test linemix compile- Compile the projectmix format- Format code according to .formatter.exsmix deps.get- Get dependenciesmix clean- Clean compiled files
- Core modules:
Runic(main API mostly construction via macros),Runic.Workflow(workflow engine runtime) - Components: Step, Rule, Condition, Map, Reduce, StateMachine, Join
- Graph-based: Uses libgraph for DAG (directed acyclic graph) representation
- Uses an adjacency index in a multigraph structure for efficient traversal across kinds of edges
- Protocols:
Invokable,Component,Transmutablefor extensibility - Dataflow: Facts flow through Steps connected by edges in the workflow graph
- Use
mix formatfor automatic formatting - Follow Elixir naming: snake_case for variables/functions, PascalCase for modules
- Import order: Standard library, external deps, internal modules (alias first)
- Pattern matching preferred over conditionals
- Use
withfor multiple success/failure operations - Module attributes for compile-time configuration
- Protocols for extensible behavior (see existing Invokable, Component protocols)
-
Elixir lists do not support index based access via the access syntax
Never do this (invalid):
i = 0 mylist = ["blue", "green"] mylist[i]Instead, always use
Enum.at, pattern matching, orListfor index based list access, ie:i = 0 mylist = ["blue", "green"] Enum.at(mylist, i) -
Elixir supports
if/elsebut **does NOT supportif/else iforif/elsif. Never useelse iforelseifin Elixir, always usecondorcasefor multiple conditionals.Never do this (invalid):
<%= if condition do %> ... <% else if other_condition %> ... <% end %>Instead always do this:
<%= cond do %> <% condition -> %> ... <% condition2 -> %> ... <% true -> %> ... <% end %> -
Elixir variables are immutable, but can be rebound, so for block expressions like
if,case,cond, etc you must bind the result of the expression to a variable if you want to use it and you CANNOT rebind the result inside the expression, ie:# INVALID: we are rebinding inside the `if` and the result never gets assigned if connected?(socket) do socket = assign(socket, :val, val) end # VALID: we rebind the result of the `if` to a new variable socket = if connected?(socket) do assign(socket, :val, val) end -
Use
withfor chaining operations that return{:ok, _}or{:error, _} -
Never nest multiple modules in the same file as it can cause cyclic dependencies and compilation errors
-
Never use map access syntax (
changeset[:field]) on structs as they do not implement the Access behaviour by default. For regular structs, you must access the fields directly, such asmy_struct.fieldor use higher level APIs that are available on the struct if they exist,Ecto.Changeset.get_field/2for changesets -
Elixir's standard library has everything necessary for date and time manipulation. Familiarize yourself with the common
Time,Date,DateTime, andCalendarinterfaces by accessing their documentation as necessary. Never install additional dependencies unless asked or for date/time parsing (which you can use thedate_time_parserpackage) -
Don't use
String.to_atom/1on user input (memory leak risk) -
Predicate function names should not start with
is_and should end in a question mark. Names likeis_thingshould be reserved for guards -
Elixir's builtin OTP primitives like
DynamicSupervisorandRegistry, require names in the child spec, such as{DynamicSupervisor, name: MyApp.MyDynamicSup}, then you can useDynamicSupervisor.start_child(MyApp.MyDynamicSup, child_spec) -
Use
Task.async_stream(collection, callback, options)for concurrent enumeration with back-pressure. The majority of times you will want to passtimeout: :infinityas option
- Read the docs and options before using tasks (by using
mix help task_name) - To debug test failures, run tests in a specific file with
mix test test/my_test.exsor run all previously failed tests withmix test --failed mix deps.clean --allis almost never needed. Avoid using it unless you have good reason