Developing Blocks

Introduction

Blocks are discrete components that allow users to view, explore, or edit data.

The Block Protocol defines a standard for communication between blocks and the applications that embed them.

The protocol is split into a core specification setting out how applications and blocks communicate, and module specifications defining what applications and blocks communicate.

This guide helps get you set up and introduces some of the key features of the graph module specification, which deals with creating, reading and updating data records (or “entities”).

In practice, most block developers will not need to know the lower-level details of the specifications, as the libraries we provide implement them.

Choosing your approach

We provide three templates which allow you to define the entry point for your block in different ways:

  • custom-element: create a block defined as a custom element (also known as Web Components).
  • html: create a block defined as an HTML file, with JavaScript added via <script> tags.
  • react: create a block defined as a React component.

To create a new block, run npx create-block-app@canary block-name --template template@canary, replacing block-name with the name to give your block, and template with one of the template names listed above (keep the @canary on the end!)

I want to use a different technology

If you want to write blocks using other technologies or frameworks, you have several options:

  1. use a custom-element template and use different approaches when constructing the element-i.e. use a custom element as a wrapper for anything else you like.
  2. use an html template and import and use your additional libraries inside the <script> tag.
  3. use a language which can be transpiled to JavaScript. As an example, see this blog post on writing a block using F#. This block uses React – you can use transpiled JavaScript in blocks defined as HTML files or custom elements too.

I don’t want to use TypeScript

