Calendar events from forms in edufeed - NIP-101 templates that compose NIP-52 events

Von Edufeed ·

Calendar events from forms in edufeed

edufeed lets people propose and publish calendar events through a Nostr form instead of an app-specific input screen. The form is a NIP-101 template (kind 30168). A filled-in form is a NIP-101 response (kind 1069). A reviewer, or the author directly, turns the response into a NIP-52 calendar event with the educational attributes described in Calendar events in edufeed.

Because the form is an event, every client renders the same questions: the edufeed app, the edufeed WordPress plugins, or any other NIP-101 client. A plain NIP-101 client can still show the form and collect responses. It just won't compose the calendar event.

This article builds on edufeed's NIP-101 profile, NIP-101-EDU. That profile defines the base encoding, field-vocab bindings to SKOS vocabularies (NIP-VOCAB, kinds 39737/39738) and the field-output tag. This article adds one output target: NIP-52 calendar events.

Roles and flow

shared template (edufeed)  ──forkOf──▶  community copy (community key)
                                              │
                         visitor / member fills it in
                                              ▼
                              response (kind 1069, encrypted to community key)
                                              │
                               reviewer edits and publishes
                                              ▼
                              calendar event (kind 31922 / 31923)
  1. Shared template. edufeed publishes one calendar form template. It is the reference every client and community uses.
  2. Community copy. NIP-101 addresses each response to the template's author. A community that wants submissions publishes an unchanged copy of the shared template under its own key (see Community copies). Responses to the copy go to the community.
  3. Response. Anyone fills in the copy. The response is encrypted to the community key, as for any non-public NIP-101-EDU form.
  4. Publishing. Someone holding the community key decrypts the response, reviews and edits it, and publishes the calendar event. Clients may also skip the response and compose the event directly from the form when the user can sign it (a logged-in editor, for example).

The calendar output target

A template whose responses compose a calendar event declares it in its settings JSON:

{"output": "nip52-calendar"}

Each field that feeds the event carries a field-output tag with a cal: target:

["field-output", "<fieldId>", "cal:<target>"]

ext (NIP-101-EDU) is not used for calendar output. A field without a cal: target does not reach the event, though it stays in the response for the reviewer to read.

Targets

Target Field type (renderElement) Answer in the response Emitted on the event
cal:title text, required the title ["title", …]
cal:content textarea Markdown description event content
cal:allDay checkbox (boolean) true or false kind 31922 if true, else 31923
cal:start datetime see Dates and times start, and D for 31923
cal:end datetime see Dates and times end, and D for 31923
cal:tzid text, default Europe/Berlin IANA zone name start_tzid and end_tzid (31923 only)
cal:location text address, room, meeting link or online ["location", …]
cal:image url image URL ["image", …]
cal:t text-array hashtags one ["t", …] each, without a leading #
cal:r text-array URLs, event page first one ["r", …] each, in order
cal:participant text-array names or npubs p or participant tags, see Participants
cal:registrationRequired radio with options true, false true or false, or no answer ["registrationRequired", …]
cal:price number non-negative decimal, comma accepted ["price", <amount>, <currency>]
cal:currency text, default EUR ISO 4217 code currency slot of price
cal:eventAttendanceMode radio the full schema.org URI (option id) ["eventAttendanceMode", <uri>]
cal:educationalLevel vocabulary-bound (field-vocab) concept URIs concept triples, see Bildungsstufe

Rules that apply to every target:

Field types and answers

Field types, list answers and their ; encoding follow NIP-101-EDU. The datetime type and the allDayField setting used below are defined there.

Dates and times

Participants

Each entry of a cal:participant answer is either a person with a Nostr key or a named person:

The role comes from the field's settings, "participantRole": "<role>", using the calendar article's roles (participant, attendee, speaker, organizer, moderator). Without it, the role slot is left out. A template asks for speakers and organizers with two fields, each with its own role. Duplicates by pubkey or by name are emitted once.

Bildungsstufe

cal:educationalLevel is bound with field-vocab to the KIM Bildungsstufe scheme:

["field-vocab", "<fieldId>", "a", "39737:d2689e2f41dabfba953da26655a94ce2aa4e029c383ee921c6a4deafab99a612:educational-level", "wss://relay.edufeed.org"]

The answer holds the concepts' external URIs, the i tag of each kind 39738 concept (e.g. https://w3id.org/kim/educationalLevel/level_C). For each URI, the composer emits the calendar article's concept triple. Labels are taken from the concept's prefLabel tags, German first, then ["educationalLevel:type", "Concept"]. If the concept event cannot be fetched, the composer emits the :id tag and the :type tag without labels; readers fall back to the URI.

Responses without a Nostr key

Visitors without a key respond with a one-time key, as NIP-101-EDU describes under "Responders without a Nostr key". Hosting sites protect that path against spam themselves.

Community copies

A community copy is a kind 30168 event under the community's key that repeats the shared template:

["a", "30168:<sharedAuthorPubkey>:<d>", "<relay>", "forkOf"]

A client can show a community's form status from these rules alone:

