Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .claude/lint-rules/orphaned_elements.star
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,13 @@ def check():
# Get references to this microflow
refs = refs_to(mf.qualified_name)

# Check if any reference is a call
# A scheduled event is an entry point: it runs the microflow without
# anything "calling" it, so a 'schedule' edge counts as a caller. Without
# this, a microflow that runs nightly in production was reported as
# orphaned — with the suggestion "Remove if unused".
has_callers = False
for ref in refs:
if ref.ref_kind == "call":
if ref.ref_kind == "call" or ref.ref_kind == "schedule":
has_callers = True
break

Expand Down
5 changes: 5 additions & 0 deletions .claude/skills/fix-issue.md

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions .claude/skills/mendix/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Detailed syntax for each MDL document type:
| [write-oql-queries.md](write-oql-queries.md) | OQL query syntax | Creating VIEW entities |
| [create-page.md](create-page.md) | Page and widget syntax | Creating pages |
| [fragments.md](fragments.md) | Fragment (reusable widget group) syntax | Reusing widget patterns across pages |
| [scheduled-events-and-queues.md](scheduled-events-and-queues.md) | Scheduled event (cron) and task queue syntax | Running a microflow on a schedule; bounding background concurrency |

## Patterns (By Use Case)

Expand Down Expand Up @@ -92,6 +93,9 @@ Load skills based on the task:
| "Create export mapping" | `json-structures-and-mappings.md` |
| "Map JSON to entities" | `json-structures-and-mappings.md` |
| "Seed/populate test data" | `demo-data.md` |
| "Run a microflow nightly / hourly / on a schedule" | `scheduled-events-and-queues.md` |
| "Add a cron job / batch job / recurring task" | `scheduled-events-and-queues.md` |
| "Limit how many background tasks run at once" | `scheduled-events-and-queues.md` |
| "Update widget properties" | `bulk-widget-updates.md` |
| "Change widgets in bulk" | `bulk-widget-updates.md` |
| "Reuse widgets across pages" | `fragments.md` |
Expand Down
208 changes: 208 additions & 0 deletions .claude/skills/mendix/scheduled-events-and-queues.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
# Scheduled Events and Task Queues

## When to Use This Skill

Use this skill when the user wants to:
- Run a microflow on a schedule ("every night at 4", "hourly", "cron", "batch job")
- Inspect or change an existing scheduled event
- Limit how many background tasks run at once (a task queue)
- Understand why `mxcli` refuses to rewrite a microflow that has a queued call

**These two features are unrelated.** A scheduled event does **not** go through a
task queue. Its own concurrency control is `OnOverlap`.

## Scheduled Events

Mendix's cron: run a microflow on a repeating schedule.

```sql
-- Inspect
list scheduled events;
list scheduled events in Ops;
describe scheduled event Ops.NightlyCleanup; -- re-executable MDL

-- Create
create scheduled event Ops.NightlyCleanup (
Microflow: Ops.SE_Cleanup,
Repeat: Daily,
HourOfDay: 4,
MinuteOfHour: 0,
TimeZone: Server,
Enabled: true
);

drop scheduled event Ops.NightlyCleanup;
```

`Microflow` and `Repeat` are **always required**. `show` is a synonym for `list`.

### Pick the Repeat first, then use only its fields

Mendix stores the repeat rule as one of eight types, and they differ in **which
fields they carry** — not just in their values. Naming a field from another
repeat is an error, not a no-op:

```
Error: Repeat Daily does not have Multiplier — it takes HourOfDay, MinuteOfHour
```

| Repeat | Fields | Means |
|--------|--------|-------|
| `Minutely` | `Multiplier` | every N minutes |
| `Hourly` | `Multiplier`, `MinuteOffset` | every N hours, at :MM past |
| `Daily` | `HourOfDay`, `MinuteOfHour` | every day at HH:MM (**no multiplier**) |
| `Weekly` | `Weekdays`, `HourOfDay`, `MinuteOfHour` | on the named days at HH:MM |
| `MonthlyByDate` | `Multiplier`, `MonthOffset`, `DayOfMonth`, `HourOfDay`, `MinuteOfHour` | the Dth of every N months |
| `MonthlyByWeekday` | `Multiplier`, `MonthOffset`, `DaySelector`, `Weekday`, `HourOfDay`, `MinuteOfHour` | the last Friday of every N months |
| `YearlyByDate` | `Month`, `DayOfMonth`, `HourOfDay`, `MinuteOfHour` | every 2 January |
| `YearlyByWeekday` | `Month`, `DaySelector`, `Weekday`, `HourOfDay`, `MinuteOfHour` | the first Monday of March |

