.jpg)
Magic Actions: Automated Solana Execution After ER Commits

Many use cases need to run state transition on Solana conditionally on the result of ER operations, or the other way around, execute a Solana transaction and delegate to an ER afterwards. Magic Actions are a mechanism that lets you schedule Solana instructions to run immediately after an Ephemeral Rollup (ER) commit, using the freshly committed state as inputs and enabling seamless composability for these use cases.
During an ER session, you’re executing against the delegated state in the ephemeral runtime for low-latency execution while accounts remain delegated. At some point, some downstream logic must run on the base layer (because it touches accounts that are not delegated, interacts with other programs, or relies on accounts that are locked). That downstream logic often depends on the latest committed version of the state you were mutating inside the ER.
Without abstraction, developers have to deal with this flow in multiple steps:commit state, wait for finality, read committed values, build a new transaction, submit to Solana. This adds latency, introduces failure points, and breaks atomicity.
Magic Actions solve this by letting developers attach instruction handlers that execute automatically on Solana immediately after a commit. The committed state becomes available as input to these handlers, enabling workflows that span both execution environments in a single transaction.
This article explains how Magic Actions work, implementation patterns, and common use cases.
How Magic Actions Work
Magic Actions require two components: an action instruction (runs on Solana) and a commit instruction (runs on ER, attaches the action). When the ER commits state back to Solana, the attached CallHandler instructions execute sequentially on Solana using the freshly committed data.
1. Define the Action Instruction
This is a standard Anchor instruction that will execute on Solana after the commit. It reads the committed state and performs the operation.
// Runs on L1 after commit
pub fn update_leaderboard(ctx: Context<UpdateLeaderboard>) -> Result<()> {
let leaderboard = &mut ctx.accounts.leaderboard;
let counter_info = &ctx.accounts.counter.to_account_info();
// Deserialize the freshly committed counter
let mut data: &[u8] = &counter_info.try_borrow_data()?;
let counter = Counter::try_deserialize(&mut data)?;
// Update leaderboard if new high score
if counter.count > leaderboard.high_score {
leaderboard.high_score = counter.count;
}
Ok(())
}
#[action] // Marks this as a Magic Action handler
#[derive(Accounts)]
pub struct UpdateLeaderboard<'info> {
#[account(mut, seeds = [LEADERBOARD_SEED], bump)]
pub leaderboard: Account<'info, Leaderboard>,
/// CHECK: Counter PDA - use UncheckedAccount because owner
/// depends on delegation status
pub counter: UncheckedAccount<'info>,
}
Note the #[action] attribute and UncheckedAccount for the committed account. Since the counter's owner changes during delegation (Delegation Program) and undelegation (your program), you can't use typed Account<> validation.
2. Build the Commit with Action
This instruction runs on the ER. It constructs the action and attaches it to the commit operation.
// Runs on ER
pub fn commit_and_update_leaderboard(
ctx: Context<CommitAndUpdateLeaderboard>
) -> Result<()> {
// Build the action instruction data
let instruction_data = anchor_lang::InstructionData::data(
&crate::instruction::UpdateLeaderboard {}
);
// Define which accounts the action needs
let action_accounts = vec![
ShortAccountMeta {
pubkey: ctx.accounts.leaderboard.key(),
is_writable: true,
},
ShortAccountMeta {
pubkey: ctx.accounts.counter.key(),
is_writable: false,
},
];
// Create the CallHandler
let action = CallHandler {
destination_program: crate::ID,
accounts: action_accounts,
args: ActionArgs::new(instruction_data),
escrow_authority: ctx.accounts.payer.to_account_info(),
compute_units: 200_000,
};
// Attach to commit
let magic_action = MagicInstructionBuilder {
payer: ctx.accounts.payer.to_account_info(),
magic_context: ctx.accounts.magic_context.to_account_info(),
magic_program: ctx.accounts.magic_program.to_account_info(),
magic_action: MagicAction::Commit(CommitType::WithHandler {
commited_accounts: vec![ctx.accounts.counter.to_account_info()],
call_handlers: vec![action],
}),
};
magic_action.build_and_invoke()?;
Ok(())
}Action Modes
Magic Actions support three execution patterns:
Commit + Action (Stay Delegated)
Commit state to Solana and execute actions, but keep accounts delegated. The ER session continues running.
MagicAction::Commit(CommitType::WithHandler {
commited_accounts: vec![counter.to_account_info()],
call_handlers: vec![action],
})
Use case: Periodic leaderboard syncs during an ongoing game session.
Commit + Undelegate + Action
Commit state, execute actions, and release accounts back to Solana control. The ER session ends.
MagicAction::CommitAndUndelegate(CommitType::WithHandler {
commited_accounts: vec![counter.to_account_info()],
call_handlers: vec![action],
})
Use case: Game over - finalize score, update leaderboard, release state.
Action Only (No Commit)
Execute Solana actions without committing any ER state. Useful for triggering external program calls.
MagicAction::BaseActions(vec![action])
Use case: Trigger a DeFi operation on L1 that doesn't depend on ER state.
Multiple Actions
You can chain multiple actions in sequence. They will execute in order on Solana.
MagicAction::Commit(CommitType::WithHandler {
commited_accounts: vec![
counter.to_account_info(),
player_stats.to_account_info(),
],
call_handlers: vec![
update_leaderboard_action,
mint_reward_action,
emit_event_action,
],
})
All actions execute atomically. If any action fails, the entire commit reverts.
Considerations for Action Integration
Fees: Action handlers execute on Solana and consume CU. Fees are paid from an escrow PDA controlled by the escrow_authority.
Compute Units: Specify adequate compute_units for each action.
Atomicity: If any action fails, the entire commit reverts.
Account Injection: The first two accounts in every action are automatically injected (escrow, escrow_auth). Your handler's account list starts at index 2.
Deserialization: Use UncheckedAccount and manual try_deserialize for committed accounts. Standard Anchor validation fails because the account owner changes during delegation.
Summary
Magic Actions extend the ER execution model by enabling automatic Solana operations after commits. Instead of orchestrating multi-step workflows, developers attach instruction handlers that execute atomically with state synchronization.
The pattern is straightforward: define an action instruction that runs on Solana, attach it to a commit instruction that runs on ER, and let Magic Actions handle the coordination. The result is seamless composability between Ephemeral Rollup performance and Solana state, without sacrificing atomicity.
Resources
