Lyfe for Craft CMS

Usage

How a workflow works

A workflow has three parts, and the editor asks for them in this order:

  1. When: one trigger. An order was placed, an order moved into "shipped", somebody registered, a cart went quiet.
  2. Only if: conditions. Rules in a group must all be true; any one group being true is enough. No conditions at all means the workflow runs every time its trigger fires.
  3. Then: an ordered list of steps. Each step has a delay that is waited before it runs, so the list reads top to bottom as a timeline.
When    an order is placed
Only if it is the customer's first order
Then    immediately    tag them "first-time-buyer"
        after 2 days   email "Welcome, a few things worth knowing"
        after 12 days  email "How did we do?"

Each triggered instance of a workflow is a run. You can watch runs under Lyfe → Activity, open one to see exactly what each step did, and cancel or advance it by hand.

Delays

A delay can be in minutes, hours, days or weeks. The first step's delay is counted from the moment the trigger fired; every later step's delay is counted from when the step before it ran.

A step with no delay (shown as "immediately") runs straight away. If the first step has no delay, it runs during the request that triggered the workflow, which for an order trigger is the customer's checkout. A failure there is caught and logged and cannot stop the checkout, but an immediate email does add the time it takes to send. To keep that work off the customer's request entirely, give the first step a delay of a minute or more. Every step after a delay is run by the scheduler (see Installation).

The Safety section

OptionWhat it does
EnabledOff means the trigger never starts this workflow. Leave it off until you have tested the workflow in the simulator.
Dry runRuns normally but sends nothing. Every step is recorded, and emails and texts appear in the message log as "simulated".
Re-check conditions before every stepOn by default. Each time a run resumes after a delay or a retry, the conditions are evaluated again; if they no longer match, the rest of the run is dropped and recorded as skipped. Leave this on for anything with a delay: the customer may have unsubscribed, refunded or bought again.
StoreRestrict the workflow to one Commerce store. All stores is the default.
Run at most this many times per contact (Pro)Once a contact has this many runs of the workflow, of any outcome, it will not start for them again.
Cooldown per contact (Pro)Minutes. A contact who started this workflow inside the cooldown will not start it again.
Send window (Pro)From and To in 24-hour HH:MM, plus Only on these days.

The send window is in your site's timezone. When a step comes due outside the window, the run waits until the window next opens instead of sending at four in the morning. Overnight windows such as 22:00 to 06:00 work. If the days you tick mean the window never opens in the coming week, Lyfe sends anyway and logs a warning, rather than holding the campaign forever.

What stops a workflow running twice

Every run carries a deduplication key, and the database refuses a second run of the same workflow with the same key. A retried webhook, a Commerce event that fires twice, or an overlapping sweep collides there and is dropped silently, because that is the point.

TriggerRuns at most once per workflow for each
Order placedorder
Order paid in fullorder
Order status changedtime the order enters a status (an order that is re-shipped is a new event)
Order refundedrefund transaction (each partial refund is separate)
Specific product purchasedorder
Account createduser
Cart abandonedabandonment (a customer who comes back and abandons again is a new event)
Tag added to a contacttime the tag is added
Scheduled contact scancontact, per period (see the trigger)
Manual onlynever deduplicated; use a run limit or cooldown

Workflows that trigger each other (one changes an order status, which triggers another that changes it back) are stopped after three levels and the refusal is logged.

Retries and stale steps

A step that fails, for example because the mail server refused the message, is retried after Wait before retrying minutes, up to Retry a failed step attempts in total. After that the run is marked failed and its error is shown on the run.

A step that came due more than Abandon steps overdue by more than hours ago is abandoned, not sent late. See Configuration.

Triggers

TriggerHandleFires whenSettings
Order placedorder.placedCommerce completes an order.None
Order paid in fullorder.paidAn order's balance reaches zero. For offline payments this can be days after it was placed.None
Order status changedorder.status.changedAn order moves into a status.Moved into (any status if none ticked), Moved out of (optional)
Order refundedorder.refundedA refund transaction succeeds.None
Specific product purchasedorder.product.purchasedA completed order contains one of the chosen products.Products, or Or SKUs (one per line, case-insensitive). With neither set, it never fires.
Account createduser.createdA new Craft user is saved, whether they registered on the front end or were created in the control panel.None
Manual onlymanualNever on its own. Started by hand; see Starting a workflow by hand.None
Cart abandoned (Pro)cart.abandonedA tracked cart with items in it has been inactive for longer than the abandonment window.Minimum cart value, Only carts with an email address (on by default)
Tag added to a contact (Pro)contact.taggedA tag the contact did not already have is added, by a workflow or in the control panel.Tags (one per line, case-insensitive; empty means any tag)
Scheduled contact scan (Pro)scheduled.contactsOn a timer, for each contact that passes the prefilter.See below

