Skip to content

2.26.0

Choose a tag to compare

@goldnead goldnead released this 18 Oct 03:02
· 94 commits to main since this release

2.26.0

Fixed: Mail rules on a flat_file install

The Mail rules screen read automations through Eloquent while its three sibling screens go through
the repository, so under flat_file the list showed the rules while Mail rules said "no rules".
It now uses AutomationRepository, and its setup guard leaves the definition tables out under
flat_file.

Fixed: a run could resume on the wrong automation on MySQL

AutomationRepository::find() and findByRef() compared the numeric id column with whatever they were
given. On MySQL id = '3f2a…' is id = 3: the engine reads the leading digits of a UUID as a number, so a
UUID starting with another automation's id returned that automation instead, ahead of the one it names. A run
resolves its automation by UUID, so it could resume on another graph. SQLite compares strings and never showed it.
The id is now only compared with a number.

Added: subscriber.email and subscriber.first_name on the grant triggers

entitlements.granted and entitlements.pending now put the grantee into the run as subscriber, the same
shape every other trigger gives. A subject of type email is the address itself; any other subject is looked up
as a Statamic user by id. Without a resolvable address both values are null. This is what lets marketing.send_email
find its recipient without a to, and what the re-entry policy (_restart_policy) uses as the subject key. Additive:
existing graphs read the same keys as before.

Added: node "Wait for Automation" (wait_for_automation)

  • Holds the run while the same person is still inside another automation, then reports how that one ended.
    Group Logic, evaluated again on every resume like Wait Until.

  • Config: email (tokenable, required), automation (handle or uuid, required), poll_hours (default 6, how often to ask
    again while the other run is queued, running or waiting), wait_if_missing_minutes (default 0: with no run of the
    person in the other automation yet, wait until that many minutes have passed since the start of this run, so a run
    that starts seconds later is not missed), settle (none, or next_day: after the other run ran to the end, wait
    until the next calendar day after it finished).

  • Output state (none, ran_to_end, opted_out, ended_early) and ran_to_end (bool, only for success without an
    opt-out). An opt-out of that automation counts as ended at once, even while its run waits on its next send step.
    Runs are matched by subject_key (the lower-cased address); test runs and the node's own run are left out.

  • The runner now puts _run (uuid, started_at) into the context next to _automation, so a node can count from the
    start of its own run.

  • Example, a second series that must never mail on the same day as the welcome series:

    trigger → wait_for_automation(email={{ contact.email }}, automation=welcome,
                                  wait_if_missing_minutes=10, settle=next_day)
            → branch(nodes.wait.state equals opted_out) → …
    

Added: action "Recent Grant" (entitlements.recent_grant)

  • Only with goldnead/statamic-entitlements installed. Asks whether a person has just unlocked something: the newest
    grant of the address that gives access now and whose activation (starts_at, created_at as fallback) lies at most
    within_minutes (default 60) back. Config: email (tokenable, required), within_minutes, sources (comma
    separated, empty = all).
  • Output found (bool), product_slug, product_title, source, granted_at (ISO 8601); always all five.
    Not found is a success with found false; test mode answers found false. In a branch use nodes.<key>.found
    equals true and the false edge; equals false is inverted (PHP compares the bool with the string "false").
  • product_title comes from the entitlements addon's product catalogue, then from the new config
    automations.entitlements.title_sources (['collection' => 'resources', 'match' => 'resource_slug', 'field' => 'title'],
    Statamic entries), then from the resource of a goldnead/statamic-lead-magnets grant. No hit leaves it empty, never
    the slug. Nothing in the addon knows a site's collections.

Added: the series opt-out page can be worded per automation

  • automations.sequence_opt_out.pages.<automation-handle> takes title, body, button, done, already_out,
    scope_note, link_label, link_url, all optional; a missing one keeps the language-file text, so a page without
    config is unchanged. :email in body and already_out becomes the address. link_url is a path on the site or an
    https:// URL from config (anything else shows no link) and appears on the page, also after the opt-out.
  • New language key sequence.back_link (default link label).