You can write your block in regular JavaScript using the methods described above - just rename your files from *.tsx/*.ts to *.jsx/*.js, remove the types, and get coding.

Creating a block

  1. Move to a folder where you want to create your block.
  2. Run npx create-block-app@canary your-block-name --template react@canary (or --template custom-element@canary).
  3. Switch to your new folder: cd [your-block-name].
  4. Run yarn install && yarn dev or npm install && npm run dev.
  5. Open http://localhost:63212 in your browser to see the starter template.

The development environment

The create-block-app package and provides everything you need to develop a block.

  • src/app.tsx or src/app.ts contains your block’s code.
    • You can include dependencies in your block but bear in mind that the more dependencies you add, the bigger your block’s download size will be. Common dependencies which you can reasonably expect an embedding application to provide (e.g. React) can be defined as peerDependencies in package.json.
  • yarn dev or npm run dev will run your block in development mode, serving it locally with hot reloading at http://localhost:63212.
    • This uses the file at src/dev.tsx to render your block within a mock embedding application called MockBlockDock.
    • By default, dev mode will also show you the properties that are being passed to your block and the contents of the mock datastore. Remove debug from MockBlockDock to turn this off, or toggle it via the provided switch in the UI.
  • yarn build or npm run build will:
    • Bundle the component into a single source file (without any dependencies listed as peerDependencies).
    • Generate a block-metadata.json file which:
      • points to the bundled source file.
      • brings in metadata from package.json, such as the description.
      • brings in anything in the blockprotocol object in package.json, e.g.
        • blockType: the type of block this is.
        • displayName: a friendly display name
        • examples: an array of example data structures your block would accept and use
        • image: a preview image showing your block in action (in place of public/block-preview.png)
        • icon: an icon to be associated with your block (in place of public/omega.svg)
        • name: a slugified name for your block (which may differ to the package name in package.json); it can be defined as blockname or @namespace/blockname, where namespace must be your username on blockprotocol.org if you intend to publish it there
      • list the externals, which are generated from peerDependencies in package.json.

Updating the block schema

A key part of the Block Protocol is the use of types to describe the data your block will work with.

Your block should be associated with an “entity type” which will be used by embedding applications to understand what sorts of entities can be sent to it (e.g. what properties do they have?).

When an embedding application loads your block, it should send an entity which complies with the structure of the block's declared entity type. We call such an entity the 'block entity'.

See working with types for more information on the type system, or jump straight to your dashboard to create a type.

Once you have created the type representing the data your block needs, copy its URL, and update the schema property in the blockprotocol object in package.json. In TypeScript block templates, you can then run yarn schema to regenerate the types for your block.

Lifecycle of a block

When a block is loaded into an embedding application:

The embedding application parses its block-metadata.json file and:

  • provides any external dependencies which the block needs.
  • sets up message handling as described in the core specification.
  • loads the block with the appropriate strategy for its blockType.

The block then receives any data which the embedder can provide straight away, for example as part of the graph module:

  • custom-element and react-type blocks will be sent initial data as properties/props.
  • html-type blocks will be sent messages containing the initial data.
  • the block can then do whatever it chooses to do with those properties.

At any time after this initialization, the block may send further messages via a Module for specific purposes, such as reading and writing data within the embedding application.

The starter blocks created by create-block-app implement a simple example of this:

  1. the Thing entity type referred to in blockprotocol.schema in package.json, for which types are found in src/types.gen.ts, defines the properties expected for the block entity.
  2. mock values for the block entity are passed to MockBlockDock in dev.tsx, including the properties it expects
  3. the block receives the data for blockEntitySubgraph (described below):
  • the react and custom-element blocks extract the block entity from the blockEntitySubgraph
  • the html block registers a callback for the blockEntitySubgraph message.
  • each block then accesses the properties from the block entity and uses the name property to render its Hello, World! message.

Using the Graph Module

The Graph Module describes how entities can be created, queried, updated, and linked together, including the block entity. It enables your block to create, read, update, and delete data in the embedding application.

The Graph Module is available via the graphModule property in each starter template. It has a number of methods corresponding to the messages defined in the specification.

Using these methods in combination, you can create complex graphs from within a block without having to know anything about the implementation details of the application embedding it.

Each message payload is the same: an object containing data and errors keys.

The graph properties object

The graph properties object is sent in properties/props for custom-element and react-type blocks, and as a message for html-type blocks.

It contains data sent from the embedding application to the block related to the graph module. Importantly:

  • readonly: a boolean indicating whether the block is in a read-only context. This typically means that the embedding application will reject any requests to update data, and the block should alter its UI and behaviour accordingly.

  • blockEntitySubgraph: this contains the 'block entity' and any entities immediately linked to it. It is a graph of entities rooted at the block entity.

We provide helper tools for extracting the key information from blockEntitySubgraph:

  • in the react template, a useEntitySubgraph hook can return the rootEntity (the block entity) and any linkedEntities
  • in the custom-element template, this.getBlockEntity() and this.getLinkedEntities() are available
  • in the html template (and everywhere), you can use functions available in @blockprotocol/graph/stdlib, for example:
    • getRoots to get the roots from a subgraph (for a blockEntitySubgraph, there should only be one)
    • getOutgoingLinkAndTargetEntities to get the entities linked from a given entity (the 'target' entities), and the links themselves (N.B this is equivalent to linkedEntities in the other templates, which are for outgoing links from the block entity onl)
    • getIncomingLinkAndTargetEntities to get the entities linking to a given entity, and the links

Many of the messages sent from the application to the block as part of the graph module return a Subgraph. You shouldn't have to worry about the internal workings of a Subgraph, but it is worth knowing that a given subgraph represents the result of a query starting with a given entity (or entities) and following links from it to other entities. The links are also entities (they may have properties and relationships of their own), known as 'link entities'.

The four components of a Subgraph are:

  • roots: the entities which were the starting point of the query (e.g. only the 'block entity' in the case of blockEntitySubgraph)
  • depths: which edges were followed from the roots when resolving the query, and how far
  • edges: connections between things in the graph. For example, a 'link entity' connects it and two other entities via hasRightEntity and hasLeftEntity edges (conceptually, the link entity is in the middle with an entity on its left and on its right)
  • vertices: each element of the graph which was encountered when starting from the roots and following the specified edges to the specified depths.

Again, you probably don't need to worry about this when getting started – but if you start to work with complex data networks made up of many entities with different relationships, the Subgraph and knowing how to query it becomes a powerful tool.

Updating the block entity

A common use for the Graph Module is to update the block entity – to update the properties that are sent to the block.

Each block template includes a demonstration of calling graphModule.updateEntity to update the block entity.

To do this, you need to call updateEntity using the entityId of the blockEntity:

// Update the block entity, and receive the updated entity in return
const { data, errors } = await graphModule.updateEntity({
  data: {
    entityId: blockEntity.metadata.recordId.entityId,
    entityTypeId: blockEntity.metadata.entityTypeId,
    properties: {
      "https://blockprotocol.org/@blockprotocol/types/property-type/name/":
        "Bob",
    },
  },
});

How you get a reference to blockEntity depends on the type of block, as described above and demonstrated in each template.

As soon as the updateEntity call is processed, for react and custom-element blocks your block will be re-rendered with the updated properties. You could therefore omit the { data, errors } from the above snippet and rely on the updated properties when the block is re-rendered.

If you’re using the custom-element template, you have a helper method to achieve the above:

this.updateSelfProperties({
  "https://blockprotocol.org/@blockprotocol/types/property-type/name/": "Bob",
});

Because properties are identified by URLs, you may wish to alias them in your code if used in multiple places. For example:

const nameKey =
  "https://blockprotocol.org/@blockprotocol/types/property-type/name/";

if (blockEntity.properties[nameKey] !== "Bob") {
  await graphModule.updateEntity({
    data: {
      entityId: blockEntity.metadata.recordId.entityId,
      entityTypeId: blockEntity.metadata.entityTypeId,
      properties: { [nameKey]: "Bob" },
    },
  });
}

You can read more about how types are described on the working with types page.

Creating new entities

You can create new entities using the createEntity method.

New entities should be assigned starting properties and an entityTypeId (a URL pointing to their entity type).

Linking entities together

Because links between entities are just a special kind of entity, you call createEntity to create them, specifying additional linkData to indicate which entities are on the 'left' and 'right' of the link. For now you can think of the 'left' entity as the source of the link, and the 'right' entity as the target or destination.

For example, to link an entity to the block entity:

// link the 'blockEntity' to some 'otherEntity' you have a reference to (e.g. if you have newly created it)
graphModule.createEntity({
  data: {
    entityTypeId:
      "https://blockprotocol.org/@blockprotocol/types/entity-type/friend/v/1",
    linkData: {
      leftEntity: blockEntity.metadata.recordId.entityId,
      destinationEntityId: otherEntity.metadata.recordId.entityId,
    },
    properties: {}, // this can contain metadata about the link, if you wish
  },
});

You can define the type of relationships between your block entity and other entities when defining its type, and the entityTypeId of the relevant relationship will then be available in the generated types (after running yarn schema)/

Any entities linked directly from the block will appear in the blockEntitySubgraph property.

You can also link other entities together, but whether or not they appear in blockEntitySubgraph will depend on whether they are connected to the block entity at all, and far away they are (what depths are required to reach them).

Exploring the data store

There are messages for exploring the data available in the embedding application:

Retrieving the subgraph rooted at a specific entity

If you know of a specific entity that you want to retrieve more detailed information on, you can call getEntity with its entityId. When doing this, you can provide how much of the graph you want to explore around the entity, by providing graphResolveDepths.

For example, to retrieve the subgraph rooted at the entity, with its links and their destinations:

const getEntityResponse = await graphModule.getEntity({
  data: {
    entityId: someEntityId,
    graphResolveDepths: {
      hasLeftEntity: {
        incoming: 1,
        outgoing: 0,
      },
      hasRightEntity: {
        incoming: 0,
        outgoing: 1,
      },
    },
  },
});

if (getEntityResponse.errors) {
  // handle errors
}

const { data: entitySubgraph } = getEntityResponse;

Here graphResolveDepths specifies two edge kinds,

  • hasLeftEntity - the edge that appears between a link entity and its left entity (which can be thought of as the 'source' of the link)
  • hasRightEntity - the edge that appears between a link entity and its right entity (which can be thought of as the 'destination' of the link)

The process of exploring a graph is known as 'traversal'. This involves visiting a vertex (an element in the graph – in our case, an entity), and then following edges from it (connections to other vertices) based on the parameters of the query. The parameters specify which kinds of edges to follow, and how many times they should be followed for any given journey through the graph. Each vertex visited might have multiple edges that can be followed from it, which means that each query actually involves many different journeys through the graph, which we refer to as 'branches' of the traversal.

For each edge kind, we specify the incoming and outgoing depths to resolve, where:

  • for hasLeftEntity
    • outgoing means there is an outgoing edge from the left entity to the link entity, and defines the number of times to explore the left entity of link entities in a given branch of traversal.
    • incoming means there is an incoming edge from the link entity to the left entity, which can be thought of as an "outgoing link entity" to the entity being traversed. The depth defines the number of times to explore outgoing link entities of entities in a given branch of traversal.
  • for hasRightEntity
    • outgoing means there is an outgoing edge from the right entity to the link entity, and defines the number of times to explore the right entity of link entities in a given branch of traversal.
    • incoming means there is an incoming edge from the link entity to the right entity, which can be thought of as an "incoming link entity" to the entity being traversed. The depth defines the number of times to explore incoming link entities of entities in a given branch of traversal.

Therefore, the parameters in the example mean:

  • (hasLeftEntity, incoming, 1) - explore outgoing link entities to a depth of 1
  • (hasLeftEntity, outgoing, 0) - do not explore the left entity of link entities
  • (hasRightEntity, incoming, 0) - do not explore incoming link entities
  • (hasRightEntity, outgoing, 1) - explore the right entity of link entities to a depth of 1

Filtering the data store

However, it's also possible to query across all entities in the datastore. The queryEntities message allows you submit a query with a filter. You can use this to browse the available entities in order to display them, or create links between them.

Example: Filtering entities by property value

To retrieve the subgraph rooted at all entities with a property https://blockprotocol.org/types/@blockprotocol/property-type/name/ with a value of "Alice" or "Bob":

const nameIsFilter = (value) => ({
  field: [
    "properties",
    "https://blockprotocol.org/types/@blockprotocol/property-type/name/",
  ],
  operator: "EQUALS",
  value,
});

const queryEntitiesResponse = await graphModule.queryEntities({
  data: {
    operation: {
      multiFilter: {
        filters: [nameIsFilter("Alice"), nameIsFilter("Bob")],
        operator: "OR",
      },
    },
  },
});

if (queryEntitiesResponse.errors) {
  // handle errors
}

const { data: subgraph } = queryEntitiesResponse;

const entities = getRoots(subgraph);

Here we use the getRoots method from @blockprotocol/graph/stdlib to retrieve the entities at the roots of the subgraph.

Example: Filtering entities by entity type

To retrieve the subgraph rooted at all entities with an entity type with base URL https://blockprotocol.org/types/@blockprotocol/entity-type/person/:

const entityTypeBaseUrlEqualsFilter = (value) => ({
  field: ["metadata", "entityTypeId"],
  operator: "EQUALS",
  value,
});

const queryEntitiesResponse = await graphModule.queryEntities({
  data: {
    operation: {
      multiFilter: {
        filters: [
          entityTypeBaseUrlEqualsFilter(
            "https://blockprotocol.org/types/@blockprotocol/entity-type/person/",
          ),
        ],
        operator: "AND",
      },
    },
  },
});

if (queryEntitiesResponse.errors) {
  // handle errors
}

const { data: subgraph } = queryEntitiesResponse;

const entities = getRoots(subgraph);
Customizing the query

Note, that we could have provided a graphResolveDepths parameter similar to the getEntity query example above, but have opted to not do so here which will default to a depth of 0 for all edge kinds, so the Subgraph will only return the roots.

More information on building filters can be found within the documentation of the queryEntities message in the specification.

Going further with the graph module

If you are using TypeScript, the types for methods available on graphModule (as defined in the @blockprotocol/graph package) should help you understand what methods are available and how they operate.

Build

Once you’ve finished writing your block, run yarn build or npm run build.

This will produce a compiled version of your code in the dist folder, along with a metadata file describing your block (block-metadata.json).

It is worth updating the blockprotocol object in package.json to include your own icon, image, and examples for your block. These will automatically be included in the block-metadata.json produced after running yarn build or npm run build.

You now have a block package that you can provide to apps to use, by publishing it on the Hub.

Publish

Once you've built a block, you can add it to the Hub, so that your block will have an instant online demo playground, and will be searchable via our block API.

To publish a block on the Hub take the following steps.

Pre-requisite: obtain an API Key

  • If you don't yet already have an account, click the sign up button in the top-right corner of any page on the Þ website.
  • Once signed in, create an API key and copy it to your clipboard.

Publish from your terminal

  • Run npm run build or yarn build to create a production build of your block (it will appear in the dist folder)
  • run npx blockprotocol@canary publish to generate a .blockprotocolrc file when prompted
  • Replace the placeholder key in that file with your API key
  • now run npx blockprotocol@canary publish again
  • See your block on the Hub!

Updating your block

You can update your published block at any time by running npm run build && npx blockprotocol@canary publish or yarn build && npx blockprotocol@canary publish

Have your block verified

In order to have your block enter the queue to be reviewed for verification status, please ensure the repository field in your package.json points to a public repository containing your code, and add a commit field to the blockprotocol object within package.json that specifies the commit hash the version you are publishing was built from.

Read more about verification in the FAQ.

Changing the Hub preview

  • Take a screenshot of your block in action and replace public/block-preview.png
  • update the description in package.json
  • add an icon to the public folder and update blockprotocol.icon in package.json
  • add a preview image to the public folder and update blockprotocol.image in package.json
  • update your block’s README.md – it will appear below your block on its hub page if you change it from the default
  • update the example properties object in blockprotocol.examples in package.json