Scheduled contact scan

Nothing happens when a customer stops buying, so there is no event to listen for. A scan walks your contacts instead. It is what win-backs and VIP tagging are built on.

SettingWhat it does
How often to scanDaily, weekly or monthly. A scan runs when that long has passed since the last one.
At least this many days since the last orderPrefilter. Contacts who have never ordered are excluded.
At most this many days since the last orderPrefilter.
At least this many ordersPrefilter.
Repeat for the same contact everyDays. Empty means at most once per contact, ever. 0 means once per scan period. A number means once per window of that many days.

The prefilter fields are a fast database filter over the figures Lyfe keeps on each contact. They are optional: your conditions are still evaluated against live Commerce data for every candidate, so a stale figure can only cost a wasted check, never a wrong send. Every contact that passes the prefilter is checked, in batches of 500, so on a large store a focused prefilter keeps the scan quick.

The figures are kept up to date as orders complete. After a bulk import, or if you suspect they have drifted, run php craft lyfe/contacts/sync-stats.

A store restriction on the workflow does not narrow a scan: contacts are not tied to a store.

Conditions

Conditions are grouped in the editor by what they look at. Text comparisons ignore case and surrounding spaces.

Order

ConditionHandleCompares
Order totalorder.totalThe order's total price
Order subtotal (items only)order.itemTotalLine items before shipping, tax and order-level adjustments
Number of itemsorder.itemCountTotal quantity across line items
Products in the orderorder.productsProducts on the order's line items
SKUs in the orderorder.skusSKUs on the order's line items
Coupon code usedorder.couponThe coupon code
Payment gatewayorder.gatewayThe gateway's handle
Shipping countryorder.shippingCountryThe shipping address's country
Order statusorder.statusThe order's current status
Order currencyorder.currencyThe order's currency code
Is the customer's first orderorder.isFirstWhether this contact completed any order before this one. Asked relative to the order, so a step nine days later gets the answer that was true at checkout.
Order is paid in fullorder.isPaidWhether the order's balance is zero

Customer

ConditionHandleCompares
Email addresscontact.emailThe contact's email
Phone numbercontact.phoneThe contact's phone number. "is not empty" is the useful check before an SMS.
Has a user accountcontact.hasAccountWhether the contact has a Craft user account
Lifetime order countcontact.orderCountCompleted orders, read live from Commerce
Lifetime spendcontact.totalSpendTotal of completed orders, read live from Commerce
Days since first ordercontact.daysSinceFirstOrderWhole days since the first completed order
Days since last ordercontact.daysSinceLastOrderWhole days since the most recent completed order. A contact who has never ordered has no value, so "at least 120" does not match them.
Tagscontact.tagsThe contact's tags
User groupscontact.userGroupsThe user account's groups
Subscribed to marketing emailcontact.subscribedEmailThe contact's email consent
Subscribed to SMScontact.subscribedSmsThe contact's SMS consent

Lifetime figures match orders by the contact's email address and by their user account, so a customer who ordered both as a guest and logged in is counted once, as one person. Spend is a plain sum of order totals across every store and currency.

Timing

ConditionHandleCompares
Trigger datetrigger.dateWhen the trigger fired
Day of the weektrigger.dayOfWeekThe day the trigger fired, in the site's timezone
Hour of the day (0-23)trigger.hourOfDayThe hour the trigger fired, in the site's timezone

Workflow

ConditionHandleCompares
Times this workflow has runworkflow.runCountAll runs of this workflow. A blunt cap on a campaign.
Times this contact has been through this workflowworkflow.contactRunCountThis contact's runs of this workflow. Lets you act on the count ("on the third reminder, add a discount") rather than only refuse.

Operators

Which operators a condition offers depends on what it compares:

ValueOperators
Numbers, amounts, daysis, is not, is greater than, is at least, is less than, is at most
A choice from a list (status, gateway, country, day)is, is not, is one of, is none of
A set (products, SKUs, tags, user groups)contains (any of), does not contain, is empty, is not empty
Yes or nois
A dateis greater than, is at least, is less than, is at most
Textis, is not, contains, does not contain, starts with, ends with, is one of, is none of, is empty, is not empty