Upgrading

  • New config keys, all optional with working defaults, so publishing is not needed: automations.timezone
    (STATAMIC_AUTOMATIONS_TIMEZONE, default Europe/Berlin; the calendar day of settle: next_day),
    automations.entitlements.title_sources (default empty), automations.integrations.lead_magnets.grant_model,
    automations.sequence_opt_out.pages (default empty).
  • No migration. Publishing the config is optional: automations.ai.max_timeout (default 900) is new.
  • Queue worker: a long AI call only works if the worker lets it. Run workers with --timeout above the longest
    AI node timeout and set the connection's retry_after above that too, or the job is killed or run twice.
    RunAutomation now sets its own $timeout (sum of the automation's AI node timeouts + 60 s) so --timeout
    only needs to be at least that.
  • An automation with a synchronous trigger that can reach an ai_generate node no longer validates
    (ai_requires_queue); set the trigger to run in the background.
  • An ai_generate node inside the body of an inline loop no longer validates (ai_in_loop): it would run once per
    item, but the job timeout budgets it once. Put the AI call outside the loop or run a separate automation per item.

Added: Generate with AI for long inputs (ai_generate)

  • Per node: timeout (seconds, empty = ai.timeout), next to the existing model and max_tokens.
  • Output JSON object with an optional JSON Schema (type, properties, required, items, enum). The reply is decoded,
    checked, and every field is readable as {{ nodes.<node>.json.<field> }}; invalid JSON or a schema violation
    fails the node with a readable message. The token picker offers the schema's top-level fields.
  • The model call sits behind the AiClient contract (Anthropic by default) so tests can fake it.

Added: action "Fetch file from URL" (asset.fetch_from_url)

  • Downloads a file from a URL into an asset container, streamed to a temporary file and from there to the
    container's disk; memory stays flat. The download stops at the first byte over the limit
    (automations.assets.fetch.max_size_mb, default 500, or the node's own) and leaves nothing behind.
  • Optional connection: its headers or OAuth2 token are sent to the URL's own origin only. A 401 renews the
    token once and downloads again. Redirects are followed by hand (five hops at most), every hop passes the
    HostGuard, and the credential is dropped for good after the first change of origin.
  • Output for the next node: asset_id, path, size, mime_type. Without overwrite a taken name gets
    -1, -2; with it the old file and its meta are restored if anything fails.
  • Config: assets.fetch.max_size_mb (STATAMIC_AUTOMATIONS_ASSET_MAX_SIZE_MB) and assets.fetch.timeout
    (STATAMIC_AUTOMATIONS_ASSET_FETCH_TIMEOUT, the HTTP timeout of the download). Switchable under
    builtin_nodes.fetch_asset_from_url. The connection is only used for a URL on its own base URL. Works with any container, including private ones from statamic-private-media.

Added: OAuth2 connections and connection templates

  • Connections get the auth type oauth2_client_credentials: token URL, client ID and secret, scope,
    client_secret_basic or client_secret_post, and free extra token request fields (grant_type
    can be overridden, e.g. Zoom's account_credentials with account_id). Every
    connection.<x>.<op> call gets a valid token on its own.
  • The token is cached per connection until expires_in minus 60 s, encrypted. A 401 drops it, fetches
    one new token and repeats the request once; a second 401 fails the node with a clear message.
  • The token URL passes the HostGuard on save and before each token request; redirects are not followed.
    Client secret, extra token field values and access tokens are masked in output, errors and the run log.
  • Connection templates (Automations::registerConnectionTemplate()): picking one on the create form
    fills it in, and saving creates the connection and its operations in one transaction. Built in:
    zoom_server_to_server (recordings.list, recording.get) and deepgram (transcribe.url).
  • No migration.