Skip to main content

Custom Extensions for Developers

TurboWarp supports custom extensions specified via URL parameter.


Our documentation about custom extensions in general leaves a lot to be desired, especially for unsandboxed extensions. We know. We're working on it.

The easiest way to load a custom extension is to open the add extension library then select "Custom Extension". You can also use the ?extension= URL parameter.


Projects that use custom extensions cannot be uploaded to the Scratch website. The project can only be loaded by a TurboWarp instance with the same extensions already loaded from the URL.

Introduction to writing custom extensions


This page assumes you are already familiar with JavaScript. If you don't already understand JavaScript, reading this page will not be an enjoyable experience.

This page is still a work in progress. You're welcome and encouraged to suggest or even write improvements on GitHub.


The example extension URLs found in this document should NOT be used in real projects. There is no guarantee they will continue to work in the future.

Custom extensions run in a sandbox and have limited access to scratch internals. This is very limiting, but custom extensions can still access powerful APIs such as fetch, WebSocket, and other web APIs.

Due to technical limitations of the sandbox, every call to a custom extension will stop the current script for a full frame, regardless of turbo mode or run without screen refresh.

Custom extensions need to be loaded from a website, for example GitHub Pages. For development, you may find it best to set up a local HTTP server. If you happen to have Python installed, that can be as easy as python -m http.server.

Hello, world!

The simplest custom extension looks something like this:

// We use class syntax to define our extension object
// This isn't actually necessary, but it tends to look the best

class MyExtension {
* Scratch will call this method *once* when the extension loads.
* This method's job is to tell Scratch things like the extension's ID, name, and what blocks it supports.
getInfo() {
return {
// `id` is the internal ID of the extension
// It should never change!
// If you choose to make an actual extension, please change this to something else.
// Only the characters a-z and 0-9 can be used. No spaces or special characters.
id: 'myextensionexample',

// `name` is what the user sees in the toolbox
// It can be changed without breaking projects.
name: 'Cool Extension',

blocks: [
// `opcode` is the internal ID of the block
// It should never change!
// It corresponds to the class method with the same name.
opcode: 'hello',
blockType: Scratch.BlockType.REPORTER,
text: 'Hello, world!'

* Corresponds to `opcode: 'hello'` above
hello() {
// You can just return a value: any string, boolean, or number will work.
// If you have to perform an asynchronous action like a request, just return a Promise.
// The block will wait until the Promise resolves and return the resolved value.
return 'Hello, world!';

// Call Scratch.extensions.register to register your extension
// Make sure to register each extension exactly once
Scratch.extensions.register(new MyExtension());

Test this example here. (Do not use this link to make real projects!)

Save this in a file on your computer and use a local HTTP server to load it using the extension parameter. If you have a local HTTP server started with python -m http.server, you might visit The extension will add a new category that contains a single reporter block that outputs "Hello, world!"


getInfo is a function that tells Scratch about your extension and what blocks it supports.

  • id is a string that represents a unique ID that this extension uses. It should only contain the characters a-z and 0-9. The same extension should always use the same ID as changing it will break existing projects.
  • name is a string that tells Scratch what name to display in the sidebar
  • blocks is a list of objects that defines which blocks your extension contains.
    • opcode is the internal name of the block, and also corresponds to the method that will be called when the block is run. Opcodes should never be changed.
    • blockType defines the type of block
      • Scratch.BlockType.REPORTER makes a round reporter
      • Scratch.BlockType.BOOLEAN makes a triangle shaped reporter that can fit into boolean inputs
      • Scratch.BlockType.COMMAND makes a stacked block
    • text is a string that defines what the block will be named in the editor. Text in [brackets] will be converted to slots for arguments (see below examples)
    • arguments is an object that defines the arguments that the block accepts. Each argument has:
      • type which defines the input shape to make. Note that regardless of the type set here, the values actually passed to the block are not guaranteed to be casted to the right type. You should manually convert arguments to numbers, for example, as they will often be passed as strings.
        • Scratch.ArgumentType.STRING for string inputs
        • Scratch.ArgumentType.NUMBER for number inputs
        • Scratch.ArgumentType.BOOLEAN for boolean inputs (defaultValue is ignored)
        • Scratch.ArgumentType.ANGLE for angles
        • Scratch.ArgumentType.COLOR for colors (#123abc string format)
        • Scratch.ArgumentType.MATRIX for a 5x5 matrix (passed in 11101010101... string format)
        • Scratch.ArgumentType.NOTE for music notes
      • defaultValue is the initial value when the block is created from the library
    • disableMonitor can be set to true to force remove the checkbox by a block to create a monitor

A more complex example

You probably didn't understand a word of the above. Here's a more thorough example that uses arguments:

class StrictEqualityExtension {
getInfo() {
return {
id: 'strictequalityexample', // change this if you make an actual extension!
name: 'Strict Equality',
blocks: [
opcode: 'strictlyEquals',
blockType: Scratch.BlockType.BOOLEAN,
text: '[ONE] strictly equals [TWO]',
arguments: {
ONE: {
type: Scratch.ArgumentType.STRING,
defaultValue: 'First value'
TWO: {
type: Scratch.ArgumentType.STRING,
defaultValue: 'Second value'
strictlyEquals(args) {
// Note strict equality: Inputs must match exactly: in type, case, etc.
return args.ONE === args.TWO;
Scratch.extensions.register(new StrictEqualityExtension());

Test this example here. (Do not use this link to make real projects!)

Remember that you must reload to apply changes.


If for some reason the extension is not appearing, or if the page never finished loading, open up your developer console and search for errors. Chances are there's going to be something in there.

Aside from that, custom extensions are just JavaScript scripts. You can use console.log and debugger as much as you'd like.


Custom extensions run in a sandbox that limits what they can access for security reasons. This "sandbox" is actually a sandboxed invisible <iframe> (using the sandbox attribute) and a restricted feature policy. Note that this differs from regular Scratch.

  • Custom extensions cannot access VM internals
  • Custom extensions cannot access any variable that wasn't explicitly passed in as an argument
  • Custom extensions cannot directly interact with sprites at all
  • Custom extensions CAN access most web APIs and open connections to servers (as long as CORS allows it)

Backwards Compatibility

Extensions should strive to be as backwards compatible as possible. Notably, that means:

  • NEVER change the extension ID
  • NEVER remove blocks
  • NEVER change opcodes
  • NEVER significantly change behavior of blocks
    • That doesn't mean you can't change it slightly or fix bugs, but any valid call to your blocks should always do the same thing, in perpetuity
  • Extensively test your extension on existing projects

Failing to follow these guidelines may effectively corrupt projects.

Differences compared to Scratch

TurboWarp's custom extensions have a few notable changes compared to custom extensions in Scratch or E羊icques.

  • TurboWarp runs custom extensions in a sandboxed third-party <iframe> while Scratch uses a first-party Worker. This means the custom extension runs on a separate origin (it can't access's indexedDB, for example), but it has access to more DOM APIs such as the Gamepad API.

Additional resources