Lightning.Workflows (Lightning v2.19.0-pre)

View Source

The Workflows context.

Summary

Functions

Builds a Trigger

Creates a snapshot from a multi.

Returns an %Ecto.Changeset{} for tracking workflow changes.

Creates an edge

Whether another webhook trigger in the project already answers on this path.

Permanently deletes a workflow that has no history left.

Gets an Edge by its associated Trigger.

Returns a list of edges with jobs to execute, given a current timestamp in Unix. This is used by the scheduler, which calls this function once every minute.

Gets a single Webhook Trigger from the segments of an /i/ request path.

Gets a single workflow with optional preloads, returns nil if not found.

Gets a single workflow with optional preloads.

Gets a workflow by id, scoped to the given project.

Returns a list of workflows for a project with optional filtering, sorting, and preloading.

Whether any work orders are still recorded against workflow.

Returns the list of workflows for a project.

Returns the list of workflows.

Returns an %Ecto.Changeset{} for changing the workflow request_deletion.

Creates a latest snapshot for the given workflow if one does not already exist for the current lock_version. Returns {:ok, snapshot} if a snapshot exists or is created.

Permanently deletes workflows that were marked for deletion long enough ago and have no history left.

Computes the name_del-style name a workflow should take when it is soft deleted, so it frees up its original name for reuse within the project.

Marks a workflow deleted and frees its name for reuse, in one step.

See Lightning.Workflows.Events.subscribe/1.

Returns a workflow name that is unique within the given project, derived from base_name. A blank or nil base_name defaults to "Untitled workflow". On collision, appends " 1", " 2", etc. until a free name is found.

Updates a trigger

Updates the enabled state of triggers associated with a given workflow as a struct or as a changeset.

Checks if a workflow exists in the given project

Returns a query for workflows accessible to a user

Functions

build_trigger(attrs)

Builds a Trigger

capture_snapshot(multi)

Creates a snapshot from a multi.

When the multi already has a :workflow change, it is assumed to be changed or inserted and will attempt to build and insert a new snapshot.

When there isn't a :workflow change, it tries to find a dependant model like a Job, Trigger or Edge and uses the workflow associated with that model.

In this case we assume that the workflow wasn't actually updated, Workflow.touch() is called to bump the updated_at and the lock_version of the workflow before a snapshot is captured.

change_workflow(workflow, attrs \\ %{})

Returns an %Ecto.Changeset{} for tracking workflow changes.

Examples

iex> change_workflow(workflow)
%Ecto.Changeset{data: %Workflow{}}

create_edge(attrs, actor)

Creates an edge

custom_path_taken?(custom_path, project_id, except_trigger_id \\ nil)

@spec custom_path_taken?(term(), Ecto.UUID.t(), term()) :: boolean()

Whether another webhook trigger in the project already answers on this path.

Advisory, so the editor can say so while the field is still open. The partial unique index is what actually guarantees it, and a save still has to handle losing the race.

delete_workflow(workflow)

@spec delete_workflow(Lightning.Workflows.Workflow.t()) ::
  {:ok, Lightning.Workflows.Workflow.t()}
  | {:error, :has_history | Ecto.Changeset.t()}

Permanently deletes a workflow that has no history left.

Returns {:error, :has_history} for a workflow that still has work orders. Their history belongs to the project and is the data retention policy's to remove; until it does, work orders and steps hold RESTRICT references to the workflow's snapshots and the delete could not succeed anyway.

Deleting the workflow row cascades to its jobs, triggers, edges, snapshots, versions, templates and AI chat sessions. Project-scoped records the workflow merely referenced, dataclips above all, are left where they are: they belong to the project and outlive it.

get_edge_by_trigger(trigger)

Gets an Edge by its associated Trigger.

Parameters

  • %Trigger{id: trigger_id}: A Trigger struct from which the associated Edge is to be found.

Returns

  • Returns an Edge struct preloaded with its source_trigger and target_job if found.
  • Returns nil if no Edge is associated with the given Trigger.

Examples

trigger = %Trigger{id: 1, ...}
Lightning.Workflows.get_edge_by_trigger(trigger)
# => %Edge{source_trigger: %Trigger{}, target_job: %Job{}, ...}