Status Condition
not published no 30168 with this d tag by the community key
current the copy exists, and its name, settings, field, field-vocab and field-output tags equal the shared template's
outdated the copy exists, but those tags differ from the latest shared template
partially available the copy is missing on some of the community's relays

The community key does not have to be the key of a kind 10222 community definition. Which key receives a community's submissions is that community's choice.

Publishing

The event composed from a response, or directly from the form, is published as the calendar article describes:

A published response is not changed or deleted. Clients keep track of which responses they have handled. They may link a response to the event coordinate it produced.

Worked example

Community copy (fields shortened):

{
  "kind": 30168,
  "pubkey": "<community>",
  "tags": [
    ["d", "edufeed-calendar-event"],
    ["name", "Veranstaltung vorschlagen"],
    ["settings", "{\"output\":\"nip52-calendar\",\"confirmationMessage\":\"Danke! Wir prüfen den Vorschlag.\"}"],
    ["a", "30168:bdc21f93b1e2cb75608cecd7a0a00a779779d9367dc9798bd9f213f06c95bc48:edufeed-calendar-event", "wss://relay.edufeed.org", "forkOf"],
    ["field", "title", "text", "Titel", "[]", "{\"renderElement\":\"text\",\"required\":true}"],
    ["field", "allday", "text", "Ganztägig", "[]", "{\"renderElement\":\"checkbox\"}"],
    ["field", "start", "text", "Beginn", "[]", "{\"renderElement\":\"datetime\",\"required\":true,\"allDayField\":\"allday\"}"],
    ["field", "end", "text", "Ende", "[]", "{\"renderElement\":\"datetime\",\"allDayField\":\"allday\"}"],
    ["field", "mode", "option", "Format", "[[\"https://schema.org/OnlineEventAttendanceMode\",\"Online\"],[\"https://schema.org/OfflineEventAttendanceMode\",\"Präsenz\"],[\"https://schema.org/MixedEventAttendanceMode\",\"Hybrid\"]]", "{\"renderElement\":\"radio\"}"],
    ["field", "level", "text", "Bildungsstufe", "[]", "{\"renderElement\":\"select\",\"multiple\":true}"],
    ["field", "speakers", "text", "Vortragende", "[]", "{\"renderElement\":\"text-array\",\"participantRole\":\"speaker\"}"],
    ["field-vocab", "level", "a", "39737:d2689e2f41dabfba953da26655a94ce2aa4e029c383ee921c6a4deafab99a612:educational-level", "wss://relay.edufeed.org"],
    ["field-output", "title", "cal:title"],
    ["field-output", "allday", "cal:allDay"],
    ["field-output", "start", "cal:start"],
    ["field-output", "end", "cal:end"],
    ["field-output", "mode", "cal:eventAttendanceMode"],
    ["field-output", "level", "cal:educationalLevel"],
    ["field-output", "speakers", "cal:participant"]
  ],
  "content": ""
}

Decrypted response tags from an anonymous visitor:

[
  ["response", "title", "OER-Werkstatt", "{}"],
  ["response", "allday", "false", "{}"],
  ["response", "start", "2026-11-20T14:00", "{}"],
  ["response", "end", "2026-11-20T16:00", "{}"],
  ["response", "mode", "https://schema.org/OnlineEventAttendanceMode", "{}"],
  ["response", "level", "https://w3id.org/kim/educationalLevel/level_C", "{}"],
  ["response", "speakers", "Jane Doe", "{}"]
]

Calendar event published by the community after review (no time zone field, so the default Europe/Berlin applies):

{
  "kind": 31923,
  "content": "",
  "tags": [
    ["d", "<fresh id>"],
    ["h", "<community>"],
    ["title", "OER-Werkstatt"],
    ["start", "1795179600"],
    ["end", "1795186800"],
    ["start_tzid", "Europe/Berlin"],
    ["end_tzid", "Europe/Berlin"],
    ["D", "20777"],
    ["participant", "Jane Doe", "", "speaker"],
    ["eventAttendanceMode", "https://schema.org/OnlineEventAttendanceMode"],
    ["educationalLevel:id", "https://w3id.org/kim/educationalLevel/level_C"],
    ["educationalLevel:prefLabel:de", "Fortbildung"],
    ["educationalLevel:prefLabel:en", "Advanced training"],
    ["educationalLevel:type", "Concept"]
  ]
}

Known deviations in edufeed-app

Two edufeed-app behaviours currently differ from the NIPs this article follows. Both are tracked as issues. Readers of existing events and responses should know about them:

Shared template

The shared calendar form is published by the edufeed account (bdc21f93b1e2cb75608cecd7a0a00a779779d9367dc9798bd9f213f06c95bc48) with d = edufeed-calendar-event, on wss://relay.edufeed.org. Every community copy points to this coordinate, so it does not move.

Implementation tracked in edufeed-app: nostr:nevent1qvzqqqqx25pzq2xhe2wt5rjq7kvy8ngu65rur2gjjxtknazftg8n3hetp2cz8z7yqy2hwumn8ghj7un9d3shjtnwva5hgtnyv4mz7qgnwaehxw309ankjarwdaehgu3wvdhk6tcqyrvemkj2868x6cw74myxdqva4fhxyjzk7d9gxd4hadwrpld6dvs42uaaj99