Contest statements#
A contest statement is the joined task sheet: one document that pulls every
problem's statement into a single book, usually behind a cover page and shared
chrome. It lives in contest.rbx.yml.
The idea that makes it work is that the contest owns the templates that wrap each problem, both inside the book and when a problem is built on its own. The problem brings content; the contest decides how it looks. That is what lets you restyle eight problems by editing one file, and what lets a problem written for last year's regional drop into this year's book unchanged.
In the sections below we'll go through the two problem templates, how a contest
statement joins the problems that share its (language, variant), and then the
extras: custom blocks, cover pages, extends, and what happens when a problem
refuses to build.
The contest owns the templates#
A problem statement is only content: the blocks of what the problem says. It carries no template of its own. The document structure lives on the contest statement, in two fields:
| Field | Produces | Used by |
|---|---|---|
standaloneProblemTemplate |
a full document for one problem | rbx st b |
contestProblemTemplate |
a fragment \subimport-ed into the book |
rbx contest st b |
Why two? Because a problem gets built in two different situations. Built on its
own, it needs a complete document: \documentclass, \begin{document}, the
works. Joined into the contest book, the book has already opened the document,
so each problem must contribute only its body, with no second
\documentclass. Same content, two wrappers.
Writing the two templates#
Writing that body twice is asking for the two copies to drift apart. Put the shared body in one file and have both templates include it:
The standalone template above opens a full document and includes the body; the fragment includes the same body with no wrapper of its own. Change the body once and both builds follow.
A template is LaTeX with Jinja2 interpolation, and the way it places
content is by reading problem.blocks.<name>. Everything else in scope, the
samples, the limits, the filters, is listed in
Template context.
The (language, variant) join#
Inside the contest file, you walk the problems list and \subimport each
problem's rendered fragment:
%- for problem in problems
\subimport{\VAR{problem.import_dir}}{\VAR{problem.import_file}}
%- endfor
The loop above is the whole join. problem.import_dir and
problem.import_file are the handles rbx gives you for the fragment it
built from each problem using your contestProblemTemplate. They exist only
here, in the join context.
But which statement does each problem contribute? The join is by (language,
variant). A contest statement pulls, from every problem, the problem statement
whose (language, variant) matches its own, and the matched statement has to
share the contest statement's rbx-* type. So a contest statement declared as
en/default joins each problem's en/default statement, and that is the
whole rule.
That same key drives the standalone build. For rbx st b to build a problem
on its own, exactly one contest statement must carry a
standaloneProblemTemplate for that problem's (language, variant):
- Exactly one: that template is used. This is the normal case.
- More than one: a hard error, because two contest statements both claim the
same
(language, variant). Disambiguate by removing one of the templates. - Zero, or no contest at all: not an error. rbx falls back to the bundled default template and warns.
Unselected dispatcher
The zero-match fallback has one exception. If the contest here is an
unselected multi-contest dispatcher, rbx st b errors instead of falling
back. Pass -C <id>, or set RBX_CONTEST=<id>, to pick a contest.
Building the book#
rbx contest st b renders each problem with the contestProblemTemplate, joins
them through the contest file, and writes
build/<statement-name>[-<profile>].pdf, keyed by the contest statement's
name rather than its language. It builds the contest
documents in the same run.
When you select a contest variant with -C <id>, the book nests under
build/variants/<id>/ instead, so building one variant never overwrites
another's. The default contest keeps the bare build/ path. The heading over
the build summary names the variant it just built.
To build a single problem instead, run rbx st b from inside the problem
directory. It picks up the same contest's standaloneProblemTemplate and writes
build/statement-<lang>[-<variant>][-<profile>].pdf.
Problem artifacts are not variant-scoped yet
A problem's own build/ directory is shared across every contest variant. A
statement built there picks up the selected contest's chrome and the
problem's letter in that contest, but always lands on the same path -- so
switching variants overwrites it. Rebuild after switching. Tracked as
#753.
Declaring a contest statement#
A contest statement is one entry under statements: in contest.rbx.yml. Below
is a fully wired one, field by field:
statements:
- name: main-en # (1)!
language: en # (2)!
variant: default # (3)!
file: statements/contest-en.rbx.tex # (4)!
type: rbx-tex # (5)!
standaloneProblemTemplate: statements/problem-standalone.rbx.tex # (6)!
contestProblemTemplate: statements/problem-in-contest.rbx.tex # (7)!
params: # (8)!
show_limits: true
nameis required and unique within the contest. It identifies the entry, since positional arguments torbx contest st bare names, and it keys the output PDF. Problem statements have noname; contest ones must.languageis an ISO 639-1 code, and half of the join key.variantis an optional discriminator that defaults todefault. It is the other half of the join key.fileis the joined document itself, the contest book that iterates over the problems.typedefaults torbx-tex. Only therbx-*types can join problems, so a contest statement is almost alwaysrbx-texorrbx-md.standaloneProblemTemplateis the full-document template forrbx st b.contestProblemTemplateis the fragment template for the join.paramsare knobs exposed to the templates asparams.*, kept separate from problem and contestvars. See Template context.
variant, params and the two templates only mean something for the rbx-*
types. Set them on a non-rbx contest statement and rbx errors out. For the
exhaustive field list, see the contest
schema.
Custom blocks#
Block names are free-form, which means you can add a section the default chrome
knows nothing about. Define the block in the problem, then render it in the
template. Guard it with %- if ... is defined, since not every problem defines
every block:
The block above shows up only for problems that wrote a hint, and everyone
else's document is untouched.
Cover pages and infosheets#
Sometimes a contest needs a page that is not a problem at all: an infosheet, a
cover page, an instruction sheet. Those are documents, contest-only standalone
pages that never join problems, and they live in their own list:
documents:
- name: infosheet-en
language: en
file: statements/infosheet-en.jinja.tex
type: jinja-tex
Because a document never joins, its type has to be one that carries no blocks:
jinja-tex, jinja-md, tex, md or pdf, and never the joining rbx-*
types.
It does still receive the problems list, but metadata-only. Each entry
exposes title, short_name, limits, profiles and groups, and no
blocks, samples or import handles. That is enough for a summary page, such
as a per-problem limits table:
\begin{tabular}{c|cc}
{\sf Problem} & {\sf Time (ms)} & {\sf Memory (MB)} \\
\hline
%- for problem in problems
\VAR{problem.short_name}
& \VAR{problem.limits.timeLimit}
& \VAR{problem.limits.memoryLimit} \\
%- endfor
\end{tabular}
The table above walks the same problems list the contest book does, and reads
only metadata off each one. Documents are built by rbx contest st b, alongside
the contest statements.
Location and date#
location and date are per-language fields on a contest statement: the place
and the date exactly as they should read in that language. They surface in the
contest.* namespace, so a cover page can print them:
statements:
- name: main-en
language: en
file: statements/contest-en.rbx.tex
location: "Porto, Portugal"
date: "July 29, 2026"
- name: main-pt
language: pt
file: statements/contest-pt.rbx.tex
location: "Porto, Portugal"
date: "29 de julho de 2026"
Notice the two entries share a location but not a date string. Same event, each language phrasing it its own way.
Reusing a recipe with extends#
Across languages, two contest statements share almost everything except the
source file and the date. extends lets one entry inherit another's build
recipe and spell out only what differs.
A contest statement extends another by name:
statements:
- name: main-en
language: en
file: statements/contest-en.rbx.tex
standaloneProblemTemplate: statements/problem-standalone.rbx.tex
contestProblemTemplate: statements/problem-in-contest.rbx.tex
- name: main-pt
language: pt
extends: main-en # (1)!
file: statements/contest-pt.rbx.tex # (2)!
- Inherits the recipe from
main-en:typeand both templates. It does not inherit identity, somain-ptkeeps its ownname,languageandvariant. - Overrides the joined document, and inherits everything else.
The merge is an allowlist of the build recipe only: type, file,
params, assets, and for contest statements the two templates. It never
copies identity, and params deep-merges key by key rather than replacing
wholesale. Cycles and dangling references are errors.
Problem statements extend by language instead. See Reusing a recipe across languages.
When a problem cannot be rendered#
Contest statements are built independently of each other, so a broken English book never blocks the Portuguese one. Within a single book the rule is the opposite, and deliberately strict: if any problem cannot be rendered, because it has no statement in that language, or its samples failed to build, or its template is broken, then that statement fails. rbx will not quietly hand you a problemset PDF with a problem missing from it.
When you do want the incomplete document, proofreading a book mid-edit while
one problem is still being written, pass --partial:
# Build the contest book without the problems that fail, instead of failing.
rbx contest statements build --partial
--partial omits the failing problems and reports each one it dropped. Because
you asked for best-effort output, the command exits 0 when every statement was
produced this way.
Warning
Problem lettering follows the problems that made it into the document, so a partial book is not a preview of the final one. Don't ship it.
Learn through examples#
Reading about templates only gets you so far, and the fastest way in is a working one. The default preset ships a complete contest: a joined task sheet, both problem templates sharing one body file, an editorial and an infosheet document. Copy it and start deleting the parts you don't need.
From here, Tutorials does for editorials what this page did for statements.