non_existent_trigger = %Trigger{id: 999, ...}
Lightning.Workflows.get_edge_by_trigger(non_existent_trigger)
# => nil

get_edges_for_cron_execution(datetime)

@spec get_edges_for_cron_execution(DateTime.t()) :: [Lightning.Workflows.Edge.t()]

Returns a list of edges with jobs to execute, given a current timestamp in Unix. This is used by the scheduler, which calls this function once every minute.

get_webhook_trigger(segments, opts \\ [])

@spec get_webhook_trigger(
  [String.t()],
  keyword()
) :: Lightning.Workflows.Trigger.t() | nil

Gets a single Webhook Trigger from the segments of an /i/ request path.

Tried in order, and no step can match more than one row, so a request can never fail on an ambiguous path:

  1. A project id and a custom path, when there is a second segment.
  2. The first segment as a trigger id.
  3. A bare custom path, for the triggers that held one before paths were namespaced. That set is fixed at migration time and never grows.

Trailing segments are ignored: /i/<trigger-uuid>/Patient posts to the same trigger as /i/<trigger-uuid>.

get_workflow(id, opts \\ [])

Gets a single workflow with optional preloads, returns nil if not found.

Examples

iex> get_workflow(123)
%Workflow{}

iex> get_workflow(456)
nil

iex> get_workflow(123, include: [:triggers])
%Workflow{triggers: [...]}

get_workflow!(id, opts \\ [])

Gets a single workflow with optional preloads.

Raises Ecto.NoResultsError if the Workflow does not exist.

Examples

iex> get_workflow!(123)
%Workflow{}

iex> get_workflow!(456)
** (Ecto.NoResultsError)

iex> get_workflow!(123, include: [:triggers])
%Workflow{triggers: [...]}

get_workflow_for_project(project, id, opts \\ [])

Gets a workflow by id, scoped to the given project.

Returns nil when the id is malformed, missing, belongs to another project, or is marked for deletion, so it can't be used to read or mutate a workflow across projects or one on its way out.

get_workflows_for(project, opts \\ [])

Returns a list of workflows for a project with optional filtering, sorting, and preloading.

Parameters

  • project - A %Project{} struct for which to retrieve workflows
  • opts - Optional keyword list of options

Options

  • :search - String to filter workflows by name using case-insensitive partial matching
  • :order_by - A tuple containing the field and direction to sort by, e.g., {:name, :asc} or {:enabled, :desc}
  • :include - List of associations to preload (defaults to [:triggers, :edges, jobs: [:workflow]])

Returns

A list of %Workflow{} structs that match the criteria

Examples

# Get all workflows for a project
iex> get_workflows_for(project)
[%Workflow{}, ...]

# Search workflows containing "api" in their name
iex> get_workflows_for(project, search: "api")
[%Workflow{name: "API Gateway"}, %Workflow{name: "External API"}]

# Sort workflows by name in descending order
iex> get_workflows_for(project, order_by: {:name, :desc})
[%Workflow{name: "Zebra"}, %Workflow{name: "Apple"}]

# Search and sort combined
iex> get_workflows_for(project, search: "api", order_by: {:name, :desc})
[%Workflow{name: "REST API"}, %Workflow{name: "API Gateway"}]

# Customize preloaded associations
iex> get_workflows_for(project, include: [:triggers])
[%Workflow{triggers: [...]}, ...]

has_history?(workflow)

@spec has_history?(Lightning.Workflows.Workflow.t()) :: boolean()

Whether any work orders are still recorded against workflow.

list_project_workflows(project_id, opts \\ [])

Returns the list of workflows for a project.

Examples

iex> list_project_workflows(project_id)
[%Workflow{}, ...]

list_workflows()

Returns the list of workflows.

Examples

iex> list_workflows()
[%Workflow{}, ...]

mark_for_deletion(workflow, actor, attrs \\ %{})

Returns an %Ecto.Changeset{} for changing the workflow request_deletion.

Examples

iex> change_request_deletion(workflow)
%Ecto.Changeset{data: %Workflow{}}

maybe_create_latest_snapshot(workflow)

Creates a latest snapshot for the given workflow if one does not already exist for the current lock_version. Returns {:ok, snapshot} if a snapshot exists or is created.

Note

In normal situations this function is not needed as the snapshot is created when the workflow is saved.

perform(job)