A numeric condition that has nothing to compare fails rather than counting as zero. "Order total is less than 10" does not match a workflow started for a contact with no order.

Actions

ActionHandleWhat it does
Send an emailemail.sendRenders the subject and body as Twig and sends through Craft's mailer.
Add tags to the contactcontact.tag.addAdds tags (one per line; Twig allowed). On Pro, a new tag fires Tag added to a contact.
Remove tags from the contactcontact.tag.removeRemoves tags.
Add a note to the orderorder.noteWrites a line into the order's history without changing its status, so staff can see what the automation did.
Send a webhookwebhook.postSends a POST, PUT or GET to a URL. Admin-only; see Who can write what.
Write a note in the run loglog.noteRecords a line on the run and does nothing else. Useful while you are working a workflow out.
Stop the run if...run.stopIfEnds the run when its own conditions match, or when they no longer match.
Send an SMS (Pro)sms.sendTexts the contact through the configured transport.
Change the order status (Pro)order.status.setMoves the order into another status, with an optional history message. Commerce's own status emails fire as usual.
Add to a user group (Pro)user.group.addAdds the contact's user account to groups, keeping its existing groups. Skipped for guests.
Remove from a user group (Pro)user.group.removeRemoves the account from groups.
Generate a discount code (Pro)commerce.discount.codeAdds a unique coupon code to one of your Commerce discounts.
Unsubscribe the contact (Pro)contact.unsubscribeTurns off marketing email, SMS, or both.
Cancel this contact's pending runs (Pro)run.cancelOthersCancels the contact's waiting runs of the chosen workflows, or of every workflow on the chosen triggers. With nothing chosen, it cancels every other workflow's runs. A choice that matches no workflow cancels nothing.

Send an email

SettingNotes
Subject, BodyTwig. The body is sent as HTML, with a plain-text version generated from it, and is wrapped in your email layout template if you set one.
ToLeave empty to send to the contact.
CC, BCCSeparate several addresses with commas, semicolons or new lines.
Reply toOverrides the default reply-to address.
TransactionalSends even to contacts who have unsubscribed from marketing email. Only correct for receipts and shipping notices.

Unless Transactional is on, an email to an unsubscribed contact is not sent. The refusal is recorded in the message log as "suppressed", and the run stops there, so later steps do not run. This applies even when To is set to a different address, because the run is still about that contact.

The Post-purchase thank you and Alert the team to a large order presets ship with Transactional on. Review that before you turn them on.

Always include {{ unsubscribeUrl }} in marketing email.

Send an SMS

The body is Twig. Keep it under 160 characters after rendering if you want it to go as a single segment. To overrides the recipient; leave it empty to text the contact's own number.

There is no transactional override for SMS. A contact who has not opted in to SMS is never texted; the attempt is recorded as "suppressed" and the run stops.

Stop the run if...

Stop when is either the conditions match or the conditions no longer match, and the conditions use the same builder as the workflow's own. The usual use is in an abandoned-cart sequence: before the second email, stop if the order has been paid.

Generate a discount code

Lyfe does not create discounts. Set up the discount in Commerce, with the rules you want, and choose it here. The step adds a new coupon code to it, so the discount's own rules decide what the code is worth.

SettingNotes
DiscountDiscounts from every store are listed.
Code prefixDefault LYFE. Twig allowed. Letters, numbers and hyphens are kept and uppercased.
Random charactersDefault 8, between 4 and 24. Codes are checked for uniqueness against existing coupons.
Maximum usesDefault 1. 0 means unlimited.
Offer expiry for the email (days)Default 30. 0 for none. Sets {{ data.discountExpiry }}; see below.

The code is available to later steps as {{ data.discountCode }}, and the expiry date as {{ data.discountExpiry }}.

Two things to know:

  • The expiry is for your copy, not enforced by Commerce. Commerce coupons have no expiry date of their own, so the code stays valid until it reaches its maximum uses or the discount itself ends. If a code must stop working on a date, set an end date on the discount.
  • The code is kept with the run, so the email that uses it can come any time later in the workflow, after a delay or a retry.

Send a webhook

