Repository navigation
2.26.0
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, ornext_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) andran_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 bysubject_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-entitlementsinstalled. Asks whether a person has just unlocked something: the newest
grant of the address that gives access now and whose activation (starts_at,created_atas 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 withfoundfalse; test mode answersfoundfalse. In a branch usenodes.<key>.found
equalstrueand thefalseedge;equals falseis inverted (PHP compares the bool with the string "false"). product_titlecomes 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 agoldnead/statamic-lead-magnetsgrant. 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>takestitle,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.:emailinbodyandalready_outbecomes the address.link_urlis 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, defaultEurope/Berlin; the calendar day ofsettle: 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
--timeoutabove the longest
AI node timeout and set the connection'sretry_afterabove that too, or the job is killed or run twice.
RunAutomationnow 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_generatenode no longer validates
(ai_requires_queue); set the trigger to run in the background. - An
ai_generatenode 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 existingmodelandmax_tokens. - Output
JSON objectwith 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
AiClientcontract (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. Withoutoverwritea 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) andassets.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 fromstatamic-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_basicorclient_secret_post, and free extra token request fields (grant_type
can be overridden, e.g. Zoom'saccount_credentialswithaccount_id). Every
connection.<x>.<op>call gets a valid token on its own. - The token is cached per connection until
expires_inminus 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
HostGuardon 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) anddeepgram(transcribe.url). - No migration.