Permanently deletes workflows that were marked for deletion long enough ago and have no history left.

A workflow becomes purgeable purge_deleted_after_days days after it was marked for deletion, and only once the last of its work orders is gone. Deleting a workflow's history is the data retention policy's job, not this one's: a workflow whose project keeps history forever is never purged, and one whose retention window is shorter than the purge window is purged on the first nightly sweep after its history expires.

An unset purge_deleted_after_days reads as zero days, in line with the other purge workers — the cron entry driving this is only registered when the setting is greater than zero.

project_workflows_using_credentials(project_credential_ids)

@spec project_workflows_using_credentials([project_credential :: Ecto.UUID.t(), ...]) ::
  %{
    optional(project :: Ecto.UUID.t()) => [workflow_name :: binary(), ...]
  }

resolve_name_for_pending_deletion(workflow)

@spec resolve_name_for_pending_deletion(Lightning.Workflows.Workflow.t()) ::
  String.t()

Computes the name_del-style name a workflow should take when it is soft deleted, so it frees up its original name for reuse within the project.

Used by soft_delete_changeset/1, which every delete path routes through.

save_workflow(changeset_or_attrs, actor, opts \\ [])

@spec save_workflow(
  Ecto.Changeset.t(Lightning.Workflows.Workflow.t()) | map(),
  struct(),
  keyword()
) ::
  {:ok, Lightning.Workflows.Workflow.t()}
  | {:error,
     Ecto.Changeset.t(Lightning.Workflows.Workflow.t())
     | :workflow_deleted
     | :snapshot_failed}

soft_delete_changeset(changeset)

Marks a workflow deleted and frees its name for reuse, in one step.

The single soft-delete transition both mark_for_deletion/3 and the provisioner route through, so a deleted workflow can never keep its name reserved on a hidden row.

subscribe(project_id)

See Lightning.Workflows.Events.subscribe/1.

to_project_space(workflows)

@spec to_project_space([Lightning.Workflows.Workflow.t()]) :: %{}

unique_workflow_name(base_name, project_id, opts \\ [])

@spec unique_workflow_name(String.t() | nil, Ecto.UUID.t(), keyword()) :: String.t()

Returns a workflow name that is unique within the given project, derived from base_name. A blank or nil base_name defaults to "Untitled workflow". On collision, appends " 1", " 2", etc. until a free name is found.

The check includes soft-deleted rows because the unique index on [:name, :project_id] is not partial. (Delete paths rename workflows to <name>_del via soft_delete_changeset/1, so in practice deletion frees the original name — but any row still occupying a name must be avoided.)

Note: this is check-then-insert, so two concurrent saves can still compute the same name and one will lose on the unique constraint. Callers already handle that {:error, changeset}; no retry is attempted here.

Options

  • :exclude_workflow_id - a workflow id whose current name should not count as a clash. Pass the workflow being renamed/edited so its own name doesn't get " 1" appended on every save.

update_trigger(trigger, attrs)

Updates a trigger

update_triggers_enabled_state(changeset, enabled?)

Updates the enabled state of triggers associated with a given workflow as a struct or as a changeset.

Parameters

  • workflow_or_changeset:
  • An %Ecto.Changeset{} containing a :triggers association.
  • A %Workflow{} struct with a triggers field.
  • enabled?:
  • A boolean indicating whether to enable (true) or disable (false) the triggers.

Returns

  • An updated %Ecto.Changeset{} with the :triggers association modified.
  • An updated %Ecto.Changeset{} derived from the given %Workflow{}.

Examples

Using an Ecto.Changeset

changeset = Ecto.Changeset.change(%Workflow{}, %{triggers: [%Trigger{enabled: false}]})
updated_changeset = update_triggers_enabled_state(changeset, true)
# The triggers in the changeset will now have `enabled: true`.

Using a Workflow struct

workflow = %Workflow{triggers: [%Trigger{enabled: false}]}
updated_changeset = update_triggers_enabled_state(workflow, true)
# The returned changeset will have triggers with `enabled: true`.

workflow_exists_in_project?(project_id, workflow_id)

Checks if a workflow exists in the given project

workflows_for_user_query(user)

@spec workflows_for_user_query(Lightning.Accounts.User.t()) :: Ecto.Queryable.t()

Returns a query for workflows accessible to a user