EmDash Hook Execution Order: Why Sequential Logic Is a Trap

EmDash's isolate paradigm runs hooks in parallel, breaking WordPress's sequential priority queue and demanding a new plugin design approach.

By Central
EmDash uses isolated V8 workers for hooks, making priority parameters decorative and requiring stateless, idempotent plugins.
Highlights
  • EmDash dispatches hooks to isolated Dynamic Workers that run in parallel, not sequentially.
  • The priority parameter in EmDash's definePlugin is decorative for cross-plugin ordering.
  • Race conditions in EmDash are solved by collapsing dependent logic into a single plugin with explicit async/await sequencing.

Every WordPress developer knows the rule. Hook priority determines execution order. add_action( 'init', 'my_func', 10 );codecodecodecode runs before add_action( 'init', 'my_func', 11 );codecodecodecode. It’s predictable. It’s reliable. It’s the bedrock of countless plugin architectures.

That advice is dangerous in EmDash.

The WordPress advice—'set your priority to 10 or 20 to control order'—is not just wrong in EmDash. It's a dangerous illusion.

The conventional wisdom about hook order doesn’t just shift in Cloudflare’s new CMS. It breaks entirely. If you’re migrating plugins or building new ones, assuming sequential execution will produce race conditions, silent failures, and data corruption that are nearly impossible to debug.

Here’s why—and what to do about it.

The WordPress Assumption — The Priority Queue Mentality

WordPress uses a global hook registry stored in memory. When do_action( 'posts:published' )codecodecodecode fires, the system walks through every registered callback sorted by a numeric priority parameter. Within the same priority level, callbacks execute sequentially in the order they were added.

This creates a deterministic chain. Plugin A finishes. Plugin B starts. Plugin B finishes. Plugin C starts. You can trace the exact path of execution through your entire plugin stack.

Developers exploit this constantly. A caching plugin hooks into save_postcodecodecodecode at priority 10. An SEO plugin hooks into the same event at priority 15, knowing the cache was already cleared. The system guarantees that ordering. It’s not a convention—it’s an architectural promise.

EmDash makes no such promise.

How EmDash Actually Executes Hooks — The Isolate Paradigm

EmDash is built on Cloudflare Workers and V8 isolates. When a hook like posts:publishedcodecodecodecode fires, EmDash doesn’t walk through a global PHP array. It dispatches an event to every registered plugin. Each plugin’s callback runs inside its own Dynamic Worker—a completely isolated V8 context.

These workers spin up in milliseconds, execute the callback, and spin back down. From the perspective of the main application thread, they run in parallel.

There is no global priority queue. There is no shared execution context. Plugin A and Plugin B, both listening to the same hook, execute concurrently. The WordPress advice—”set your priority to 10 or 20 to control order”—is not just wrong in EmDash. It’s a dangerous illusion.

The prioritycodecodecodecode parameter in EmDash’s definePlugincodecodecodecode function is effectively decorative for cross-plugin ordering. It controls the order plugins are registered in the system, not the order they execute. Two workers cannot wait on each other without explicit orchestration.

The posts:published Hook — A Concrete Example

The source material for EmDash shows a plugin that sends an email when a post is published. It declares two capabilities: read_contentcodecodecodecode and email_sentcodecodecodecode. The hook is posts:publishedcodecodecodecode.

Imagine a second plugin that logs the publication to an external analytics service. Both plugins hook into the same event. In WordPress, you’d set the logging plugin to priority 5 and the email plugin to priority 10. The log writes first. The email sends second. Ordered. Predictable.

In EmDash, both plugins receive the event simultaneously. The log worker spins up, reads the post content, and makes an HTTP request to your analytics endpoint. The email worker spins up, reads the post content, and sends an SMTP message.

Neither worker knows the other exists. The log might complete first. The email might complete first. The HTTP request from the log worker might fail because the email worker’s outbound call consumed a shared resource limit. These are race conditions you cannot reproduce in a local testing environment that uses a single-threaded Node.js process.

The deeper problem is that many plugins implicitly depend on state set by other plugins. An SEO plugin might write meta fields during posts:publishedcodecodecodecode. A social-sharing plugin might read those fields during the same hook. In WordPress, the execution order guarantees the fields exist. In EmDash, the social-sharing plugin might read stale or empty values.