SettingNotes
URLTwig allowed.
MethodPOST, PUT or GET. GET sends no body.
BodyTwig. Leave empty to send Lyfe's own JSON summary: the workflow handle, when it triggered, the contact (id, email, phone, names, tags), the order (id, number, reference, total, currency, email, status) and data.
Content typeDefault application/json.
Extra headersOne Header: value per line. A value that is exactly $VARIABLE is replaced with that environment variable. Put any prefix such as Bearer inside the variable.
Timeout (seconds)Default 10.

Any 2xx response counts as success. Anything else fails the step, records the status and the first 300 characters of the response, and is retried like any other failure.

Variables in emails and SMS

Every text setting that says "Twig" is rendered against the run. Subjects work too: Your order {{ order.reference }}.

VariableWhat it is
contactThe contact: email, firstName, lastName, phone, and their tags
orderThe order, for order and cart triggers. Empty for account, tag, scan and manual triggers.
userThe Craft user account, if there is one
workflowThe workflow: name, handle
statsLive purchase history: orderCount, totalSpend, firstOrderDate, lastOrderDate
unsubscribeUrlThe contact's personal unsubscribe link
cartRestoreUrlFor an abandoned cart (Pro), a link that puts the cart back in the customer's session. Empty otherwise, and empty once the cart has been turned into an order.
dataValues specific to the trigger or earlier steps (below)

data contains:

KeySet by
data.fromStatus, data.toStatusOrder status changed (status handles)
data.refundAmountOrder refunded
data.itemTotal, data.lineItemCountCart abandoned
data.tagTag added to a contact
data.discountCode, data.discountExpiryGenerate a discount code
<p>Hi {{ contact.firstName ?? 'there' }},</p>

<p>Your order {{ order.reference }} is on its way.</p>

{% if stats.orderCount > 1 %}<p>Thanks for coming back.</p>{% endif %}

<p><a href="{{ unsubscribeUrl }}">Unsubscribe</a></p>

In an untrusted step

A step that a non-admin wrote and no admin has approved renders in Craft's Twig sandbox, against plain values rather than objects (see Who can write what). The same variable names work, with these values:

  • contact: id, email, firstName, lastName, fullName, phone, tags
  • order: id, number, shortNumber, reference, email, currency, isCompleted, dateOrdered, itemSubtotal, itemSubtotalAsCurrency, totalPrice, totalPriceAsCurrency, totalQty, and lineItems, each with description, sku, qty, price, priceAsCurrency, total, totalAsCurrency
  • user: id, email, firstName, lastName, fullName
  • workflow: name, handle
  • stats, data, unsubscribeUrl and cartRestoreUrl, as above

Use the ...AsCurrency values for prices, because the commerceCurrency filter is not allowed in the sandbox.

{% for item in order.lineItems %}
  {{ item.qty }} x {{ item.description }}: {{ item.totalAsCurrency }}
{% endfor %}
Total: {{ order.totalPriceAsCurrency }}

On your own templates

craft.lyfe is available in front-end templates:

MethodReturns
craft.lyfe.contact(email)The contact for an address, or for the logged-in user with no argument
craft.lyfe.unsubscribeUrl(contactOrEmail)That contact's unsubscribe link
craft.lyfe.cartRestoreUrl(order, redirect)The restore link for a tracked cart, optionally redirecting afterwards
craft.lyfe.cart(order)Lyfe's tracking record for a cart
craft.lyfe.tags(contact)A contact's tags
craft.lyfe.isPro()Whether the Pro edition is active

The simulator

Lyfe → Simulator answers "would this run, and if not, why not?" against a real order or contact. Enter an order ID, or a contact's email, optionally limit it to one workflow, and click Simulate. For each workflow you get:

  • anything blocking it: Lyfe switched off, the workflow disabled, Pro required on Lite, the trigger's own settings ruling the subject out, or the contact being over the run limit
  • every condition, with the actual value and whether it matched
  • every step, with when it would land and what it would do: who it would email and with what subject, which tags it would add, which runs it would cancel
✗ Win back a lapsed customer
    · The conditions do not match.
    ✓ Subscribed to marketing email (yes) is yes
    ✗ Days since last order (12) is at least 120

The simulator uses the same code as a live run, in simulation mode, so what it shows is what the engine would do. It sends nothing, but simulated emails and texts are written to the message log marked "simulated" so you can read the rendered copy. For triggers that carry extra data, the simulator fills in a plausible stand-in, for example the first status a status trigger listens for.

Links such as Test against an order in the workflow editor and Simulate against this order on Commerce's order screen open the simulator with the form filled in. Nothing runs until you click Simulate.

