verify-with-oql

Verify microflow side effects and data changes with OQL against a running app. Use after executing changes to confirm data was created, updated or deleted as expected, or to back a browser test with a

SKILL.md

GitHub
<!-- skill-index: generated by scripts/skill-index.py; make check-skill-index -->

Find it fast

sed -n '<from>,<to>p' SKILL.md one section rather than reading the file; ./mxcli syntax <topic> answers how do I write this for a fraction of either.

lines section
26-33 When to Use This Skill
34-54 The Pattern
55-103 OQL Verification Examples
104-163 Combining with Playwright
164-187 Combining with Hot Reload
188-204 Tips
205-210 Related Skills
<!-- /skill-index -->

Verify with OQL Skill

This skill documents how to verify microflow side effects and data changes using OQL queries against a running Mendix app.

When to Use This Skill

Use this when:

  • You've deployed changes and want to verify data was created/updated/deleted correctly
  • You need to confirm a microflow produced the expected side effects
  • You're combining Playwright UI tests with backend data verification
  • You want a fast feedback loop: deploy, trigger, verify

The Pattern

The core verification workflow is:

  1. Deploy — apply MDL changes and rebuild
  2. Trigger — execute the action (via UI, microflow call, or API)
  3. Verify — query the database with OQL to confirm the result
# 1. Deploy
mxcli exec changes.mdl -p app.mpr
mxcli docker build -p app.mpr --skip-check
mxcli docker reload -p app.mpr   # or: mxcli docker up -p app.mpr --fresh --wait

# 2. Trigger (example: call a microflow that creates test data)
# This could be via Playwright, a rest call, or manual interaction

# 3. Verify
mxcli oql -p app.mpr "select Name, Email from MyModule.Customer where Name = 'Jane Doe'"

OQL Verification Examples

Check data was created

# Verify a customer was created
mxcli oql -p app.mpr "select Name, Email from Sales.Customer where Name = 'Test Customer'"

# count records
mxcli oql -p app.mpr "select count(*) as Total from Sales.Order"

Check data was updated

# Verify status was changed
mxcli oql -p app.mpr "select OrderNumber, status from Sales.Order where OrderNumber = 'ORD-001'"

Check data was deleted

# Verify record no longer exists (should return empty)
mxcli oql -p app.mpr "select count(*) as Total from Sales.Customer where Name = 'Deleted Customer'"

Check associations

# Verify an association was set (join query)
mxcli oql -p app.mpr \
  "select o.OrderNumber, c.Name from Sales.Order o join o/Sales.Order_Customer/Sales.Customer c where o.OrderNumber = 'ORD-001'"

JSON output for assertions

Use --json for structured output that's easy to parse in scripts:

# json output for piping to jq
mxcli oql -p app.mpr --json "SELECT Name FROM Sales.Customer" | jq '.[].Name'

# count check in a script
count=$(mxcli oql -p app.mpr --json "SELECT count(*) AS Total FROM Sales.Order" | jq -r '.[0].Total')
if [ "$count" -gt 0 ]; then
  echo "Orders exist: $count"
fi

Combining with Playwright

The most powerful pattern: trigger actions through the UI with Playwright, then verify side effects with OQL.

Example: Create via UI, verify via OQL

// tests/verify-create.spec.ts
import { test, expect } from '@playwright/test';
import { execSync } from 'child_process';
import { login } from './utils/login';

function oql(query: string): any[] {
  const result = execSync(
    `mxcli oql -p app.mpr --json "${query}"`,
    { encoding: 'utf-8' }
  );
  return JSON.parse(result);
}

test('creating a customer via UI persists correctly', async ({ page }) => {
  await login(page);
  await page.goto('/p/Customer_Edit');

  // Fill the form
  await page.locator('.mx-name-txtName input').fill('OQL Test Customer');
  await page.locator('.mx-name-txtEmail input').fill('oql@test.com');
  await page.locator('.mx-name-btnSave').click();

  // wait for save to complete
  await page.waitForTimeout(2000);

  // Verify via OQL
  const rows = oql("select Name, Email from Sales.Customer where Name = 'OQL Test Customer'");
  expect(rows).toHaveLength(1);
  expect(rows[0].Email).toBe('oql@test.com');
});

Example: Verify microflow side effects

test('approving an order updates status and creates audit log', async ({ page }) => {
  await login(page);
  await page.goto('/p/Order_Overview');

  // Click approve on first order
  await page.locator('.mx-name-btnApprove').first().click();
  await page.waitForTimeout(2000);

  // Verify order status changed
  const orders = oql("select status from Sales.Order where OrderNumber = 'ORD-001'");
  expect(orders[0].Status).toBe('Approved');

  // Verify audit log was created
  const logs = oql("select action from Sales.AuditLog where action = 'Order Approved'");
  expect(logs.length).toBeGreaterThan(0);
});

Combining with Hot Reload

For the fastest iteration loop when developing microflow logic:

# 1. Edit microflow
mxcli exec fix-logic.mdl -p app.mpr

# 2. Rebuild (fast)
mxcli docker build -p app.mpr --skip-check

# 3. Hot reload (no restart, keeps data)
mxcli docker reload -p app.mpr

# 4. Trigger the microflow (via UI or test)

# 5. Verify the result
mxcli oql -p app.mpr "select status from Sales.Order where OrderNumber = 'ORD-001'"

# Repeat from step 1 until correct

This loop avoids container restarts and database resets, making each iteration take seconds instead of minutes.

Tips

  • Use --json for scripted assertions — structured output is easier to parse than table format
  • OQL is read-only — mxcli oql uses the preview_execute_oql action which cannot modify data
  • OQL won't see rows you wrote directly into Postgres until the runtime reloads. mxcli oql executes through the running app's query layer (not a separate DB), so rows seeded with direct SQL INSERTs (see demo-data) are invisible until the runtime re-reads them. After seeding, run mxcli docker reload (or restart the app) before trusting an OQL count(*) of 0. To confirm the rows landed before a reload, query Postgres directly (mxcli sql …).
  • Check before and after — query the state before triggering an action to establish a baseline
  • Common OQL patterns for testing:
    • count(*) to verify record counts
    • where clauses to find specific records
    • join to verify associations were set
    • ORDER by ... limit 1 to check the most recent record
  • --direct mode is faster when the admin port is reachable (after admin.addresses build patch)

Related Skills