Automation Center Reference Public

Getting Started

Switch the Automation Center on safely: confirm the cron runtime is ticking, run the migrations, verify the engine card, grant the ten permissions, and build a first automation that only observes until you trust it.

Guide version: r1 Module version: 0.3.3 Updated: 2026-09-04 Estimated time: 14 min 1 views
Introduction

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.

ℹ️
Budget an observation period, not an afternoon. Steps 1–6 take perhaps twenty minutes. Step 7 — letting the automation observe for a real payroll period before you consider Live — is the part that actually matters, and it is measured in days.

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
Do not rely on the legacy /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.

????
Observe mode does not need the actor. Nothing is written, so nothing needs attributing. You can complete this entire page on a database where the migration has not run — you simply cannot promote to Live afterwards.

Enable the module and run its migrations

Turn the module on. Settings → Manage Modules → Automation Center. If you are provisioning without the UI, the equivalent is the tenant setting module_automations = 1. It is the module's single enable guard — there is no second per-feature toggle to hunt for.
Run the migrations. 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 --allmigrate:latest has no tenant fan-out, which is exactly why the toggle seed is split across two migrations.
Confirm the tables landed. Three tables, tenant database only: automations, automation_runs, automation_run_items. Their ledger is migrations_automations; php erpat migrate:status lists it alongside every other set.
Reload and look at the sidebar. Automations appears in the Administration section, between System Logs and Settings. If it is missing but the setting is on, you are missing the automations permission — see below.
ℹ️
Disabling the module does not unreachable-ify its URLs. Routes stay registered, so the controller self-gates on 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:

FieldWhat 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.
ℹ️
The checksum is a catalogue fingerprint, not a code fingerprint. It is built from three declared fields only, so rewriting a handler's internals leaves it unchanged. That is the intended trade — it answers "do both nodes see the same set of types at the same declared versions?", which is the question that actually causes support tickets. Use it for that, not as a deployment hash.

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
automationsSee the page and the Overview tab. Without it the sidebar entry does not render.Admin
automations_createCreate a draft automation.Admin
automations_updateEdit a draft, pause, resume.Admin
automations_deleteArchive an automation.Admin
automations_publishMove a draft to active — schedule it.Senior admin
automations_go_livePublish in Live mode: let it change records unattended.Owner / manager only
automations_run_nowTrigger a manual run. Still a normal run row through the normal executor.Admin
automations_logsRuns & Logs tab and run detail.Admin + auditors
automations_analyticsAnalytics tab.Admin
automations_settingsModule 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.

There is a known gap here — do not discover it in production. The type declares 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 keyattendance.auto_decision
LabelAttendance Auto Decision
Owned byHumanResource — modules/HumanResource/config/automations.php
Requiresmodule_human_resource switched ON
Phase 1 actionApprove 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.

ℹ️
Zero types is a valid state. The module deliberately declares no hard dependency on HumanResource, because that would take the whole Automation Center offline instead of just the one unavailable capability. Run history, past analytics and the engine card all keep working with an empty catalogue — you simply cannot create anything new.

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.

Type. Pick Attendance Auto Decision. Only available types are offered.
Action. Approve — the only Phase 1 action. This is also where the attendance specifics live: the lookback window, the settle delay, and the overtime policy.
Scope & rules. Who it covers (departments, teams, individuals, or everyone) and the eligibility conditions a record must satisfy. Start narrow: one department you know well beats the whole company for a first run, because you can check the answer by eye.
Schedule & safety. A cron expression (default */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.
Review & preview. Read the summary back, then press Run preview. The preview and the eventual run share one evaluator — a preview that could disagree with the run it predicts would be worse than no preview at all.
Save the draft, then publish it in Observe mode. Publishing schedules it. In Observe it will claim its occurrences, evaluate every candidate, and record an outcome and a reason code per record, while changing nothing at all.

The two settings people get wrong first

Lookback (default 7 days)

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.

Settle delay (default 12 hours)

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.

PolicyWhat it decidesUse 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:

CheckHealthy
Runs are completingcompleted, on schedule, not failed, timed_out, or a wall of skipped_overlap.
The volume is plausibleThe 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 sampleOpen 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 senseSkipped 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 settledYou have stopped editing it. Every edit changes the config hash and invalidates the observation you just spent a period collecting.
Live changes real records, and it is gated by four preconditions — all four, every time. You need the separate 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.

Is the runtime ticking? php erpat cron:status. No scheduler, no runs. This is the single most common cause.
Is the module on for this tenant? The dispatcher checks module_automations on every tick and skips quietly when it is off. The setting is per tenant database.
Did the tenant get the tables? The job exits quietly when automations is absent, rather than fataling the tick. Run php erpat migrate:modules --all.
Is the automation active? A draft is not scheduled. Neither is a paused automation, and neither is one sitting in Needs review.
Is the type available? Settings tab. Inactive means module_human_resource is off, and the run becomes DEFINITION_UNAVAILABLE rather than executing.
Has it actually come due? A */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.
Did it run and find nothing? A completed run with zero items is a working automation and an empty candidate set — usually the settle delay, the lookback, or a scope that matches nobody.
????
"It says success but nothing changed" is the expected result in Observe mode. That is the whole point of Observe: the run reports what it would have done. If you expected records to change, check the run's mode before you check anything else. More symptoms and their causes are in FAQ & Troubleshooting.

Where to go next

Was this guide helpful?

Report a content problem