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.
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!)
If you want to write blocks using other technologies or frameworks, you have several options:
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.html
template and import and use your additional libraries inside the <script>
tag.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.
npx create-block-app@canary your-block-name --template react@canary
(or --template custom-element@canary
).cd [your-block-name]
.yarn install && yarn dev
or npm install && npm run dev
.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.
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.
src/dev.tsx
to render your block within a mock embedding application called MockBlockDock
.debug
from MockBlockDock
to turn this off, or toggle it via the provided switch in the UI.yarn build
or npm run build
will:
peerDependencies
).block-metadata.json
file which:
source
file.package.json
, such as the description.blockprotocol
object in package.json
, e.g.
blockType
: the type of block this is.displayName
: a friendly display nameexamples
: an array of example data structures your block would accept and useimage
: 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 thereexternals
, which are generated from peerDependencies
in package.json.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.
When a block is loaded into an embedding application:
The embedding application parses its block-metadata.json
file and:
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.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:
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.MockBlockDock
in dev.tsx
, including the properties
it expectsblockEntitySubgraph
(described below):react
and custom-element
blocks extract the block entity from the blockEntitySubgraph
html
block registers a callback for the blockEntitySubgraph
message.Hello, World!
message.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.
graph
properties objectThe 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
:
react
template, a useEntitySubgraph
hook can return the rootEntity
(the block entity) and any linkedEntities
custom-element
template, this.getBlockEntity()
and this.getLinkedEntities()
are availablehtml
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 linksMany 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 faredges
: 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.
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.
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).
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).
There are messages for exploring the data available in the embedding application:
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:
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.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 1However, 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.
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.
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);
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.
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.
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.
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.
npm run build
or yarn build
to create a production build of your block (it will appear in the dist
folder)npx blockprotocol@canary publish
to generate a .blockprotocolrc
file when promptednpx blockprotocol@canary publish
againYou can update your published block at any time by running
npm run build && npx blockprotocol@canary publish
or yarn build && npx blockprotocol@canary publish
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.
public/block-preview.png
description
in package.jsonpublic
folder and update blockprotocol.icon
in package.json
image
to the public
folder and update blockprotocol.image
in package.json
README.md
– it will appear below your block on its hub page if you change it from the defaultblockprotocol.examples
in package.json
Previous