1. How StreamField Works
A StreamField is stored as a JSON array in a single database column. Each element has a
type key (the block name) and a value key (the block data):
[
{ "type": "heading", "value": { "text": "Why we built this", "size": "h2" }, "id": "abc1" },
{ "type": "paragraph", "value": "<p>It started with a simple problem...</p>", "id": "abc2" },
{ "type": "image", "value": 42, "id": "abc3" },
{ "type": "callout", "value": { "body": "Key insight here", "style": "info" }, "id": "abc4" }
]
The id per block enables stable cross-references, link targets, and preview anchors.
Wagtail 5+ uses use_json_field=True by default (the legacy StreamField
used a custom database type; use_json_field=True uses a plain JSONField).
2. Built-in Block Types
from wagtail.blocks import (
CharBlock, # Single-line text
TextBlock, # Multi-line plain text
RichTextBlock, # HTML rich text editor
BooleanBlock, # Checkbox
IntegerBlock, # Integer number
FloatBlock, # Float number
DecimalBlock, # Decimal number
RegexBlock, # Text validated against a regex
URLBlock, # URL field
EmailBlock, # Email field
DateBlock, # Date picker
TimeBlock, # Time picker
DateTimeBlock, # Combined datetime
ChoiceBlock, # Single choice dropdown
MultipleChoiceBlock, # Multi-select
PageChooserBlock, # Wagtail page foreign key
DocumentChooserBlock, # Wagtail document FK
ImageChooserBlock, # Wagtail image FK
SnippetChooserBlock, # Wagtail snippet FK
EmbedBlock, # oEmbed (YouTube, Vimeo, Twitter)
StaticBlock, # No value — renders a fixed template
StructBlock, # Dict of sub-blocks
ListBlock, # Ordered list of one block type
StreamBlock, # Ordered list of mixed block types
RawHTMLBlock, # Unescaped HTML (use with caution)
)
3. StructBlock — Composite Fields
StructBlock groups multiple sub-blocks into a named composite. Use it for
semantically related fields that always appear together:
from wagtail.blocks import StructBlock, CharBlock, RichTextBlock, URLBlock, ChoiceBlock
class CalloutBlock(StructBlock):
style = ChoiceBlock(choices=[
('info', 'Info'),
('warning', 'Warning'),
('success', 'Success'),
('danger', 'Danger'),
], default='info')
title = CharBlock(required=False)
body = RichTextBlock(features=['bold', 'italic', 'link'])
class Meta:
icon = 'warning'
label = 'Callout Box'
template = 'blocks/callout_block.html'
class CTABlock(StructBlock):
heading = CharBlock()
body = RichTextBlock(features=['bold', 'italic'])
btn_text = CharBlock(label='Button text')
btn_url = URLBlock(label='Button URL')
class Meta:
icon = 'link'
template = 'blocks/cta_block.html'
In the template, access sub-blocks via value.style, value.title, etc.
The Meta.template path is relative to any configured template directory.
4. ListBlock — Repeating Items
ListBlock stores an ordered list of a single block type. Good for feature lists,
step-by-step instructions, or team member grids:
from wagtail.blocks import ListBlock, StructBlock, CharBlock
from wagtail.images.blocks import ImageChooserBlock
class TeamMemberBlock(StructBlock):
name = CharBlock()
role = CharBlock()
photo = ImageChooserBlock(required=False)
bio = CharBlock(required=False, max_length=200)
class Meta:
icon = 'user'
class TeamGridBlock(StructBlock):
heading = CharBlock(required=False)
members = ListBlock(TeamMemberBlock())
class Meta:
icon = 'group'
template = 'blocks/team_grid.html'
In the template, iterate with {% for member in value.members %}.
5. StreamBlock — Nested Streams
You can nest a StreamBlock inside a StructBlock to create
two-column layouts or tab panels where each column/tab has its own stream:
from wagtail.blocks import StreamBlock, StructBlock, RichTextBlock, CharBlock
from wagtail.images.blocks import ImageChooserBlock
class ColumnContentBlock(StreamBlock):
paragraph = RichTextBlock()
image = ImageChooserBlock()
class Meta:
required = False
class TwoColumnBlock(StructBlock):
left = ColumnContentBlock()
right = ColumnContentBlock()
class Meta:
icon = 'placeholder'
label = 'Two Columns'
template = 'blocks/two_column.html'
{# blocks/two_column.html #}
<div class="two-col">
<div class="col-left">{% include_block value.left %}</div>
<div class="col-right">{% include_block value.right %}</div>
</div>
6. ChooserBlocks — Relational Content
Chooser blocks create foreign-key relationships to Wagtail objects. They render a picker widget in the admin:
from wagtail.blocks import StructBlock, CharBlock, PageChooserBlock
from wagtail.snippets.blocks import SnippetChooserBlock
from wagtail.images.blocks import ImageChooserBlock
class RelatedPostBlock(StructBlock):
post = PageChooserBlock(page_type='blog.BlogPage')
label = CharBlock(required=False, help_text='Override the post title for the link')
class Meta:
template = 'blocks/related_post.html'
class TestimonialBlock(StructBlock):
# Testimonial is a Snippet model
testimonial = SnippetChooserBlock('testimonials.Testimonial')
class Meta:
template = 'blocks/testimonial.html'
In the template, value.post gives you the full BlogPage instance —
you can call any model property or method on it. {% pageurl value.post %} gives the
correct URL accounting for the site root.
7. Custom Block Class
For complete control over rendering logic, subclass Block directly and override
render():
from wagtail.blocks import Block
from django.utils.html import format_html
import qrcode
import io
import base64
class QRCodeBlock(Block):
"""Renders a QR code image inline from a URL value."""
def render(self, value, context=None):
img = qrcode.make(value)
buf = io.BytesIO()
img.save(buf, format='PNG')
b64 = base64.b64encode(buf.getvalue()).decode()
return format_html(
'<figure class="qr-code"><img src="data:image/png;base64,{}" alt="QR code for {}"></figure>',
b64, value
)
class Meta:
icon = 'link'
label = 'QR Code'
8. Block Templates
Each block's template receives a value context variable (the block's data) and the
parent page's context. Always use {% load wagtailimages_tags %} for image blocks:
{# blocks/callout_block.html #}
<div class="callout callout--{{ value.style }}">
{% if value.title %}
<strong class="callout__title">{{ value.title }}</strong>
{% endif %}
<div class="callout__body">{{ value.body }}</div>
</div>
For blocks that should render nothing (a divider, a spacer), use StaticBlock
with its own template — no value needed:
from wagtail.blocks import StaticBlock
class DividerBlock(StaticBlock):
class Meta:
icon = 'horizontalrule'
label = 'Divider'
template = 'blocks/divider.html'
# admin_text shows in the block picker description
admin_text = 'Inserts a horizontal rule'
9. Querying StreamField Data
You can filter pages based on block content using PostgreSQL JSON operators via
__contains:
# Find all BlogPages whose body contains a callout block
from blog.models import BlogPage
pages_with_callout = BlogPage.objects.filter(
body__contains=[{'type': 'callout'}]
)
# Find pages with a specific callout style
pages_with_warning = BlogPage.objects.filter(
body__contains=[{'type': 'callout', 'value': {'style': 'warning'}}]
)
For extracting text from StreamField (for search indexing or summaries), use
StreamField.get_searchable_content():
page = BlogPage.objects.first()
# Returns a list of text strings from all text-bearing blocks
text_chunks = page.body.stream_block.get_searchable_content(page.body)
summary = ' '.join(text_chunks)[:500]
10. StreamField Migrations
Adding a new block type to an existing StreamField does not require a data migration — JSON data without the new block type is still valid. The migration only modifies the Python schema, not the stored data:
# After adding a new block type to BlogStreamBlock
python manage.py makemigrations blog
python manage.py migrate
Renaming a block type does require a data migration to update existing JSON records:
# migrations/0012_rename_callout_to_alert.py
from django.db import migrations
from wagtail.blocks.migrations.migrate_operation import MigrateStreamData
# wagtail.blocks.migrations provides helpers for renaming types
from wagtail.blocks.migrations.operations import RenameStreamChildrenOperation
class Migration(migrations.Migration):
dependencies = [('blog', '0011_previous')]
operations = [
MigrateStreamData(
app_name='blog',
model_name='BlogPage',
field_name='body',
operations_and_block_paths=[
(RenameStreamChildrenOperation(old_name='callout', new_name='alert'), ''),
],
),
]
The wagtail.blocks.migrations module (added in Wagtail 4.1) provides a set of
data migration operations: rename, remove, alter block value, and move blocks. Always run
these through standard Django migrations to keep them reversible and version-controlled.