I need to change my graph. How?

If the change does not break the flow, simply make the change, test, and deploy it.

If the change breaks the flow, create a new graph, with a new name and/or version. Switch your code to create executions of the new graph instead of the old graph, and new executions will follow the new flow.

As long as the old graph continues to be registered, old executions will continue to navigate the flow defined in the old graph.

Examples of changes that might break the flow:

  • Changing upstream dependencies on pre-existing computation nodes.
  • Removing a node that other nodes still depend on, or renaming a node.

Note that removing a node that nothing depends on is not a breaking change — you can do it in place, keeping the same graph name and version (see "Removing a node that nothing depends on" below).

Anything that would have the current executions go "Huh? Now what do I do??!?" is likely to be a breaking change, that would require creating a new graph.

However, if you don't care about the state of current or old executions, you can archive them all (Journey.list_executions(graph_name: graph.name, graph_version: graph.version) |> Enum.each(fn e -> Journey.archive(e.id) end)), and simply change your existing graph, even if changes are major.

Examples

A minor change (no new graph needed)

Updating this graph

graph = Journey.new_graph(
  "zodiac",
  "v1.0.0",
  [
    input(:name),
    input(:birthday),
    compute(:zodiac_sign, [:name, :birthday], &compute_zodiac/1)
  ]
)

with a couple of extra independent or downstream nodes is not a breaking change, and it does not require a new graph definition:

graph = Journey.new_graph(
  "zodiac",
  "v1.0.0",
  [
    input(:name),
    input(:birthday),
    # new input node:
    input(:pet_preference),
    compute(:zodiac_sign, [:name, :birthday], &compute_zodiac/1),
    # new computation:
    compute(:horoscope, [:zodiac_sign, :pet_preference], &compute_horoscope/1)
  ]
)

If a graph was updated to include new nodes, the executions of this graph will be upgraded to include those new nodes when they are loaded.

Removing a node that nothing depends on (no new graph needed)

Removing a node that nothing else depends on is also not a breaking change. You can remove it in place, keeping the same graph name and version.

For example, removing :horoscope from the graph above (nothing depends on it):

graph = Journey.new_graph(
  "zodiac",
  "v1.0.0",
  [
    input(:name),
    input(:birthday),
    input(:pet_preference),
    compute(:zodiac_sign, [:name, :birthday], &compute_zodiac/1)
    # :horoscope removed — nothing depended on it
  ]
)

Existing executions keep working without a new graph. A few things to keep in mind:

  • The removed node's previously computed value stays in existing executions' history (and is still visible via Journey.values/2), but it is inert — nothing reads it and nothing recomputes it.
  • Stop calling Journey.get/3 for the removed node: it is no longer part of the graph, so that call will raise.
  • This applies to a node that nothing depends on. If other nodes depend on it, remove or update those nodes first — otherwise it is a breaking change (create a new graph).

A major change (need a new graph)

If you need to make :zodiac_sign dependent on having the user's credit card number (for some reason), you'll likely need a new graph (or have existing executions enter an ambiguous state).

graph = Journey.new_graph(
  "zodiac",
  # new version!
  "v2.0.0",
  [
    input(:name),
    input(:birthday),
    # new input node:
    input(:cc),
    # new upstream prerequisite:
    compute(:zodiac_sign, [:name, :birthday, :cc], &compute_zodiac/1)
  ]
)

Then make sure your configuration registers both graphs:

config :journey, :graphs, [
  &MyApp.Graphs.Zodiac.V1.graph/0,
  &MyApp.Graphs.Zodiac.V2.graph/0
]

The strategy described here applies to evolving a graph due to changing requirements or fixing of bugs in the code or in the flow.