Last Updated: 4/7/2026
Creating Custom Blocks
Agricultural Microworlds uses Blockly to provide a visual programming interface for students. Adding a new custom block requires three coordinated steps: defining the block’s visual structure, adding it to the toolbox, and implementing the JavaScript code generator that translates the block into simulation commands.
Prerequisites
Before creating a custom block, you should:
- Understand the existing Blockly Blocks Reference
- Be familiar with the Worker Command System
- Have the development environment running (see Installation & Setup)
All custom block code lives in agricultural-microworlds.client/src/SetUpCustomBlocks/.
Step 1: Define the Block Structure
Block definitions live in customBlockDefinitions.js. Each block is defined as a JSON object that describes its visual appearance, inputs, and behavior.
Basic Block Definition
Here’s the structure of a simple statement block:
var waitXDays = {
type: "wait_x_days",
message0: "wait %1 days",
args0: [
{
type: "input_value",
name: "DAYS",
check: "Number",
},
],
previousStatement: null,
nextStatement: null,
style: "control_blocks",
tooltip: "Wait for a certain number of days",
};
Blocks["wait_x_days"] = {
init: function () {
this.jsonInit(waitXDays);
},
};Block Definition Properties
| Property | Purpose | Example Values |
|---|---|---|
type | Unique identifier for the block | "wait_x_days", "move_forward" |
message0 | Display text with %1, %2 placeholders for inputs | "wait %1 days" |
args0 | Array of input field definitions | See input types below |
previousStatement | Allows blocks above (null = yes) | null for statement blocks |
nextStatement | Allows blocks below (null = yes) | null for statement blocks |
output | Type of value returned (for expression blocks) | "Number", "Boolean" |
style | Visual style reference from theme | "control_blocks", "movement_blocks" |
tooltip | Help text on hover | "Wait for a certain number of days" |
Input Types
The args0 array defines what inputs the block accepts:
Number Input:
{
type: "input_value",
name: "DAYS",
check: "Number",
}Dropdown Menu:
{
type: "field_dropdown",
name: "DIRECTION",
options: [
["left", "0"],
["right", "1"],
],
}Number Field (inline):
{
type: "field_number",
name: "NUM",
value: 1,
}Expression Blocks vs. Statement Blocks
Statement blocks (like move_forward, wait_x_weeks) execute actions and stack vertically. They have previousStatement and nextStatement set to null.
Expression blocks (like math_number, get_current_week) return values and plug into inputs. They have an output property instead:
var getCurrentDay = {
type: "get_current_day",
message0: "Current Day",
output: "Number",
style: "variable_blocks",
tooltip: "Gets the current day number from the simulation.",
};
Blocks["get_current_day"] = {
init: function () {
this.jsonInit(getCurrentDay);
},
};Step 2: Add to Toolbox
Once defined, add your block to the toolbox in toolboxConfig.js. The toolbox is organized into categories:
export const toolbox = {
kind: "categoryToolbox",
contents: [
{
kind: "category",
name: "Control",
categorystyle: "control_category",
contents: [
{ kind: "block", type: "toggle_harvesting" },
{ kind: "block", type: "toggle_seeding" },
{ kind: "block", type: "wait_x_weeks" },
{ kind: "block", type: "wait_x_days" }, // Add your new block here
],
},
// ... other categories
],
};Available Categories
The existing categories and their styles:
| Category | Style | Color | Purpose |
|---|---|---|---|
| Movement | movement_category | 300 (purple) | Tractor movement commands |
| Variables | variable_category | 330 (pink) | Variable operations and simulation state |
| Numbers | numbers_category | 230 (blue) | Numeric values and operations |
| Control | control_category | 190 (teal) | Simulation control (harvesting, seeding, waiting) |
| Logic | logic_category | 135 (cyan) | Conditionals and boolean operations |
| Loops | loops_category | 60 (green) | Iteration constructs |
| Events | events_category | 150 (light green) | Program entry points |
Choose the category that best fits your block’s purpose. The categorystyle determines the category’s color in the toolbox, while the block’s style property determines the individual block color.
Step 3: Implement the Code Generator
The code generator in blocklyJSGenerator.js translates your block into JavaScript code that runs in the Web Worker. This generated code calls simulation methods that send commands to the simulation engine.
Generator Function Structure
javascriptGenerator.forBlock["wait_x_days"] = function (block, generator) {
const days =
generator.valueToCode(block, "DAYS", generator.ORDER_ATOMIC) || "1";
return `await simulationMethods.waitXDays(${days});\n`;
};Reading Block Values
Input values (from input_value fields):
const duration =
generator.valueToCode(block, "DURATION", generator.ORDER_ATOMIC) || "1";Dropdown selections (from field_dropdown fields):
const direction = block.getFieldValue("DIRECTION");Number fields (from field_number fields):
const number = block.getFieldValue("NUM");Generator Return Values
Statement blocks return a string ending with \n:
return `await simulationMethods.waitXDays(${days});\n`;Expression blocks return an array [code, order]:
return ["simulationMethods.currentWeek", generator.ORDER_ATOMIC];The order constant (like generator.ORDER_ATOMIC, generator.ORDER_EQUALITY) controls operator precedence for proper parenthesization.
Async vs. Sync Commands
Commands that take time (movement, waiting, turning) use await:
return `await simulationMethods.moveForward(${duration});\n`;Instant state changes (toggling harvesting, switching crops) don’t:
return `simulationMethods.toggleHarvesting(${inputType});\n`;Complete Example: Wait X Days Block
Here’s a complete implementation of a hypothetical “wait X days” block:
1. Definition in customBlockDefinitions.js:
var waitXDays = {
type: "wait_x_days",
message0: "wait %1 days",
args0: [
{
type: "input_value",
name: "DAYS",
check: "Number",
},
],
previousStatement: null,
nextStatement: null,
style: "control_blocks",
tooltip: "Wait for a certain number of days",
};
Blocks["wait_x_days"] = {
init: function () {
this.jsonInit(waitXDays);
},
};2. Toolbox entry in toolboxConfig.js:
{
kind: "category",
name: "Control",
categorystyle: "control_category",
contents: [
{ kind: "block", type: "wait_x_weeks" },
{ kind: "block", type: "wait_x_days" }, // Add here
// ... other blocks
],
}3. Generator in blocklyJSGenerator.js:
javascriptGenerator.forBlock["wait_x_days"] = function (block, generator) {
const days =
generator.valueToCode(block, "DAYS", generator.ORDER_ATOMIC) || "1";
// Convert days to hours (24 hours per day)
return `await simulationMethods.waitXWeeks(${days} / 7.0);\n`;
};Note: This example reuses the existing waitXWeeks command by converting days to weeks. For a true waitXDays command, you would need to add a new handler in simulationEngine.js (see Worker Command System).
How Generated Code Becomes Worker Commands
When a student runs their Blockly program:
- Blockly generates JavaScript from the blocks using your generator functions
- The generated code runs in a Web Worker with access to
simulationMethods - Each
simulationMethodscall sends a message to the main thread with a command name and arguments - The simulation engine receives the message via
handleWorkerMessage()insimulationEngine.js - The engine executes the command (e.g.,
moveForward(),waitXWeeks()) which updates simulation state - For async commands, the engine sends a response back to the worker when complete
This architecture keeps the simulation loop running smoothly on the main thread while student code executes in a separate worker thread.
Pattern Reference: Existing Blocks
Study these existing blocks as patterns:
- Simple statement:
turn_left(no inputs, just an action) - Statement with number input:
move_forward(takes duration) - Statement with dropdown:
toggle_harvesting(ON/OFF choice) - Statement with both:
turn_x_degrees(number input + direction dropdown) - Expression returning number:
get_current_week(no inputs, returns value) - Expression with inputs:
custom_compare(two number inputs, returns boolean)
All patterns are fully implemented in the three source files mentioned above.
Testing Your Block
After implementing all three steps:
- Start the development server:
npm run dev - Open the Blockly workspace
- Find your block in the appropriate category
- Drag it into the workspace and connect it to other blocks
- Run the program and verify the simulation behavior matches your expectations
- Check the browser console for any JavaScript errors from the generated code
What’s Next
- Worker Command System: Learn how your block’s commands are processed by the engine
- State Management System: Understand the simulation state changes your commands trigger
- Simulation Managers: Explore how managers handle the resulting state updates