Scripting guide
A script is a small JavaScript program that places Minecraft blocks. This guide covers writing one and running it in the browser.
Introduction
Scripts run entirely in your browser, inside an isolated JavaScript interpreter. Nothing is uploaded while you experiment: you write code on one side of the editor, press Run, and the blocks it places appear in the 2D and 3D viewers on the other side.
The point of a script, rather than a one-off build, is that other people can run it with different settings. A script that declares inputs gets a panel of controls beside the viewer, so someone who has never read your code can change the size, the material, or the seed and export the result.
Your first script
Open the editor, replace what is there with the following, and press Run. It lays an 8 by 8 stone floor and puts a torch in the middle of it.
function main(context) {
const { Creation } = context;
const creation = Creation('My first build');
creation.fillBox(0, 0, 0, 7, 0, 7, 'minecraft:stone');
creation.setBlock(3, 1, 3, 'minecraft:torch');
return { creation };
}Three things are worth noticing. Coordinates are integers, and y is the vertical axis, so the torch sits one block above the floor. fillBox takes two opposite corners and fills everything between them, which is far faster than looping. And every script hands its work back by returning { creation }; a script that builds a creation but never returns it produces nothing.
Block ids are the vanilla ones, with the minecraft: namespace. The editor autocompletes them as soon as you start typing inside a string, so you rarely have to look one up.
Anatomy of a script
A script is plain JavaScript with two special top-level functions. You may use TypeScript syntax if you prefer: types are stripped before the script is stored or run.
main(context, inputs)
Required. This is what runs when someone presses Run. It receives the context object holding every helper, and the current values of the inputs you declared. It must return an object containing a creation, and may also return a camera.
defineInputs(context)
Optional. Return a map of input definitions and the editor renders a control for each one. The values then arrive as the second argument to main, keyed the same way.
function defineInputs(context) {
const { Input } = context;
return {
size: Input.number({
label: 'Size',
description: 'Length of each side, in blocks',
defaultValue: 8,
min: 1,
max: 64,
}),
material: Input.block({
label: 'Material',
defaultValue: 'minecraft:stone',
}),
};
}
function main(context, inputs) {
const { Creation } = context;
const creation = Creation('Platform');
const last = inputs.size - 1;
creation.fillBox(0, 0, 0, last, 0, last, inputs.material);
return { creation };
}defineInputs runs before assets load
Nothing is loaded yet when defineInputs is called, so only the listing calls on Textures are available there, such as listBlockNames() or listMobModels(). Colour lookups, models and sprites belong in main.
Placing blocks
A creation collects block operations in the order you make them. Later writes to the same cell win, so you can lay a solid shape and then carve into it.
You do not need to build at the origin. The build is normalised to its own bounding box on export, so negative coordinates and whatever offsets are convenient for your maths are fine.
For anything large, prefer fillBox over nested loops: one operation covering a million blocks is cheaper to produce, store and render than a million operations. Loop only where the shape genuinely varies per block.
Block states
Stairs face a direction, logs have an axis, doors have a half. These are block states, and they are always passed as strings, including "true" and "false".
// States are always strings, including "true" and "false".
creation.setBlock(0, 0, 0, 'minecraft:oak_stairs', {
facing: 'east',
half: 'bottom',
shape: 'straight',
waterlogged: 'false',
});
// Ask rather than guess: a bare banner has no placeable default.
const states = Textures.getPlacementStates('minecraft:white_banner');
creation.setBlock(2, 0, 0, 'minecraft:white_banner', states);
// Two-cell blocks place their other half for you, in the right
// direction: this one call leaves a whole bed facing east, so the
// head lands in the cell at x=5.
creation.setBlock(4, 0, 0, 'minecraft:red_bed', { facing: 'east' });A block's own defaults are often not placeable ones, which is the most common reason a single-cell block looks wrong in the viewer. Textures.getPlacementStates returns the states that let a block stand on the ground, and Textures.getStateOptions lists every state a block accepts along with its legal values. Use them instead of guessing from the block name.
A block's render can reach outside its cell
A standing banner is one block, but its cloth is drawn rising about 1.6 blocks above the cell it sits in. That is a render, not a block: it adds nothing to the layer count, the material list or the build height, so a banner on the top layer still leaves the build one layer tall above whatever it stands on.
Two-cell blocks are placed whole
Doors, beds and tall flowers occupy two cells. You place one and setBlock adds the other half itself, carrying your states across and putting a bed's head at the end it faces. To place a lone half deliberately, name it: passing { half: 'upper' } or { part: 'head' } places just that one.
Inputs
Each helper on Input produces one control. The type of the value that reaches main follows from the helper you used.
| Helper | Value in main | Type-specific options |
|---|---|---|
Input.number | number | min, max, step |
Input.string | string | defaultValue |
Input.boolean | boolean | defaultValue |
Input.select | string | options: string[] | { value, label }[] |
Input.multiselect | string[] | options: string[] |
Input.block | string | options: block ids (defaults to every block) |
Input.file | ScriptFile | null | accept, required, paintable, allowMultipleFiles |
Options every input accepts
| Option | Type | Description |
|---|---|---|
label | string | Control label. Required. |
description | string | Help text under the control. |
defaultValue | varies | Value used before the viewer touches anything. |
folder | string | Groups this control with others of the same folder. |
order | number | Sort position within a folder. Lower renders first. |
visibleWhen | { input, in?, notIn? } | Hide the control unless another input holds one of these values. |
resetsInputs | string[] | Reset these inputs to their defaults when this one changes. |
resetValuesByOption | Record<string, Record<string, unknown>> | Preset values to apply instead of the defaults, keyed by this input's value. |
folder and order are worth using as soon as a script has more than a handful of controls, and visibleWhen keeps a panel honest when a script branches: an input that only matters in one mode should not be visible in the other.
Files and images
Scripts cannot reach the network, so anything from outside comes in through a file input. A file arrives as a ScriptFile: name, type, size, plus text for text files, dataUrl, and imageData for images.
imageData is { width, height, data }, where data is flat RGBA, four entries per pixel, rows running top to bottom. Since Y runs upwards in a build, you will usually flip the row index.
function defineInputs(context) {
const { Input } = context;
return {
image: Input.file({
label: 'Image',
accept: 'image/*',
paintable: true,
}),
};
}
function main(context, inputs) {
const { Creation, Textures } = context;
const creation = Creation('Pixel art');
const image = inputs.image;
if (!image || !image.imageData) return { creation };
const { width, height, data } = image.imageData;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const at = (y * width + x) * 4;
if (data[at + 3] < 128) continue; // Skip transparent pixels.
const block = Textures.findClosestBlockToColor(
data[at],
data[at + 1],
data[at + 2],
);
// Image rows run top-down, Y runs up, so flip the row.
creation.setBlock(x, height - 1 - y, 0, block);
}
}
return { creation };
}Setting paintable: true on an image input gives the viewer an in-app painter instead of a file picker, which is a good default for scripts where the image is a sketch rather than a photo.
A file input holding a structure file is special: the host parses it for you before main starts, and Structures.getLoadedSchem() hands back the blocks it contains. With no name it resolves the input called schematic; pass an input name to pick one when a script takes several builds, as a diff does.
function defineInputs(context) {
const { Input } = context;
// The host pre-parses a file input named exactly `schematic`.
return {
schematic: Input.file({ label: 'Build', accept: '.litematic,.schem,.nbt' }),
};
}
function main(context) {
const { Creation, Structures, Logger } = context;
const creation = Creation('Doubled');
const loaded = Structures.getLoadedSchem();
if (!loaded.ok) {
Logger.warn(loaded.error);
return { creation };
}
for (const op of loaded.operations) {
creation.setBlock(op.x * 2, op.y, op.z * 2, op.name, op.states);
}
return { creation };
}Framing the result
Return a camera next to the creation to choose where the 3D viewer starts. Useful when the interesting side of a build is not the one the default camera lands on.
return {
creation,
camera: {
position: [40, 30, 40],
target: [0, 8, 0],
},
};Marking blocks
Return highlights to have the 3D viewer draw a coloured border around named cells. The blocks keep their own models, so this says something about a cell without repainting the build: which blocks a diff added, which cells a check flagged, which ones a pass touched.
Each entry is a { color, blocks } pair, where color is a hex string such as #22c55e and blocks is a list of [x, y, z] cells. Up to eight groups and 200,000 outlined cells are drawn per run, and an entry the viewer cannot read is skipped rather than failing the script.
function main(context) {
const { Creation } = context;
const creation = Creation('Review');
const added = [];
creation.setBlock(0, 0, 0, 'minecraft:stone');
added.push([0, 0, 0]);
// Each block still renders as itself; only the border is coloured.
return {
creation,
highlights: [{ color: '#22c55e', blocks: added }],
};
}Reference: context
Both defineInputs and main receive a context object. Destructure what you need from it rather than reaching for globals.
function main(context, inputs) {
const { Creation, Logger, Textures, Structures } = context;
// ...
}| Helper | Purpose |
|---|---|
Creation | Builds the output. |
Logger | Writes to the editor's log panel. |
Textures | Block data: names, colours, states, models, mob and skin models. |
Structures | Vanilla jigsaw structures, and the schematic the viewer supplied. |
Tree, Trees, Rocks, Gradients | Prebuilt generators that return operations. |
The editor knows all of this
Full type definitions ship with the editor, so hovering a helper or pressing Ctrl + Space shows the exact signature and documentation inline. This page covers the parts you reach for most.
Reference: Creation
Creation(name?: string): CreationInstanceCreates the object your script builds into. Most scripts make exactly one, at the top of main.
| Parameter | Type | Description |
|---|---|---|
name | string | Name carried into exports. Defaults to "Untitled". |
Returns a new creation to place blocks into.
creation.setBlock(x, y, z, name, states?): voidPlaces one block. Writing to a cell twice keeps the later write. A two-cell block gets its second half placed too, unless the states you pass already name a half.
| Parameter | Type | Description |
|---|---|---|
x, y, z | number | Position. Integers; Y is up. |
name | string | Block id, for example minecraft:stone. |
states | Record<string, string> | Block states. Values are strings, including "true". |
creation.setBlock(0, 0, 0, 'minecraft:oak_log', { axis: 'y' });creation.fillBox(x1, y1, z1, x2, y2, z2, type): voidFills every cell between two corners. Prefer this to a loop whenever the block does not vary.
| Parameter | Type | Description |
|---|---|---|
x1, y1, z1 | number | One corner, inclusive. |
x2, y2, z2 | number | The opposite corner, inclusive. |
type | string | { name, states } | Block id, or a block with states. |
creation.fillBox(0, 0, 0, 15, 0, 15, {
name: 'minecraft:oak_log',
states: { axis: 'x' },
});creation.addOperations(ops): voidAppends operations in bulk. This is how the built-in generators get their output into a creation.
| Parameter | Type | Description |
|---|---|---|
ops | Operation[] | Operations, such as those a generator returns. |
creation.getOperations(): Operation[]Returns everything placed so far, which lets a script inspect or post-process its own output before returning.
creation.reset(): voidDiscards every operation, leaving an empty creation.
Reference: Input
Available on the context passed to defineInputs. See Inputs above for the full option tables.
Input.number(config)
Input.string(config)
Input.boolean(config)
Input.select(config)
Input.multiselect(config)
Input.block(config)
Input.file(config)Each returns a typed input definition. Return them from defineInputs keyed by the name you want to read in main.
sourceEntity: Input.select({
label: 'Entity',
options: Textures.listMobModels().map((model) => model.id),
visibleWhen: { input: 'sourceType', in: ['entity'] },
})Reference: Logger
Logger.info(...args)
Logger.warn(...args)
Logger.error(...args)
Logger.debug(...args)Writes a line to the log panel below the editor. Output is capped at 1,000 lines per run, so log summaries rather than one line per block.
Logger.info('placed', creation.getOperations().length, 'operations');Reference: Textures
The largest namespace, and the one that turns a script from a shape generator into something that knows about Minecraft. Only the listing calls work inside defineInputs.
Listing blocks
| Call | What it returns |
|---|---|
listBlockNames(mcVersion?) | Every block id, optionally restricted to one Minecraft release. |
listPaletteBlocks() | Full, opaque, survival-obtainable cubes: the set a colour match can land on. |
listBuildableBlocks() | Palette blocks that also stay put, leaving out sand, concrete powder and the light sources. |
listModelBlocks() | Blocks whose own model can be voxelized into a statue of themselves. |
listItems() | Every item there is sprite art for. |
listMobModels() | Mob model templates, as { id, displayName }. |
listEntityModels() | The same models worth making a statue of, including the armour stand. |
listPosePresets() | The poses a mob can be built in, as { id, displayName }. |
listMinecraftVersions() | The versions listBlockNames can filter by, oldest first. |
Colour matching
Textures.findClosestBlockToColor(r, g, b, palette?, options?): stringFinds the block whose baked colour is nearest an RGB value. On patterned blocks the default average can be a colour that appears nowhere in the texture, so dominant is usually the better match for anything glazed or patterned.
| Parameter | Type | Description |
|---|---|---|
r, g, b | number | Colour components, 0 to 255. |
palette | string[] | Restrict the search to these blocks. Defaults to every palette block. |
options | { colorSource?, metric? } | colorSource is "average" or "dominant"; metric is "lab" or "ciede2000". |
Returns the id of the closest block.
const block = Textures.findClosestBlockToColor(200, 60, 40, palette, {
colorSource: 'dominant',
metric: 'ciede2000',
});Textures.getBlockColor(blockName, colorSource?): { r, g, b } | nullThe inverse lookup. Pair it with findClosestBlockToColor to match one block to another, such as picking the carpet closest to a floor.
Returns the block's colour, or null when it has no colour data.
States and shapes
Textures.getPlacementStates(blockName): Record<string, string> | nullPass these to setBlock rather than placing a block bare. A model that picks its variant on facing resolves to nothing without one, buttons default to face=wall and float, and live corals default to waterlogged=true.
Returns states that let the block stand on the ground, or null.
Textures.getStateOptions(blockName): Record<string, string[]> | nullPair it with getPlacementStates when shape matters. A pane, fence or wall with nothing to connect to is drawn as a bare post, so ask for the connections you want.
Returns every state the block accepts and its legal values.
Textures.getSecondPart(blockName): { states, offset } | nullFor doors, beds and tall flowers; null for anything that stands on its own. setBlock already places the second half, so reach for this only when a script needs to know the shape a block will take, such as checking it has room before placing it.
Returns the other half's states and offset, or null.
Textures.getBlockTraits(blockName): BlockTraits | nullBuild-relevant facts about a block, for deciding whether to offer or place it. Beats guessing from substrings of the block name.
Returns flags such as BlockEntity, NeedsSupport and CreativeOnly.
Models and textures
Textures.getBlockModel(blockName, blockProperties?): BlockModel | nullThe raw model structure, for voxelizing a block into a larger statue of itself. Entity-style blocks such as chests and beds come back with synthesized geometry flagged by entityModel; ones with no geometry at all, such as skulls, return null.
Returns the model's elements and face textures, or null.
Textures.sampleTextureAtUV(textureName, u, v, options?): [r, g, b, a] | nullReads one pixel of a texture. Without exact, a non-square texture is treated as an animation strip and an unknown name silently samples the atlas corner, so pass it when either is possible.
| Parameter | Type | Description |
|---|---|---|
textureName | string | A texture name from a model's faces. |
u, v | number | Coordinates from 0 to 1. |
options | { exact?: boolean } | exact reads the texture's own rect and returns null when it is missing. |
Textures.getTextureSize(textureName): { width, height } | nullLets you voxelize at the resolution a block was actually drawn at: vanilla sprites are 16 by 16, resource packs may be 32 or 64.
Returns the size of one frame, or null.
Textures.getItemSprite(itemId): { width, height, data } | nullOnly the item named by a sourceItem input is loaded before a run, so asking for a different one returns null. Sprites are supplied at 64 pixels for art authored at 16, so average each 4 by 4 patch rather than reading single pixels.
Returns RGBA rows, top row first, or null.
Mobs and skins
Textures.getMobModel(name, options?): { blocks, size, sourceBounds } | nullVoxelizes a mob into blocks you can place. Hand the returned blocks to setBlock one at a time, or offset them first.
| Parameter | Type | Description |
|---|---|---|
name | string | A model id from listMobModels or listEntityModels. |
options | { scale?, offset?, textureName?, palette?, pose? } | Voxel scale, placement offset, the blocks to build it from, and how it stands. |
Textures.listPosePresets(): Array<{ id, displayName }>Pass an id straight back as pose, or pass the angles yourself: headTurn, headTilt, rightArmSwing, rightArmRaise, leftArmSwing, leftArmRaise, rightLegSwing and leftLegSwing, each in degrees. A swing is forward when positive, a raise lifts the arm away from the body, and the head turns to the mob's left and tilts down when positive. Each part turns around its own joint, so only a mob built like a person takes the lot: a four-legged mob turns its head and stands as it is. Rotation is resampled onto the block grid, so a statue posed at a steep angle wants a larger scale to stay clean.
Returns the poses a mob can be built in, for a picker.
Textures.getSkinModel(name, options): { blocks, size, sourceBounds } | nullThe same thing for a player model, textured with a skin the viewer uploads.
| Parameter | Type | Description |
|---|---|---|
name | string | A player model from getPlayerModels. |
options | { textureFile, scale?, offset?, palette? } | textureFile is a ScriptFile from a file input holding the skin image. |
Reference: Structures
Structures.getLoadedSchem(name?): { ok, operations?, error? }The host parses every structure file input before main runs, in any format the schematic importer supports, up to four per run. Check ok before reading operations: a build that is missing or unreadable reports its own error without costing you the others.
| Parameter | Type | Description |
|---|---|---|
name | string | The file input to read. Defaults to the one named schematic, or the first declared. |
Returns the parsed blocks of the schematic that input holds.
Structures.listLoadedSchems(): string[]In the order getLoadedSchem() resolves them, so the first entry is what a call with no name returns.
Returns the input names a schematic was parsed under.
Structures.generate(options): Promise<string[]>Generates a vanilla jigsaw structure such as a village or a pillager outpost, and renders it directly.
| Parameter | Type | Description |
|---|---|---|
options.structureId | string | A structure pool, e.g. "minecraft:village/plains/town_centers". |
options.depth | number | How many pieces to place. |
options.radius | number | Maximum horizontal radius. |
options.height | number | Maximum vertical height. |
const chunkIds = await Structures.generate({
structureId: 'minecraft:village/plains/town_centers',
depth: 7,
});Reference: Generators
Four namespaces produce ready-made shapes. All of them return setBlock operations anchored at the origin rather than drawing anything, so you can offset, filter or repeat their output before it reaches the creation.
function main(context) {
const { Creation, Trees, Rocks } = context;
const creation = Creation('Grove');
// Generators return operations rather than drawing anything themselves,
// so you can filter or offset them before they reach the creation.
creation.addOperations(Trees.generate({ species: 'Oak', seed: 1, count: 3, spread: 12 }));
creation.addOperations(Rocks.generate({ formation: 'Boulder', seed: 7, size: 5 }));
return { creation };
}| Namespace | Generates | Discovery |
|---|---|---|
Trees | Detailed trees and groves by species, with canopies, dressing and vines. | Trees.species() |
Rocks | Boulders, geodes and basalt columns. | Rocks.formations() |
Gradients | Linear, radial and four-corner block ramps with dithering. | options: kind, stops, dither, paletteFilter |
Tree | The older, parameter-driven tree generator. | Tree.getDefaults() |
Sandbox and limits
Scripts run in an isolated QuickJS interpreter. There is no DOM, no window, no document, no timers and no network access. A script that needs outside data takes a file input.
While a script runs
| Limit | Value |
|---|---|
| Memory | 128 MB |
| Run time | 10 minutes, then the run is interrupted |
| Source size in the editor | 1,000,000 characters |
| Coordinate range | ±30,000,000 |
| Log output | 1,000 lines, 2,000 characters per line |
| Blocks placed | Uncapped |
Troubleshooting
No main function defined
The script parsed but never declared main. Declare it as a top-level function, not nested inside another one and not assigned conditionally.
Invalid script block name
A setBlock or fillBox call received something that is not a block id string. This usually comes from a lookup that can miss, such as a palette index that ran past the end of an array.
Invalid script coordinate
A coordinate was NaN, infinite, or outside ±30,000,000. Almost always a division by zero or an input parsed into NaN.
The script runs but nothing appears
Check that main returns { creation }, and that the creation you return is the one you placed blocks into.
A block renders as a plain cube or a bare post
It was placed without usable states. Use Textures.getPlacementStates, and Textures.getStateOptions for the connections on panes, fences and walls.
Something else? Reach us from the FAQ page.