When Order Actually Matters (And How to Enforce It)

The most common scenario where order matters is a cascading operation: update a post meta field, regenerate a sitemap, purge the CDN cache. In WordPress, this is a simple hook chain with increasing priority values. In EmDash, three isolated workers can step on each other.

The fix is counterintuitive. Don’t try to enforce cross-plugin ordering. Instead, build a single orchestrator plugin that sequences its own internal operations.

EmDash provides hooks through the definePlugincodecodecodecode function, but you can also use standard JavaScript async/awaitcodecodecodecode patterns inside a single plugin. A single Dynamic Worker runs your entire callback sequentially. If you need to update meta, regenerate a sitemap, and purge cache, do it in one plugin with three internal steps.

“`javascript

definePlugin({

id: ‘my-orchestrator’,

capabilities: [‘read_content’, ‘write_content’, ‘cache_purge’],

hooks: {

‘posts:published’: async ({ context, post }) => {

// Step 1: Update meta

await context.updateMeta(post.id, { sitemap_changed: true });

// Step 2: Regenerate sitemap

await context.regenerateSitemap();

// Step 3: Purge cache

await context.purgeCDN();

}

}

});

“`

This runs inside a single isolate. No parallel execution. No race conditions. No dependency on other plugins.

If you must coordinate across multiple plugins, use EmDash’s internal event bus or a shared D1 database table as a coordination layer. One plugin writes a completion marker. The next plugin reads it. This is slower than WordPress’s shared memory model, but it’s safe.

A hidden variable here is the file system. EmDash loads plugins based on file globs in the src/plugins/codecodecodecode directory. Unlike WordPress’s database-driven plugin registry, the order your operating system returns file names—which varies by filesystem, OS, and even directory fragmentation—influences the order plugins are registered. This makes relying on registration order even more fragile than it already is.

Testing for Hook Execution Order

The EmDash playground is useful for exploring the admin interface. It’s not useful for testing race conditions.

You need integration tests that account for non-deterministic execution. Use Promise.allSettled()codecodecodecode patterns and assert on final system state, not on the sequence of side effects. A test that passes because Plugin A ran first today might fail tomorrow.

“`javascript

// Bad test: assumes execution order

test(’email sends after log’, async () => {

await triggerPublish();

expect(logSpy).toHaveBeenCalled();

expect(emailSpy).toHaveBeenCalled(); // May fail if email ran first

});

// Good test: asserts final state

test(‘both plugins complete’, async () => {

await triggerPublish();

await new Promise(r => setTimeout(r, 2000)); // Let isolates settle

expect(await getLogCount()).toBe(1);

expect(await getEmailCount()).toBe(1);

});

“`

The EmDash CLI includes a testcodecodecodecode command that spins up a local D1 database and isolates. Use it. Run your tests multiple times in rapid succession. If you ever see a flaky failure, you have an ordering dependency you need to collapse into a single plugin.

The AI Agent Wildcard

EmDash ships with a built-in MCP server. This means AI agents can trigger hooks programmatically. An agent might call posts:publishedcodecodecodecode directly, bypassing the normal UI flow.

When an agent triggers a hook, it doesn’t respect any implicit ordering you might have built around user workflows. The agent fires the event, and all registered workers spin up in parallel. If your plugins depend on a specific chain of side effects, an agent can silently break that chain.

The solution is the same: single-plugin orchestration with explicit async/awaitcodecodecodecode sequencing. Design your plugins as if every hook call is the first and only call. Stateless. Idempotent. Independent.

EmDash’s architecture isn’t a bug. It’s a feature that forces you to write plugins that are truly isolated. That’s a better foundation for a secure, scalable CMS than WordPress’s shared-everything model. But it requires unlearning the priority queue mentality.

Let go of the sequence. Embrace the isolate.

Questions answered
  • Why does EmDash break WordPress hook priority rules?EmDash runs each plugin's callback in its own isolated V8 worker, executing them concurrently rather than sequentially from a global priority queue.
  • How should developers handle hook ordering in EmDash?Collapse dependent logic into a single plugin and use explicit async/await sequencing instead of relying on priority parameters.
Share This Article