Field values:

| Field | Value |
|-------|-------|
| `Multiplier` | 1 or more (defaults to 1) |
| `MinuteOffset` | 0–59 |
| `MonthOffset` | 0-based: which month of a multi-month cycle fires |
| `HourOfDay` / `MinuteOfHour` | 0–23 / 0–59 |
| `DayOfMonth` / `Month` | 1–31 / 1–12 |
| `Weekdays` | quoted list: `'Monday, Friday'` (case-insensitive) |
| `DaySelector` | `First`, `Second`, `Third`, `Fourth`, `Last` |
| `Weekday` | `Sunday` … `Saturday` |

Optional on any repeat:

| Property | Values | Default |
|----------|--------|---------|
| `Enabled` | `true` / `false` | `false` — **a new event does not run until you enable it** |
| `OnOverlap` | `DelayNext` / `SkipNext` | `DelayNext` |
| `TimeZone` | `UTC` / `Server` | `UTC` |
| `StartDateTime` | RFC 3339, e.g. `'2026-01-01T04:00:00Z'` | none |
| `Documentation` | free text | none |

`SkipNext` drops a run that would overlap the previous one; `DelayNext` waits.

### More examples

```sql
-- Every two hours, 23 minutes past
create scheduled event Ops.HourlyPing (
Microflow: Ops.SE_Ping,
Repeat: Hourly,
Multiplier: 2,
MinuteOffset: 23
);

-- Mondays and Fridays at 09:30
create scheduled event Ops.WeeklyReport (
Microflow: Ops.SE_Report,
Repeat: Weekly,
Weekdays: 'Monday, Friday',
HourOfDay: 9,
MinuteOfHour: 30
);

-- The last Friday of every third month, 18:00
create scheduled event Ops.QuarterEnd (
Microflow: Ops.SE_Close,
Repeat: MonthlyByWeekday,
Multiplier: 3,
MonthOffset: 2,
DaySelector: Last,
Weekday: Friday,
HourOfDay: 18
);
```

## Task Queues

A task queue bounds how many queued microflow calls run at once.

```sql
list queues;
describe queue Ops.OrderProcessing;

create queue Ops.OrderProcessing ( Parallelism: 3, ClusterWide: true );
create queue Ops.Mail; -- defaults: parallelism 1, per-instance

create or modify queue Ops.OrderProcessing ( Parallelism: '$MyModule.Workers' );
drop queue Ops.Mail;
```

| Property | Meaning | Default |
|----------|---------|---------|
| `Parallelism` | how many run at once — an **expression**, not a number | `1` |
| `ClusterWide` | `true` = across the cluster, `false` = per runtime instance | `false` |

Mendix stores parallelism as an expression string, so `3` and `'3'` are the same
thing and an arbitrary expression is legal.

## Common Mistakes

| Mistake | Symptom | Fix |
|---------|---------|-----|
| `Multiplier` on a `Daily` repeat | `Repeat Daily does not have Multiplier` | Daily has no multiplier — use `HourOfDay`/`MinuteOfHour`, or switch to `Hourly` |
| Forgetting `Enabled: true` | The event is in the model but never runs | Set `Enabled: true` (the default is false) |
| `TimeZone: server` | `has the wrong casing — Mendix stores it as "Server"` | Use the exact spelling: `Server`, `UTC`, `DelayNext`, `SkipNext`, `Last`, `Friday` |
| `HourOfDay: 24` | `it must be between 0 and 23` | Hours are 0–23; midnight is `0` |
| Expecting a queue to throttle a scheduled event | Nothing changes | They are unrelated — use `OnOverlap` |

## Rewriting a Microflow with a Queued Call Is Refused

MDL cannot yet author a *queued call* — the binding lives on the call activity
inside a microflow, not on the queue. So `create or replace|modify microflow` is
refused when the stored microflow has one:

```
Error: microflow Ops.ACT_Caller has 1 call(s) bound to a task queue (Ops.MyQueue),
and rewriting it would silently drop that binding
```

This is deliberate. Change that microflow in Studio Pro, or remove the task queue
from the call first. Without the refusal the binding was written back as null and
the project then looked *healthier* than before — `mx check` stopped reporting
`CE1613 "The selected task queue no longer exists"`, because the configuration
the error was about had been deleted.

## Validation Checklist

Before presenting a script:

```bash
mxcli check script.mdl # catches wrong-repeat fields (MDL-SCHED01)
mxcli check script.mdl -p app.mpr --references
```