From the console:

php craft lyfe/workflows/simulate 1234          # enabled and disabled workflows

Dry run

A dry run is a live run that sends nothing. Turn it on per workflow under Safety, or for everything with Global dry run in the settings. Real triggers start real runs, delays are waited, conditions are re-checked, and every step records what it would have done. Emails and texts are logged as "simulated" with their rendered content. Actions that change data (tags, order notes, status changes, discount codes, cancellations) are not performed either; the run records what they would have done.

Dry run is the way to watch a week of real traffic before going live.

Activity, messages and the log

ScreenShows
ActivityEvery run, filterable by status and workflow. Open a run to see each step's outcome and the messages it produced. Run now advances a waiting run immediately (and resets its retry count); Cancel stops it; Process due steps now does what the cron job does.
MessagesEvery email and SMS: sent, failed, suppressed (by consent) or simulated, with the rendered content.
LogEngine events outside any single run: workflows skipped because they need Pro, failures caught during a customer's request, scans, consent changes made on the unsubscribe page. Filter by level and category (engine, runs, carts, scans, email, consent).

Commerce's order edit screen also gets a Lyfe panel showing the runs for that order and the contact's recent messages.

Contacts, consent and tags

Lyfe keeps its own contact list, because most Commerce customers never register an account and consent has to be stored somewhere. A contact is identified by email address, without regard to case, so a guest order, a registered account and a manual entry for the same address are one contact.

Lyfe creates a contact when an order or a new user account starts one of your workflows, and on Pro for every tracked cart with an email address (see Create contacts automatically in Configuration). An order that no enabled workflow listens for does not create one, so run the backfill below to cover your existing customers. You can also add and edit contacts under Lyfe → Contacts.

Each contact has:

  • Email, First name, Last name and Phone. Names come from the order's billing address, or the user account. A phone number is taken from the address only if your address field layout has a field with the handle phone. Lyfe fills in blanks as it learns more but never overwrites a value that is already set.
  • Tags, used for segments. Edit them as a comma-separated list on the contact screen.
  • Subscribed to marketing email and Subscribed to SMS.
  • Purchase history, read live from Commerce, with Refresh the cached figures to update the copy that scheduled scans filter on.
  • Recent runs and messages, and the contact's unsubscribe link.

To build contacts from orders placed before you installed Lyfe:

php craft lyfe/contacts/backfill

It is safe to run again; existing contacts are updated, never duplicated.

Consent

Email and SMS consent are separate. New contacts get the defaults from the settings: subscribed to email, not subscribed to SMS.

A contact can change their own consent through {{ unsubscribeUrl }}, which opens a page where they choose which channels to keep. You can change it on the contact screen, with the Unsubscribe the contact action (Pro), or from the console:

php craft lyfe/contacts/unsubscribe someone@example.com    # email and SMS

Lyfe never sends marketing email to a contact who has unsubscribed from it, and never texts a contact who has not opted in to SMS. Each refusal is recorded in the message log as "suppressed". Steps that do not send anything, such as tagging, still run up to that point.

Tags and chaining (Pro)

Adding a tag fires Tag added to a contact, so one workflow can hand off to the next. A weekly scan tags big spenders vip; a separate workflow triggered by the vip tag sends a thank-you and adds them to a user group. Neither workflow needs to know about the other.

Starting a workflow by hand

On a contact's screen, Run a workflow for this contact starts any workflow for them. From the console, lyfe/workflows/run-for-tag starts one for every contact with a tag, after asking you to confirm:

php craft lyfe/workflows/run-for-tag spring-campaign wholesale

A manual start ignores whether the workflow is enabled and what its trigger is set to listen for. It does not ignore its conditions, its run limit or its cooldown: those protect the customer. A manual run has no order attached, so conditions about the order fail. Use the Manual only trigger for workflows that only ever start this way.

Carts (Pro)

With Track carts on, Lyfe records when each cart with items in it was last touched. When a cart has been inactive for Consider a cart abandoned after minutes, the next sweep marks it abandoned and fires Cart abandoned. Sweeps run with lyfe/run/due, or from Check for abandoned carts now on the Carts screen.

  • A cart emptied by the customer stops being tracked.
  • A customer who comes back to an abandoned cart puts it back in play. If they abandon it again, that is a new abandonment and starts a new run.
  • When an abandoned cart becomes an order it is marked recovered; a cart that became an order without being abandoned is marked placed.
  • Carts inactive for longer than Stop tracking a cart after days are dropped.

