Skip to content
Development
Command

/portaljs-define-schema

Define a dataset's metadata profile — infer a Frictionless Table Schema from its data, add Data Package metadata (license, sources, keywords), and write it into datasets.json so the showcase renders a typed field table. Extend or customize via the L0→L3 profile ladder.

From plugin
portaljs
2.3k25 skills25 commands
Install
> /plugin marketplace add datopian/portaljs
> /plugin install portaljs@datopian-portaljs

How it fires

How this command gets triggered: by you, by Claude, or both.

  • Fires itselfClaude auto-loads it when your prompt matches the work.
  • You can call itInvoke it directly when you want it.
  • Slash command/portaljs-define-schema

Context preview

What this command does when you run it.

Define a dataset's metadata profile — infer a Frictionless Table Schema from its data, add Data Package metadata (license, sources, keywords), and write it into datasets.json so the showcase renders a typed field table. Extend or customize via the L0→L3 profile ladder.

Command definition

portaljs-define-schema.md
description: Define a dataset's metadata profile — infer a Frictionless Table Schema from its data, add Data Package metadata (license, sources, keywords), and write it into datasets.json so the showcase renders a typed field table. Extend or customize via the L0→L3 profile ladder.
allowed-tools: Read, Write, Edit, Bash

/portaljs-define-schema

The **authoring** skill for the metadata-profile contract (`lib/metadata`). Where [`/portaljs-add-dataset`](/docs/skills/portaljs-add-dataset) registers *that a dataset exists*, this skill describes *what its data means*: it infers a Frictionless **Table Schema** (fields, types, constraints) from the data, adds the **Data Package** descriptor fields a catalog surfaces (title, licenses, sources, keywords), and writes them onto the dataset's entry in `datasets.json`. The showcase at `/@<namespace>/<slug>` then renders a typed column/description table instead of a bare preview.

The model is **Frictionless-native**. DCAT / DCAT-AP is a serialization layer on top (designed-in at `lib/metadata/dcat.ts`, built in the later DCAT-interop phase) — this skill authors the native model, not the export.

The skill is **interactive** and **never dead-ends**: if the brief is thin it interviews in short rounds, infers sensible defaults from the data, echoes the schema for confirmation, and lets you reply **"use defaults"** to accept the inferred schema as-is.

See [`lib/metadata/README.md`](https://github.com/datopian/portaljs/blob/main/examples/portaljs-catalog/lib/metadata/README.md) for the contract and the L0→L3 ladder.

The profile ladder — pick a level

Most datasets want **L0**. Reach for higher levels only when you actually need them.

| Level | What it is | When | |-------|-----------|------| | **L0** | Use the default `frictionless-tabular` profile as-is; just declare the dataset's schema + metadata. | Default. Tabular CSV/TSV with the standard Frictionless types. | | **L1** | Extend L0 — same default profile, plus a few extra package fields you care about. | You need extra descriptive metadata but standard validation is fine. | | **L2** | A fully custom profile (your own `schema` template + `validate()`), registered in `lib/metadata`. | A dataset type with validation rules L0 doesn't express. | | **L3** | Multiple profiles in the registry, resolved per dataset by its `profile` field. | A portal mixing dataset types, each with its own profile. |

Steps

1. Gather input from `$ARGUMENTS` (interview if thin)

Extract what's present:

  • `PORTAL_DIR` — portal directory (default: `.`)
  • `DATASET` — which dataset to describe, by `slug` or `namespace/slug`
  • `LEVEL` — `L0` (default) · `L1` · `L2` · `L3`

If `DATASET` is missing, read `PORTAL_DIR/datasets.json` and ask which one (list the slugs), then wait:

Which dataset should I define a schema for? (reply with a slug, or "all" to do each in turn)
  - country-codes        (reference)  — already has a schema
  - population-2022       (reference)  — no schema yet
  - co2-emissions         (reference)  — no schema yet
You can also say "use defaults" and I'll infer the schema from the data and confirm it.

Don't ask about the level up front — default to **L0** and only offer to go higher in step 4 if the data or the user's answers call for it.

2. Validate the portal directory

The target must be a `portaljs-catalog` portal with the metadata contract. Confirm `PORTAL_DIR/datasets.json`, `PORTAL_DIR/lib/metadata/types.ts`, and `PORTAL_DIR/pages/[owner]/[slug].tsx` exist. If `lib/metadata/` is missing, the portal predates the metadata-profile contract — tell the user and offer to proceed by writing the schema onto `datasets.json` anyway (the fields are optional and ignored by older showcases) rather than failing.

3. Infer the Table Schema from the data

Locate the dataset entry in `datasets.json`, then read its file from `PORTAL_DIR/public/data/<file>`. For CSV/TSV, sample the header + first ~50 data rows:

# Replace FILE with PORTAL_DIR/public/data/<file>.
head -1 FILE        # header → field names
sed -n '2,51p' FILE # sample rows → infer types

Infer each field's `type` from the sampled values, using the contract's vocabulary (`lib/metadata/types.ts` → `FieldType`): `integer` if every value matches `^[+-]?\d+$`; `number` if numeric but not all integers; `year` if all 4-digit; `boolean` for true/false/yes/no/0/1; `date`/`datetime` if `Date.parse`-able; `geopoint` for `"lon, lat"`; otherwise `string`. When a column is empty or ambiguous, default to `string` — never guess wildly. (This mirrors `coercesTo()` in `frictionless-tabular.ts`, so an inferred schema validates clean against its own data.)

Build a `TableSchema`:

  • One `Field` per column: `name` (exact header, spaces preserved), inferred `type`, and a

short `title` + `description` you draft from the column name and the dataset's purpose.

  • `constraints`: mark `required: true` for columns with no missing values in the sample;

`unique: true` for columns whose sampled values are all distinct; add a `pattern` only when the values clearly fit one (e.g. uppercase codes `^[A-Z]{2}$`).

  • `primaryKey`: the column (or composite) that uniquely identifies a row, if obvious.

For **JSON / GeoJSON** datasets, the L0 tabular profile doesn't apply — explain that schema authoring here covers tabular data, offer to capture package metadata only (step 5), and stop short of a `fields` schema.

4. Confirm the schema (and offer to go beyond L0)

Echo the inferred schema as a compact table for confirmation:

Inferred schema for <dataset> — say "go" to write it, or correct any field:

  field        type      required  unique  notes
  -----------  --------  --------  ------  -----------------------------
  <name>       <type>    <y/n>     <y/n>   <title / pattern>
  ...
  primaryKey: <col(s) or none>

This uses the default L0 'frictionless-tabular' profile.

Then, only if warranted, offer to go higher: "Want extra meta

Read more
Ships withportaljs

🌀 AI-native framework for building data portals. Scaffold a full portal from a brief and load datasets in minutes with agentic skills — any backend (CKAN, GitHub, Frictionless).

Get the whole plugin