- [ ] Every scheduled event has `Microflow` and `Repeat`
- [ ] Only that repeat's fields are used
- [ ] `Enabled: true` if it is meant to run
- [ ] Enum values spelled exactly (`Server`, `DelayNext`, `Last`, `Monday`)
- [ ] The target microflow exists and takes no parameters

## Querying and Linting

Both document types are in the catalog after `refresh catalog`:

```sql
-- Anything that fires more often than once a minute
select QualifiedName, RepeatDescription, Microflow
from CATALOG.SCHEDULED_EVENTS
where Enabled = 1 and IntervalSeconds < 60;

select QualifiedName, Parallelism, ClusterWide from CATALOG.QUEUES;
```

A scheduled event counts as a caller of the microflow it runs, so
`show callers of Ops.SE_Cleanup` lists it and the lint rule for orphaned
microflows (QUAL004) does not flag it. `IntervalSeconds` is derived from the
schedule, not from the legacy `Interval`/`IntervalType` pair Mendix also stores.

Starlark lint rules can iterate both: `scheduled_events()` yields
`repeat`, `interval_seconds`, `on_overlap`, `time_zone`, `enabled`, and
`microflow_name`; `queues()` yields `parallelism` (a string) and `cluster_wide`.

## Related

- `mxcli syntax scheduled-event`, `mxcli syntax queue` — full syntax reference
- `write-microflows.md` — writing the microflow the event calls
- `project-settings.md` — after-startup / before-shutdown microflows
16 changes: 12 additions & 4 deletions .claude/skills/verify-in-runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,18 @@ whatever happens to be cached.
mxcli new PopupDemo --version 11.12.2 --output-dir /root/pd
```

> **Trap: `PathTooLongException`.** Mendix's package extractor fails on deep paths.
> A scratchpad path like
> `/tmp/claude-.../<uuid>/scratchpad/proj` is already too long and
> `mx create-project` dies. Use a short root (`/root/pd`). The same applies to Go
> **Trap: `PathTooLongException`.** MxToolset refuses any full path over **259
> characters** — its own Windows-compatibility limit, not the filesystem's — and
> aborts extraction part way through, leaving ~259 files and no `.mpr`. With the
> blank 11.13 template's longest relative path at 181 characters, the output
> directory gets **77**. A scratchpad path like
> `/tmp/claude-.../<uuid>/scratchpad/proj` blows that on its own.
>
> `mxcli new` handles this since #825 — it creates the project in a short staging
> directory and moves it into place, so any depth works and a failure never leaves
> partial output. It warns when the final path exceeds 259 that Studio Pro on
> Windows may not open the project. **Calling `mx create-project` directly still
> dies**, so keep using a short root (`/root/pd`) for that. The same applies to Go
> tests: set `TMPDIR=/root/t` so `t.TempDir()` stays short, or the scaffolding fails
> and the test **skips** — which is indistinguishable from passing.

Expand Down
27 changes: 25 additions & 2 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,23 @@ concurrency:
group: "pages"
cancel-in-progress: false

# Publishing is gated on the repository actually having a Pages site.
#
# A fork inherits this workflow but not the upstream Pages configuration, so
# actions/deploy-pages fails there with a 404 that has nothing to do with the
# change being pushed:
#
# Failed to create deployment (status: 404) ... Ensure GitHub Pages has been
# enabled: https://github.com/<owner>/mxcli/settings/pages
#
# Every push to a fork's main therefore left a permanent red X, which is how a
# real failure gets missed. The book is still BUILT on forks (and on pull
# requests) — only the upload and deploy are skipped, so a docs change is still
# proven to compile wherever it is pushed.
#
# A fork that wants to publish its own copy: enable Pages (Settings → Pages →
# Source: GitHub Actions), then set the repository variable DEPLOY_DOCS=true
# (Settings → Secrets and variables → Actions → Variables).
jobs:
build:
runs-on: ubuntu-latest
Expand All @@ -37,13 +54,19 @@ jobs:
run: mdbook build docs-site

- name: Upload artifact
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
if: >-
github.ref == 'refs/heads/main' && github.event_name == 'push'
&& (github.event.repository.fork != true || vars.DEPLOY_DOCS == 'true')
uses: actions/upload-pages-artifact@v5
with:
path: docs-site/book

deploy:
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
# Same condition as the upload above: with no artifact there is nothing to
# deploy, and deploy-pages fails rather than no-ops when Pages is absent.
if: >-
github.ref == 'refs/heads/main' && github.event_name == 'push'
&& (github.event.repository.fork != true || vars.DEPLOY_DOCS == 'true')
needs: build
runs-on: ubuntu-latest
environment:
Expand Down
3 changes: 3 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,7 @@ Regenerate after modifying `MDLLexer.g4`, `MDLParser.g4`, or any `domains/*.g4`
- `.claude/skills/overview-pages.md` - CRUD page patterns
- `.claude/skills/master-detail-pages.md` - Master-detail page patterns
- `.claude/skills/generate-domain-model.md` - Entity/Association syntax
- `.claude/skills/mendix/scheduled-events-and-queues.md` - **Scheduled events (Mendix's cron) and task queues**: the eight Repeat variants and which fields each one takes, why a queue does NOT throttle a scheduled event, and why rewriting a microflow with a queued call is refused
- `.claude/skills/check-syntax.md` - Pre-flight validation checklist
- `.claude/skills/organize-project.md` - Folders, MOVE command, project structure conventions
- `.claude/skills/manage-security.md` - Security roles, access control, GRANT/REVOKE patterns
Expand Down Expand Up @@ -668,6 +669,8 @@ Full syntax tables for all MDL statements (microflows, pages, security, navigati
- ALTER WORKFLOW (SET properties, INSERT/DROP/REPLACE activities, outcomes, paths, conditions, boundary events)
- CALCULATED BY microflow syntax for calculated attributes
- Image collections (SHOW/DESCRIBE/CREATE/DROP)
- Scheduled events — Mendix's cron (LIST/DESCRIBE/CREATE [OR MODIFY]/DROP). `Repeat:` names one of the eight `ScheduledEvents$*Schedule` variants and only that variant's fields are accepted; a field from another repeat is refused by `mxcli check` (MDL-SCHED01) and by exec, which call the same function. The document shape is pinned by re-serializing three whole Studio Pro-authored events (Workflow Commons 4.11.0, OIDC SSO 4.6.0, SAML 4.2.1) element by element — `modelsdk/gen` is **wrong** about two properties here: the integers are stored as int64 (gen says int32, the #585 mismatch) and `StartDateTime` is a BSON datetime (gen says string), so both engines share one raw-BSON codec in `mdl/scheduledevents`. `Interval`/`IntervalType` are legacy siblings of `Schedule` that Studio Pro writes and does not keep in sync — derived on CREATE, carried through untouched on MODIFY. Only the Day and Hour variants have a Studio Pro reference; the other six are metamodel-derived and verified to load. Both are in the catalog (`CATALOG.SCHEDULED_EVENTS`, `CATALOG.QUEUES`) and a scheduled event emits a `schedule` edge into `CATALOG.REFS` — without it a microflow run only by a scheduled event was reported as dead by `show callers`, `GRAPH_DEAD_ASSETS` and lint rule QUAL004. See `.claude/skills/mendix/scheduled-events-and-queues.md`
- Task queues (LIST/DESCRIBE/CREATE [OR MODIFY]/DROP QUEUE). `Config.ParallelismExpression` is a **string** and the sibling int32 `Parallelism` is not written — matching all four Studio Pro queues in Business Events 3.12.1. Binding a *call* to a queue is not yet authorable, so `CREATE OR REPLACE|MODIFY MICROFLOW` is **refused** when the stored microflow has a queued call (guard-don't-drop, ADR-0005): the rebuild used to write `QueueSettings` back as null, which made `mx check` go from CE1613 to 0 errors by deleting the user's configuration
- AI agent documents: Model, Knowledge Base, Consumed MCP Service, Agent (LIST/DESCRIBE/CREATE/DROP, with variables, tools, KB tools, dollar-quoted multi-line prompts; requires AgentEditorCommons module, Mendix 11.9+)
- OData contract browsing (SHOW/DESCRIBE CONTRACT ENTITIES/ACTIONS FROM cached $metadata)
- AsyncAPI contract browsing (SHOW/DESCRIBE CONTRACT CHANNELS/MESSAGES FROM cached AsyncAPI)
Expand Down
6 changes: 6 additions & 0 deletions cmd/mxcli/cmd_check.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,12 @@ Examples:
// operation, so the mapping would be dropped in silence (#843).
violations = append(violations, executor.ValidateRestClientMappings(prog)...)

// Flag a scheduled event whose Repeat and fields disagree (a Multiplier on
// a Daily repeat, an HourOfDay of 99). Decidable from the statement, so it
// runs here rather than at exec, where the script would already have
// passed check.
violations = append(violations, executor.ValidateScheduledEvents(prog)...)

if isStructured {
// Always emit structured output (even when clean)
formatter.Format(violations, os.Stderr)
Expand Down
Loading
Loading