The Carts screen shows each cart's status, value and last activity, and totals for abandoned and recovered value.

{{ cartRestoreUrl }} links to Commerce's own load-cart action, which puts the cart back in the customer's browser session.

A complete abandoned-cart setup is two presets: Abandoned cart recovery sends two emails and stops if the order is paid in between, and Stop chasing recovered carts cancels any pending chase when the customer places an order.

The preset library

Every preset installs disabled, with copy marked EDIT ME. Installing one twice gives two separate workflows.

PresetHandleEditionWhat it does
Post-purchase thank youpost-purchase-thanksLiteEmail an hour after checkout. Transactional.
Review requestreview-requestLiteEmail 14 days after the order moves into completed.
Replenishment reminderreplenishmentLiteEmail 45 days after a chosen product is bought. Choose the products before enabling.
New account welcomewelcome-accountLiteWelcome email, then a nudge three days later if they have not ordered.
First-time buyer follow-upfirst-order-vs-repeatLiteTags first-time-buyer, emails two days later. First orders only.
Repeat customer follow-uprepeat-customerLiteEmail two days after the second and later orders.
Alert the team to a large orderhigh-value-alertLiteEmails your team when an order total is 1000 or more. Set the address.
Follow up on a refundrefund-follow-upLiteTags refunded, emails a day later.
Abandoned cart recoveryabandoned-cartProTwo emails, an hour and a day after abandonment.
Stop chasing recovered cartscart-recovered-cleanupProCancels pending abandoned-cart runs when an order is placed.
Win back a lapsed customerwin-backProDaily scan for 120 days without an order; tags lapsed and emails.
Clear the lapsed tag when they returnwin-back-returnProSwaps lapsed for won-back on their next order.
Tag VIP customersvip-taggingProWeekly scan; tags vip at 500 or more lifetime spend. Set the threshold in your currency.
Text the customer when an order shipsshipped-smsProSMS when the order moves into shipped, to opted-in contacts with a phone number.

The status presets assume your Commerce order statuses have the handles completed and shipped. If yours are different, change the trigger before enabling.

Stop chasing recovered carts cancels the contact's pending runs of every workflow triggered by an abandoned cart, and nothing else, so the thank-you and review emails the same order starts are left alone.

php craft lyfe/workflows/library
php craft lyfe/workflows/install win-back

Console commands

CommandWhat it does
lyfe/run/dueThe cron entry point. Advances due runs, then on Pro sweeps carts and runs due scans. Options: --limit=N, --queue (push a queue job instead), --runs-only (skip the cart sweep and scans). Exits non-zero if any run failed.
lyfe/run/statusEdition, whether Lyfe and global dry run are on, runs by status, and how many are due now.
lyfe/run/cancel <runId>Cancel one run.
lyfe/run/prune [days]Delete finished runs, messages and log entries older than days, or Keep history for if omitted.
lyfe/workflowsList workflows with trigger, step count, status and run counts.
lyfe/workflows/libraryList the presets.
lyfe/workflows/install <preset>Install a preset, disabled.
lyfe/workflows/simulate <orderId>Simulate every workflow against an order. Add --all to include disabled workflows.
lyfe/workflows/run-for-tag <workflow> <tag>Start a workflow (by handle) for every contact with a tag.
lyfe/contacts/backfillBuild contacts from every completed order. --batch=N sets the batch size (default 500).
lyfe/contacts/sync-statsRefresh the order figures scheduled scans filter on.
lyfe/contacts/unsubscribe <email>Unsubscribe an address from email and SMS.

All of them run as php craft <command>.

Extending Lyfe

Triggers, conditions, actions and SMS transports are registries keyed by a handle, so a module can add its own. Handles are what Lyfe stores, so your class can move without breaking saved workflows.

use justinholtweb\lyfe\events\RegisterTypesEvent;
use justinholtweb\lyfe\services\Types;
use yii\base\Event;

Event::on(Types::class, Types::EVENT_REGISTER_ACTIONS, function(RegisterTypesEvent $event) {
    $event->types[] = MyAction::class;
});

The other events are Types::EVENT_REGISTER_TRIGGERS, Types::EVENT_REGISTER_RULES and Sms::EVENT_REGISTER_TRANSPORTS. An action extends justinholtweb\lyfe\base\BaseAction and implements handle(), displayName(), settingsFields() and run().