dojo
Dojo Engine framework patterns — World, Systems, Models, Events, Components, Store, permissions, testing with spawn_test_world, and deployment with sozo.
Best use case
dojo is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Dojo Engine framework patterns — World, Systems, Models, Events, Components, Store, permissions, testing with spawn_test_world, and deployment with sozo.
Teams using dojo should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/dojo/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How dojo Compares
| Feature / Agent | dojo | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Dojo Engine framework patterns — World, Systems, Models, Events, Components, Store, permissions, testing with spawn_test_world, and deployment with sozo.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# Dojo Engine Framework
This skill covers Dojo Engine patterns for building fully on-chain games on Starknet. For pure Cairo language and Starknet contract patterns, see the `cairo` skill.
## When to Use
- Creating or modifying Dojo contracts (`#[dojo::contract]`)
- Defining models (`#[dojo::model]`) or events (`#[dojo::event]`)
- Working with WorldStorage (read/write models, emit events)
- Composing Starknet components within Dojo contracts
- Testing with `spawn_test_world`
- Configuring permissions and deployment
## Architecture Overview
Dojo is an Entity Component System (ECS) framework:
- **World** — Central coordinator and database, manages all resources within namespaces
- **Models (What)** — Data structs stored in the World (`#[dojo::model]`)
- **Systems (How)** — Stateless contracts that operate on models (`#[dojo::contract]`)
- **Events** — Indexed signals for off-chain sync (`#[dojo::event]`)
- **Components** — Reusable logic modules (`#[starknet::component]`)
### Data Flow
```
System (#[dojo::contract])
└── self.world(@NAMESPACE()) → WorldStorage
├── world.read_model(key) → Model
├── world.write_model(@model)
└── world.emit_event(@event)
```
## Essential Imports
```cairo
// In systems (always needed)
use dojo::model::{ModelStorage, ModelValueStorage};
use dojo::event::EventStorage;
use dojo::world::WorldStorage;
// In models
use starknet::ContractAddress;
// For nested structs in models
use dojo::meta::Introspect;
// In tests
use dojo_cairo_test::{
ContractDef, ContractDefTrait, NamespaceDef, TestResource,
WorldStorageTestTrait, spawn_test_world,
};
```
## Models (`#[dojo::model]`)
Models are the state. Structs annotated with `#[dojo::model]` are stored in the World.
```cairo
#[derive(Drop, Serde)]
#[dojo::model]
pub struct Game {
#[key]
pub id: u64, // Primary key — used for lookup
pub level: u8, // Data fields
pub score: u32,
pub slots: felt252, // Can store packed data
}
```
### Key Rules
1. **At least one `#[key]` field required**
2. **Keys must come first** in the struct
3. **Keys are not stored** — used only for indexing
4. **Required derives**: `Drop`, `Serde`
5. **Optional derives**: `Copy` (for primitive types)
### Composite Keys
```cairo
#[derive(Copy, Drop, Serde)]
#[dojo::model]
pub struct VaultPosition {
#[key]
pub user: felt252, // First key
pub shares: u256,
pub lockup: u64,
}
// Read with single key
let position: VaultPosition = world.read_model(user_felt);
// For multiple keys, use tuple
let resource: GameResource = world.read_model((player, location));
```
### Custom Nested Structs
Must derive `Introspect`:
```cairo
#[derive(Drop, Copy, Serde, Introspect)]
pub struct Vec2 {
pub x: u32,
pub y: u32,
}
```
### Model API
```cairo
let mut world = self.world(@"NUMS");
// Read (returns default/zero if not set)
let game: Game = world.read_model(game_id);
// Write / Update
world.write_model(@game);
// Generate unique ID
let entity_id = world.uuid();
// Delete
world.erase_model(@game);
```
## Events (`#[dojo::event]`)
Events are automatically indexed by Torii (Dojo's off-chain indexer).
```cairo
#[derive(Copy, Drop, Serde)]
#[dojo::event]
pub struct Purchased {
#[key]
pub player_id: felt252, // Indexed field for filtering
pub starterpack_id: u32,
pub quantity: u32,
pub time: u64,
}
// Emit in a system or component
world.emit_event(@Purchased {
player_id: caller.into(),
starterpack_id: 1,
quantity: 1,
time: get_block_timestamp(),
});
```
## Systems (`#[dojo::contract]`)
Systems are **thin entry points**. They get WorldStorage and delegate to components.
```cairo
#[starknet::interface]
pub trait IPlay<T> {
fn set(ref self: T, game_id: u64, index: u8) -> u16;
fn select(ref self: T, game_id: u64, index: u8);
}
#[dojo::contract]
pub mod Play {
use dojo::model::ModelStorage;
use crate::components::playable::PlayableComponent;
use crate::constants::NAMESPACE;
// Embed components
component!(path: PlayableComponent, storage: playable, event: PlayableEvent);
impl PlayableInternalImpl = PlayableComponent::InternalImpl<ContractState>;
#[storage]
struct Storage {
#[substorage(v0)]
playable: PlayableComponent::Storage,
}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
#[flat]
PlayableEvent: PlayableComponent::Event,
}
#[abi(embed_v0)]
impl PlayImpl of super::IPlay<ContractState> {
fn set(ref self: ContractState, game_id: u64, index: u8) -> u16 {
// Get world for namespace
let world = self.world(@NAMESPACE());
// Delegate to component
self.playable.set(world, game_id, index)
}
}
}
```
### World Access
```cairo
// With explicit namespace (preferred in this project)
let world = self.world(@NAMESPACE()); // NAMESPACE() returns "NUMS"
// With default namespace
let world = self.world_default();
// With inline namespace
let world = self.world(@"my_namespace");
```
### Initialization (`dojo_init`)
Called once on contract deployment:
```cairo
fn dojo_init(ref self: ContractState, owner: ContractAddress, entry_price: u128) {
let mut world = self.world(@NAMESPACE());
let mut store = StoreImpl::new(world);
// Create initial config
let config = ConfigTrait::new(owner, entry_price);
store.set_config(config);
// Initialize components
self.initializable.initialize(world);
}
```
## Component Pattern (Nums-specific)
Components contain the **business logic** and receive `WorldStorage` as a parameter.
```cairo
#[starknet::component]
pub mod PlayableComponent {
use dojo::world::WorldStorage;
use crate::{StoreImpl, StoreTrait};
#[storage]
pub struct Storage {}
#[event]
#[derive(Drop, starknet::Event)]
pub enum Event {}
#[generate_trait]
pub impl InternalImpl<
TContractState,
+HasComponent<TContractState>,
+Drop<TContractState>,
> of InternalTrait<TContractState> {
fn set(
ref self: ComponentState<TContractState>,
world: WorldStorage,
game_id: u64,
index: u8,
) -> u16 {
// [Setup] Store
let mut store = StoreImpl::new(world);
// [Check] Game state
let caller = starknet::get_caller_address();
let mut game = store.game(game_id);
game.assert_does_exist();
game.assert_not_over();
// [Effect] Place number
game.place(game.number, index, ref rand);
// [Interaction] Write back
store.set_game(@game);
game.number
}
}
}
```
### Dependent Component Access
```cairo
// Immutable access
let questable = get_dep_component!(@self, Quest);
questable.progress(world, player, task_id, count, true);
// Mutable access
let mut rankable = get_dep_component_mut!(ref self, Rankable);
rankable.submit(world: world, leaderboard_id: 1, score: 42);
```
### Component Embedding in Contract
```cairo
// 1. Declare component
component!(path: PlayableComponent, storage: playable, event: PlayableEvent);
impl PlayableInternalImpl = PlayableComponent::InternalImpl<ContractState>;
// 2. Add to storage
#[storage]
struct Storage {
#[substorage(v0)]
playable: PlayableComponent::Storage,
}
// 3. Add to events
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
#[flat]
PlayableEvent: PlayableComponent::Event,
}
```
## Store Wrapper Pattern (Nums-specific)
The `Store` struct wraps `WorldStorage` for convenience:
```cairo
#[derive(Copy, Drop)]
pub struct Store {
pub world: WorldStorage,
}
#[generate_trait]
pub impl StoreImpl of StoreTrait {
fn new(world: WorldStorage) -> Store { Store { world } }
// Model access
fn game(self: @Store, game_id: u64) -> Game { self.world.read_model(game_id) }
fn set_game(mut self: Store, game: @Game) { self.world.write_model(game) }
fn config(self: @Store) -> Config { self.world.read_model(WORLD_RESOURCE) }
fn set_config(mut self: Store, config: Config) { self.world.write_model(@config) }
// Dispatchers
fn nums_disp(self: @Store) -> INumsTokenDispatcher {
let config = self.config();
INumsTokenDispatcher { contract_address: config.nums }
}
// Event emission
fn claimed(mut self: Store, player_id: felt252, game_id: u64, reward: u64) {
let event = ClaimedTrait::new(player_id, game_id, reward);
self.world.emit_event(@event);
}
}
```
## OpenZeppelin + Dojo Integration
OZ components compose with Dojo contracts using the same `component!()` pattern:
```cairo
#[dojo::contract]
pub mod Vault {
use openzeppelin::token::erc20::extensions::erc4626::ERC4626Component;
use openzeppelin::access::accesscontrol::AccessControlComponent;
component!(path: ERC4626Component, storage: erc4626, event: ERC4626Event);
component!(path: AccessControlComponent, storage: accesscontrol, event: AccessControlEvent);
// ERC4626 hooks for custom deposit/withdraw logic
impl ERC4626HooksImpl of ERC4626Component::ERC4626HooksTrait<ContractState> {
fn before_deposit(ref self: ERC4626Component::ComponentState<ContractState>, ...) {
let mut contract_state = self.get_contract_mut();
contract_state.pausable.assert_not_paused();
let world = contract_state.world(@NAMESPACE());
// ... custom logic using world
}
}
}
```
## Permissions
### Configuration-Based (deployment)
```toml
# dojo_sepolia.toml
[[writers]]
tag = "NUMS-Config"
address = "NUMS-Play"
[[writers]]
tag = "NUMS-Game"
address = "NUMS-Play"
```
### CLI
```bash
sozo auth grant writer NUMS-Position,NUMS-actions
sozo auth grant owner NUMS,NUMS-admin
sozo auth revoke writer NUMS-Position,NUMS-actions
sozo auth list
```
### Runtime (Cairo)
```cairo
// Grant
world.grant_writer(selector_from_tag!("NUMS-Position"), system_address);
world.grant_owner(selector_from_tag!("NUMS"), admin_address);
// Check
let is_owner = world.is_owner(resource_selector, address);
let can_write = world.is_writer(resource_selector, address);
```
### Permission Types
| Type | Can write data | Can grant permissions | Can upgrade |
| ---------- | :--------------: | :-------------------: | :---------: |
| **Owner** | ✅ | ✅ | ✅ |
| **Writer** | ✅ | ❌ | ❌ |
| **Reader** | ✅ (always open) | ❌ | ❌ |
## Testing
### Test Setup (Nums pattern)
```cairo
use dojo_cairo_test::{
ContractDef, ContractDefTrait, NamespaceDef, TestResource,
WorldStorageTestTrait, spawn_test_world,
};
fn setup_namespace() -> NamespaceDef {
NamespaceDef {
namespace: "NUMS",
resources: [
TestResource::Model(m_Game::TEST_CLASS_HASH),
TestResource::Model(m_Config::TEST_CLASS_HASH),
TestResource::Event(e_Claimed::TEST_CLASS_HASH),
TestResource::Contract(Play::TEST_CLASS_HASH),
].span(),
}
}
fn setup_contracts() -> Span<ContractDef> {
[
ContractDefTrait::new(@"NUMS", @"Setup")
.with_owner_of([dojo::utils::bytearray_hash(@"NUMS")].span())
.with_init_calldata(array![/* init args */].span()),
ContractDefTrait::new(@"NUMS", @"Play")
.with_writer_of([dojo::utils::bytearray_hash(@"NUMS")].span()),
].span()
}
pub fn spawn_game() -> (WorldStorage, Systems) {
set_contract_address(OWNER());
let ndef = setup_namespace();
let world = spawn_test_world(world::TEST_CLASS_HASH, [ndef].span());
world.sync_perms_and_inits(setup_contracts());
let (play_address, _) = world.dns(@"Play").expect('Play not found');
let systems = Systems {
play: IPlayDispatcher { contract_address: play_address },
};
(world, systems)
}
```
### Integration Test
```cairo
#[test]
fn test_set_number() {
let (world, systems) = spawn_game();
// Execute action
systems.play.set(game_id, 5);
// Verify state
let game: Game = world.read_model(game_id);
assert(game.level == 1, 'wrong level');
}
#[test]
#[should_panic(expected: ('Game: is over',))]
fn test_cannot_play_after_game_over() {
let (world, systems) = spawn_game();
// ... setup game over state
systems.play.set(game_id, 0); // Should panic
}
```
### Test Utilities
| Function | Purpose |
| --------------------------------------------- | ------------------------------------- |
| `spawn_test_world(class_hash, [ndef].span())` | Create test world |
| `world.sync_perms_and_inits(contracts)` | Initialize contracts with permissions |
| `world.dns(@"contract_name")` | Resolve contract address |
| `world.read_model(key)` | Read model state |
| `world.write_model_test(@model)` | Write model (bypass permissions) |
| `world.erase_model(@model)` | Delete model |
### Cheat Codes
```cairo
use starknet::testing;
testing::set_caller_address(player);
testing::set_contract_address(contract);
testing::set_block_timestamp(1000);
testing::set_block_number(42);
```
## Namespace Pattern (Nums)
```cairo
// constants.cairo
#[inline]
pub fn NAMESPACE() -> ByteArray { "NUMS" }
// Usage in systems
let world = self.world(@NAMESPACE());
// Usage in DNS
world.dns_address(@"Token").expect('Token not found!')
```
## Deployment
### Build & Migrate
```bash
scarb build # Compile contracts
sozo test # Run tests
sozo migrate --profile sepolia # Deploy to Sepolia
sozo migrate --profile mainnet # Deploy to Mainnet
```
### Deployment Config
```toml
# dojo_sepolia.toml
[world]
seed = "nums-v1.0.49"
name = "nums"
[env]
rpc_url = "https://api.cartridge.gg/x/starknet/sepolia"
account_address = "0x..."
keystore = "./keystore_sepolia.json"
[[init_call_args]]
tag = "NUMS-Token"
execute = true
selector = "initializer"
calldata = ["0x...", "12000000000000000000000000"]
```
## Torii Indexer
```toml
# torii_sepolia.toml
world_address = "0x4e296..."
rpc = "https://api.cartridge.gg/x/starknet/sepolia"
[indexing]
polling_interval = 250
[grpc]
optimistic = true
[[contracts]]
type = "historical"
tag = "NUMS-TrophyProgression"
```
```bash
torii --config torii_sepolia.toml # Start indexer
```
## Quick Reference
| You want to... | Code |
| ---------------- | ---------------------------------------- |
| Get world | `let world = self.world(@NAMESPACE());` |
| Read model | `let game: Game = world.read_model(id);` |
| Write model | `world.write_model(@game);` |
| Emit event | `world.emit_event(@MyEvent { ... });` |
| Get caller | `starknet::get_caller_address()` |
| Generate ID | `world.uuid()` |
| Resolve contract | `world.dns(@"ContractName")` |Related Skills
dojo-world
Manage world permissions, namespaces, resource registration, and access control. Use when configuring world ownership, setting up authorization policies, or managing resource permissions.
dojo-token
Implement, deploy, and index ERC20 and ERC721 tokens in Dojo. Use when adding token contracts, deploying them, or configuring Torii to index balances and transfers.
dojo-test
Write tests for Dojo models and systems using spawn_test_world, cheat codes, and assertions. Use when testing game logic, verifying state changes, or ensuring system correctness.
dojo-system
Create Dojo systems that implement game logic, modify model state, and handle player actions. Use when implementing game mechanics, player commands, or automated logic.
dojo-review
Review Dojo code for best practices, common mistakes, security issues, and optimization opportunities. Use when auditing models, systems, tests, or preparing for deployment.
dojo-model
Create Dojo models for storing game state with proper key definitions, trait derivations, and ECS patterns. Use when defining game entities, components, or state structures.
dojo-migrate
Manage world migrations, handle breaking changes, and upgrade Dojo versions. Use when updating deployed worlds, migrating to new versions, or handling schema changes.
dojo-init
Initialize new Dojo projects with proper directory structure, configuration files, and dependencies. Use when starting a new Dojo game project or setting up the initial project structure.
dojo-indexer
Set up and configure Torii indexer for GraphQL queries, gRPC subscriptions, and SQL access. Use when indexing your deployed world for client queries or real-time updates.
dojo-config
Configure Scarb.toml, dojo profiles, world settings, and dependencies. Use when setting up project configuration, managing dependencies, or configuring deployment environments.
dojo-architecture
Shinigami architecture for fully onchain Dojo games — Elements, Types, Models, Components, Systems, Helpers, Store, Events, Interfaces. Use when structuring a new game, adding modules, understanding the codebase hierarchy, or implementing new game mechanics.
ui-ux-pro-max
UI/UX design intelligence for web and mobile. Includes 50+ styles, 161 color palettes, 57 font pairings, 161 product types, 99 UX guidelines, and 25 chart types across 10 stacks (React, Next.js, Vue, Svelte, SwiftUI, React Native, Flutter, Tailwind, shadcn/ui, and HTML/CSS). Actions: plan, build, create, design, implement, review, fix, improve, optimize, enhance, refactor, and check UI/UX code. Projects: website, landing page, dashboard, admin panel, e-commerce, SaaS, portfolio, blog, and mobile app. Elements: button, modal, navbar, sidebar, card, table, form, and chart. Styles: glassmorphism, claymorphism, minimalism, brutalism, neumorphism, bento grid, dark mode, responsive, skeuomorphism, and flat design. Topics: color systems, accessibility, animation, layout, typography, font pairing, spacing, interaction states, shadow, and gradient. Integrations: shadcn/ui MCP for component search and examples.