Getting Started
Somebody has asked you to turn the Automation Center on. This page takes you from a disabled module to a first automation that is quietly watching, and stops there deliberately — nothing on this page changes a single business record.
The module ships disabled. That is not caution theatre: its tenant migration seeds
module_automations as '0' on purpose, because enabling on install
would publish a sidebar entry with nothing behind it. Everything below is the work of
putting something behind it.
Before you begin: two prerequisites
Both of these live outside this module, and both fail quietly rather than loudly. Check them first — almost every "I enabled it and nothing happened" report traces back to one of the two.
1. The Advanced Cron runtime must actually be ticking
The Automation Center adds no scheduler of its own. It contributes exactly one job
— automation_dispatch, scheduled * * * * * — to ERPat's Advanced
Cron runtime, and that runtime owns the fan-out across tenants, the lock, the catch-up
watermark and the run log. If nothing is driving the runtime, your automation's schedule
is a row in a table and nothing more.
On a server, that driver is an OS-level cron entry. On a development machine it is the bundled scheduler:
# Is anything driving the runtime on this host?
php erpat cron:status
# Development only — a long-running scheduler that ticks on each wall-clock minute.
php erpat cron:start
# Or drive exactly one tick by hand, to prove the plumbing end to end.
php erpat cron:tick automation_dispatch/cron HTTP alias. It is deprecated and can
be kill-switched per tenant via cron_legacy_alias_enabled = 0, which returns
HTTP 410 — and a caller that ignores the status code will silently skip every tick
forever. cron:start shells out to the CLI tick precisely to avoid it.
2. The System actor must exist — if you ever intend to go Live
When an automation approves a record in Live mode, somebody's name goes on that approval.
It is not yours, and it is not the employee's: it is a dedicated System user seeded
by the core migration
application/migrations/20260903232432_seed_system_actor_user.php. That row is
user_type='system', inactive, login disabled, no password — invisible to every
staff-scoped query and impossible to sign in as.
The resolver erpat_system_actor_id() returns 0 when the row is
absent, and 0 must be treated as a refusal, never as a usable id. The attendance
handler does exactly that: on a database without the actor it refuses to run Live rather
than writing approvals with a blank approver. So an unmigrated database does not produce
bad data — it produces a failed run, which is the correct outcome and also a confusing one
if you have not read this paragraph.
Enable the module and run its migrations
module_automations = 1. It is the module's single enable guard —
there is no second per-feature toggle to hunt for.php erpat migrate:latest covers core
(including the System actor and the main-database toggle row) and finishes by running
module migrations. To reach existing tenant databases you need
php erpat migrate:modules --all — migrate:latest has no tenant
fan-out, which is exactly why the toggle seed is split across two migrations.automations, automation_runs,
automation_run_items. Their ledger is
migrations_automations; php erpat migrate:status lists it
alongside every other set.automations permission — see below.with_module("automations") in its constructor
and redirects. Toggling the module off is a real off switch for the UI and for the
dispatcher, which checks the same setting on every tick and skips.
Verify the engine before you build anything
Two checks, thirty seconds, and they save you from debugging an automation that was never going to run.
From the command line
# The dispatcher must appear in the registered-job list.
php erpat cron:list
# Expect a row: automation_dispatch · * * * * * · per-tenant
# 'per-tenant' matters — runsForGlobalScope() is false, so the runtime calls
# handle() once per active tenant with the DB connection already switched.
php erpat cron:list --tenant=<company_slug>From the Settings tab
Open Automations → Settings. The engine card is small on purpose — it reports the three facts that are worth reporting and sends you to Cron Manager for everything else:
| Field | What it tells you |
|---|---|
| Dispatch job | Always automation_dispatch. There is exactly one, and this module
never writes a system cron entry of its own. |
| Registry checksum | A 12-character fingerprint of the assembled type catalogue — every type key with
its definition_version and config_schema_version, sorted
and hashed. Two application nodes showing different checksums are running
different code. |
| Registered types | Reads available / total. 1 / 1 is healthy.
0 / 1 means a type is declared but its owning module is switched
off. |
Enabling, rescheduling, pausing and manually running the dispatcher all live in Cron Manager, per tenant. The Settings tab deliberately does not duplicate those controls — a second place to schedule the same job is a second place for the two to disagree.
Grant the permissions
Ten keys, all owned by this module and appended to the Roles editor under System: Automation Center while the module is enabled. They disappear from the editor when you switch the module off, which is the intended behaviour and not a bug report.
| Permission | What it allows | Typical holder |
|---|---|---|
automations | See the page and the Overview tab. Without it the sidebar entry does not render. | Admin |
automations_create | Create a draft automation. | Admin |
automations_update | Edit a draft, pause, resume. | Admin |
automations_delete | Archive an automation. | Admin |
automations_publish | Move a draft to active — schedule it. | Senior admin |
automations_go_live | Publish in Live mode: let it change records unattended. | Owner / manager only |
automations_run_now | Trigger a manual run. Still a normal run row through the normal executor. | Admin |
automations_logs | Runs & Logs tab and run detail. | Admin + auditors |
automations_analytics | Analytics tab. | Admin |
automations_settings | Module settings tab and the engine card. | Senior admin |
Why go_live is a separate key
Scheduling a rule that only reports and letting that rule act are different
decisions with different blast radii, so they are different permissions. A capable
operator can build, preview, publish and monitor all day with
automations_publish and never be able to promote anything. Splitting the two
is what lets you delegate the work without delegating the risk.
go_live is also the only permission in this list that is checked twice:
once when you promote, and again at run time for every Live run. If the publisher loses the
permission — role change, departure — the next Live run fails closed with
PUBLISHER_UNAUTHORIZED and the automation moves to Needs review. Observe
runs are deliberately not gated that way: they change nothing, and silencing a report
because its author left removes visibility at exactly the wrong moment.
publish_permissions (for attendance: attendance_approval), but
nothing currently reads it. So an operator holding
automations_publish + automations_go_live but without
attendance_approval can today publish a Live attendance automation they could
not perform by hand. Until that gate is wired, treat
automations_go_live as if it implied every action permission the type needs,
and grant it accordingly. Tracked in Research & Known
Gaps.
Confirm a type is available
The Automation Center contains no business logic. Every kind of automation you can build is contributed by the module that owns the underlying records, through a declarative config sidecar. Today there is exactly one:
| Type key | attendance.auto_decision |
|---|---|
| Label | Attendance Auto Decision |
| Owned by | HumanResource — modules/HumanResource/config/automations.php |
| Requires | module_human_resource switched ON |
| Phase 1 action | Approve only. The handler refuses anything else, so no screen offers Reject. |
The Settings tab lists every declared type with an Available or Inactive badge. Inactive means the type is correctly declared but its owning module is off — a type can never outlive its owner's toggle, and an automation that references an unavailable type moves to Needs review rather than running against a disabled feature.
Build your first automation — in Observe mode
Go to Automations → Automations → New. The builder is a five-step wizard. The stepper is guidance, not security: every value is revalidated server-side, so clicking straight to step 5 skips nothing that matters.
*/15 * * * *), a
timezone (default Asia/Manila), the overlap policy, and the three caps —
batch_size 250, max_items_per_run 1000,
first_live_run_limit 50.The two settings people get wrong first
This is what keeps a routine fifteen-minute tick away from a historical backlog. One measured live tenant carries roughly 112,000 pending attendance rows reaching back to 2020; a 7-day window reduces that to the current flow. Widen it only deliberately, and preview again afterwards.
Approving minutes after clock-out closes the window in which corrections and late edits normally happen. Waiting for a record to stop moving before acting on it is cheaper than reversing a decision afterwards.
Overtime: choose a decision, not a deletion
Measured overtime hours are always stored. The policy records a decision about
them in attendance_metrics.ot_status, and unapproved hours are withheld at read
time — so no policy destroys a number, and every choice is reversible from the Overtime
tab.
| Policy | What it decides | Use when |
|---|---|---|
skip default |
Decides nothing about the overtime; it is queued for a person in the Overtime tab. | Always, to begin with. No unreviewed overtime reaches payroll. |
exclude |
Approves the base hours, rejects the overtime. The detected hours survive and can be approved later. | Overtime genuinely requires a human sign-off you do not want to automate. |
include |
Approves overtime together with the base hours. | Overtime is pre-authorised by roster and needs no second look. |
exclude and include only bite where the tenant's Overtime
Approval setting is on. With it off there is nothing to decide, and every row stays
approved with its overtime payable exactly as before.
What "good" looks like before you think about Live
Let the automation observe for at least one full period of the work it is meant to handle — a payroll cutoff, not an afternoon. Then read the runs and ask five questions:
| Check | Healthy |
|---|---|
| Runs are completing | completed, on schedule, not failed, timed_out, or a wall of skipped_overlap. |
| The volume is plausible | The would_approve count per run resembles what a person actually approves in that window. A first run in the thousands means the scope or the lookback is wider than you intended. |
| You agree with a sample | Open a run, take ten would_approve items at random, and check them by hand. If you would have clicked Approve on all ten, the rule matches the judgement you were applying in your head. |
| The refusals make sense | Skipped records carry a reason code, not silence. MISSING_TIME_OUT and LEAVE_CONFLICT are the rule working. A reason you cannot explain is a rule you do not yet understand. |
| The configuration has settled | You have stopped editing it. Every edit changes the config hash and invalidates the observation you just spent a period collecting. |
automations_go_live permission; the type must
declare it can apply; the automation must have a completed run to point at; and
that run's config_hash must still match the current configuration. The last
one is the subtle one: preview, widen the scope, then promote, and the run that "proved"
the rule was watching a different rule against a different set of records — so the
promotion is refused and you observe once more. The first Live run is additionally capped
(your step-4 first_live_run_limit, default 50; 25 if the safety block is
absent). Read Permissions, Authority & Scheduling
before you promote anything, and Building & Running for
the promotion itself.
If nothing happens
Work down this list in order — it is ordered by how often each one is the answer.
php erpat cron:status. No scheduler, no
runs. This is the single most common cause.module_automations on every tick and skips quietly when it is off. The
setting is per tenant database.automations is absent, rather than fataling the tick. Run
php erpat migrate:modules --all.module_human_resource is off, and the run becomes
DEFINITION_UNAVAILABLE rather than executing.*/15 schedule in
Asia/Manila is not due right now just because you saved it. Use Run
now (with automations_run_now) to stop waiting.