| name | create-tutorial |
| description | Scaffold a new Membrane API Gateway tutorial in the api-gateway repo — the numbered self-teaching YAML under distribution/tutorials/<category>/, its support files and README links, and the matching auto-discovered integration test. Use whenever the user asks to create, add, write, or scaffold a tutorial (or a tutorial step), even if they only describe the gateway behavior they want to teach and don't say the word "skill" or name the files. |
Create a Tutorial
In this repo a tutorial is usually a runnable, self-teaching YAML config.
The leading comment block of the file is the lesson; the config below it is the working
example the reader runs. A complete tutorial therefore has three parts, and they must stay
in sync:
- The numbered YAML in
distribution/tutorials/<category>/ — the lesson plus the config.
- Category support files —
membrane.sh, membrane.cmd, a README.md (and
run-docker.* where the category offers Docker), so the reader can actually start it.
- An integration test under
distribution/src/test/java/com/predic8/membrane/tutorials/…
so the tutorial can't silently rot. These tests unzip the built distribution and start
Membrane against the tutorial's YAML, so a broken example fails CI.
A tutorial step can also be a plain .md file — use this when the step needs no local
Membrane config (e.g. a curl-only walkthrough against a hosted API). A .md step gets no
integration test; it just lives alongside the numbered YAMLs and is linked from the README.
Work through the steps below in order. The reader of a tutorial follows the file top-to-bottom
with no other docs open, so the single most important thing is that the YAML teaches itself.
Step 1 — Choose the category and filename
Categories live under distribution/tutorials/: getting-started, json, xml, advanced,
transformation, security, soap, orchestration, misc, ai/.... Pick the one that fits;
only create a new category if nothing fits (see "New category" at the end).
Files are numbered NN-Title.yaml (e.g. 10-PathParameters.yaml). The number sets reading
order. List the directory and pick the next gap — convention leaves room between numbers
(10, 20, 30…) so steps can be inserted later. Match the existing PascalCase-with-hyphens
title style.
Step 2 — Write the YAML (the lesson lives here)
Open a sibling file in the same category and mirror its shape. Every tutorial YAML has the
same skeleton:
api:
port: 2000
...
Hold to these conventions — they're what makes the set feel like one coherent course:
- Pin the schema line to whatever sibling files use (grep one:
grep -m1 schema= distribution/tutorials/<category>/*.yaml). It tracks the current release version, so don't
invent a number.
- Keep the comment block short. Steps are a command and its expected output — nothing more.
Don't explain what the config does; the reader can see the config. Don't repeat information
the
name: field already provides.
- No section-header comments above each
api: block. The name: field describes the API.
- Inline comments only for the non-obvious WHY — a surprising built-in, a hidden constraint.
Never comment what the interceptor name already says.
- Keep the config minimal and focused on the one concept the step teaches. Tutorials favor
clarity over completeness; richer setups belong under
distribution/examples/.
- Port 2000 is the house convention for tutorial listeners unless the lesson needs more.
- Use
return: status: 200 not return: {}.
- If this file continues a chain, end the previous file's comment block with a
"Continue with file NN-Title.yaml" pointer to the new one.
Step 3 — Make the category runnable
The reader runs ./membrane.sh -c <file> from the category directory, so that directory needs
the launcher scripts and a README.
- If
membrane.sh / membrane.cmd are missing from the category dir, copy them verbatim from
a sibling category — they are identical generic launchers that walk up to find MEMBRANE_HOME.
- Copy
run-docker.sh / run-docker.cmd only if the category documents a Docker workflow.
README.md: the category README points at the first file in the chain ("open
NN-First.yaml and follow the instructions there"). If you added the first
file of a new category, write this README modeled on a sibling's.
Step 4 — Write the integration test (don't skip this)
Without a test the example will drift out of sync with the code and break unnoticed — that's the
whole reason these tests exist. The harness starts Membrane from the built distribution with
your YAML and lets you assert against the live gateway.
Test package mirrors the category but with underscores: dir getting-started →
package com.predic8.membrane.tutorials.getting_started. The ExampleTests @Suite selects the
whole com.predic8.membrane.tutorials package, so a test placed correctly is auto-discovered
— no registration needed.
Each category has an Abstract<Category>TutorialTest that fixes getTutorialDir() (this is where
the hyphen dir name is restored). Your concrete test extends it, names the YAML, and asserts:
package com.predic8.membrane.tutorials.<category_with_underscores>;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.containsString;
public class <Name>TutorialTest extends Abstract<Category>TutorialTest {
@Override
protected String getTutorialYaml() {
return "NN-Title.yaml";
}
@Test
void <whatItVerifies>() {
given()
.when()
.get("http://localhost:2000/...")
.then()
.statusCode(200)
.body(containsString("..."));
}
}
Notes:
- Copy the license header from an existing sibling test rather than retyping it, so the year and
wording match the repo.
- Assert the observable behavior the YAML comments promise in Step 2 — if the lesson says
"you should see Customer: 7", assert exactly that. The test is the executable form of the lesson.
- If you created a brand-new category, also create its
Abstract<Category>TutorialTest modeled on
an existing one — it just returns the hyphenated dir name from getTutorialDir().
- Sub-categories (e.g.
security/jwt) need their own abstract base returning the full relative
path (security/jwt), not just the top-level category abstract. Model on AbstractAiTutorialTest
/ AbstractGoogleTutorialTest in distribution/src/test/java/com/predic8/membrane/tutorials/ai/.
.md steps have no integration test — skip this step for them.
Step 5 — Verify
Run the test through the existing run-example-test skill. Pass -b so the distribution is
rebuilt first: the tutorial YAML ships inside distribution/target/*.zip, and the tests run
from the unzipped distribution, so an un-rebuilt run would test the old YAML (or none).
.claude/skills/run-example-test/run-example-test.sh -b <Name>TutorialTest
A green run means the example actually starts and behaves as the lesson claims.
Common pitfalls
Template JSON auto-quoting
When a template interceptor has contentType: application/json, the engine automatically
adds quotes around every ${...} interpolation. Writing "${expr}" produces ""value"" and
breaks JSON. Always write interpolations unquoted:
- template:
contentType: application/json
src: |
{
"sub": ${user()},
"aud": "demo-resource"
}
String literals that are not interpolated keep their quotes as normal.
Built-in functions in Groovy templates
Prefer the built-ins over raw property access:
user() — authenticated username (from basicAuthentication security scheme)
scopes() — list of granted scopes
hasScope("read") — boolean scope check
Avoid the verbose ${property.'membrane.security.schemes'[0].username} — use ${user()} instead.
New category (only when nothing fits)
distribution/tutorials/<new-category>/ with the first NN-*.yaml, membrane.sh,
membrane.cmd, and a README.md.
- Add a section linking the new category in
distribution/tutorials/README.md.
- Create
com.predic8.membrane.tutorials.<new_category>.Abstract<NewCategory>TutorialTest
extending AbstractMembraneTutorialTest, returning the hyphenated dir from getTutorialDir().
- Add concrete tests as in Step 4.
Checklist