atomgraph/linkeddatahub/app

Claude Code and agent skill by atomgraph/linkeddatahub

Trust tier 3
  • community
Source check pending

Checking that the SKILL.md file is still in the repository.

Build a LinkedDataHub application declaratively with the ldh CLI — the document tree (containers and items), content blocks, SPARQL views in table/grid/list/map/graph layouts, charts, federated queries, public access, and an app directory installed with ldh push. Use when the task is to present data in an existing dataspace or to package an installable app. For the domain model and data imports use ldh:dataset; for rendering that data cannot declare (custom XSLT) use ldh:package.

Install

npx vskill@latest install atomgraph/linkeddatahub/app

SKILL.md

Build a LinkedDataHub application

A LinkedDataHub app is declared, not generated: it is RDF documents in the dataspace, with no code and no build step. Pages are documents. A page's content is an ordered list of blocks. Lists, tables, maps and charts are blocks backed by SPARQL queries. The whole app can live in a directory of Turtle files that ldh push writes into the dataspace, so it is versioned in git and re-installable.

References:

  • HTTP API for agents: https://github.com/AtomGraph/LinkedDataHub/blob/master/AGENTS.md
  • CLI (incl. push): https://docs.linkeddatahub.com/reference/command-line-interface/
  • Documents and blocks: https://docs.linkeddatahub.com/reference/data-model/
  • Worked examples, one directory per app:

https://github.com/AtomGraph/LinkedDataHub-Apps/tree/master/demo (northwind-traders: dashboards, faceted views, charts; copenhagen: map views)

Ground rules shared with the other ldh skills

  • The default graph is empty. Every query pattern goes inside GRAPH ?g { … }. Give each

linked resource its own graph variable, because each resource lives in its own document.

  • A document is not the thing it describes. The topic is <doc#this>, linked with

foaf:primaryTopic, and its properties go there.

  • Setup: export LDH_BASE=https://<dataspace>/ LDH_CERT_FILE=<owner keystore.p12> LDH_CERT_PASSWORD=….

The password is the deployment's, never a guess.

  • Use an `ldh` that matches the instance. Build it from the same LinkedDataHub checkout or release

as the running image (ldh --version). An instance whose end-user responses carry no https://w3id.org/atomgraph/linkeddatahub/dataspaces#admin Link (check with ldh get --head --accept text/turtle "$LDH_BASE") predates the CLI's admin discovery. There, ldh admin commands stop with "advertises no lds:admin link". An admin base advertises itself as its lds:admin only from 6.2.1 on. Use the image built from the same checkout as the CLI.

The document model

ResourceShape
Container (a section other documents live under)<> a dh:Container ; dct:title "…"
Item (a leaf page: a dashboard, a chart page, or a record)<> a dh:Item ; dct:title "…", plus foaf:primaryTopic <#this> when it describes a thing
Rich-text block<#intro> a ldh:XHTML ; rdf:value """<div xmlns="http://www.w3.org/1999/xhtml">…</div>"""^^rdf:XMLLiteral
Embedded resource block<#b> a ldh:Object ; rdf:value <resource>
Block order<> rdf:_1 <#intro> ; rdf:_2 <#b> on the document

Only `ldh:XHTML` and `ldh:Object` may be `rdf:_N` values. Views, charts and anything else are wrapped in an ldh:Object whose rdf:value names them. Bare ones are rejected by validation. XHTML must be well-formed, in the XHTML namespace, with each tag on one line.

Pick the type by what will live under the page, not by how it looks. A page made only of blocks (a chart page, a dashboard, an about page) with nothing beneath it is a dh:Item. Make it a dh:Container only when documents will be created under it, e.g. the target of an import. Both types take the same blocks.

Built-ins worth knowing: rdf:value ldh:ChildrenView lists the container's children, and the query ldh:SelectChildren selects them for a view of your own (e.g. a map of the children). ldh create container writes the children view as the first block; --no-children-view leaves it out, for a container that embeds its own view of them.

Views and charts

Views render resources; charts render numbers. A view's SELECT returns resources (their rows are navigable, facetable and linkable). A query with COUNT/SUM/GROUP BY belongs in a chart, never a view.

turtle
@prefix ldh: <https://w3id.org/atomgraph/linkeddatahub#> .
@prefix ac: <https://w3id.org/atomgraph/client#> .
@prefix dh: <https://w3id.org/atomgraph/linkeddatahub/document-hierarchy#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix dct: <http://purl.org/dc/terms/> .
@prefix sp: <http://spinrdf.org/sp#> .
@prefix spin: <http://spinrdf.org/spin#> .
<> a dh:Container ;
dct:title "Products" ;
rdf:_1 <#intro> ;
rdf:_2 <#products-block> ;
rdf:_3 <#per-category-block> .
<#intro> a ldh:XHTML ;
rdf:value """<div xmlns="http://www.w3.org/1999/xhtml"><p>Everything in the catalogue.</p></div>"""^^rdf:XMLLiteral .
<#products-block> a ldh:Object ;
dct:title "All products" ;
rdf:value <#products-view> .
<#products-view> a ldh:View ;
dct:title "All products" ;
spin:query <#select-products> ;
ac:mode ac:TableMode .
<#select-products> a sp:Select ;
dct:title "Select products" ;
sp:text """PREFIX schema: <https://schema.org/>
SELECT DISTINCT ?product
WHERE { GRAPH ?g { ?product a schema:Product ; schema:name ?name } }
ORDER BY ?name""" .
<#per-category-block> a ldh:Object ;
dct:title "Products per category" ;
rdf:value <#per-category> .
<#per-category> a ldh:ResultSetChart ;
dct:title "Products per category" ;
spin:query <#count-per-category> ;
ldh:chartType ac:BarChart ;
ldh:categoryVarName "categoryName" ;
ldh:seriesVarName "productCount" .
<#count-per-category> a sp:Select ;
dct:title "Count products per category" ;
sp:text """PREFIX schema: <https://schema.org/>
SELECT ?categoryName (COUNT(DISTINCT ?product) AS ?productCount)
WHERE {
GRAPH ?pg { ?product a schema:Product ; schema:category ?category }
GRAPH ?cg { ?category schema:name ?categoryName }
}
GROUP BY ?category ?categoryName
ORDER BY DESC(?productCount)""" .
  • View layout modes (ac:mode): ac:ListMode, ac:TableMode, ac:GridMode, ac:MapMode (the

resources need geo:lat/geo:long), ac:GraphMode.

  • Chart types (ldh:chartType): ac:BarChart, ac:LineChart, ac:ScatterChart.

ldh:categoryVarName and ldh:seriesVarName name the query's variables, without ?. ldh:seriesVarName may repeat (--series-var-name too); the series are drawn in the order the query's SELECT projects them.

  • A query runs against the dataspace's own endpoint unless it names a service:

<#q> ldh:service <#dbpedia> with <#dbpedia> a sd:Service ; dct:title "DBpedia" ; sd:endpoint <https://dbpedia.org/sparql> ; sd:supportedLanguage sd:SPARQL11Query. Time a remote query before building on it, and write it against the remote store's own graph layout.

  • Every non-optional pattern filters rows. A pattern that is there only to sort or to display

(?book schema:datePublished ?year for ORDER BY ?year) goes in OPTIONAL, or resources missing that value silently drop out of the view.

  • Per-resource panels ("orders of this customer") are property views in the ontology (ldh:dataset),

not blocks.

The app as a directory

ldh push maps a directory onto the document tree:

my-app/
├── root.ttl → PUT to $LDH_BASE (the root document itself)
├── products.ttl → PUT to ${LDH_BASE}products/
├── products/
│ ├── products.csv → uploaded into products/ (unless ignored)
│ └── featured.ttl → PUT to ${LDH_BASE}products/featured/
├── admin/model/ns.ttl → the ontology, installed separately (ldh:dataset), so ignored
└── .ldhignore → gitignore-style: admin/ *.sh *.csv

Relative URIs in each file resolve against its target URL, so write <> and <#…>. A subdirectory's document name.ttl sits beside it, not inside it. Non-RDF files are uploaded and addressed as ${LDH_BASE}uploads/{sha1}, and .xml files count as RDF/XML. Ignore CSVs that ldh import csv uploads itself.

`root.ttl` replaces the seeded root document, so keep its type: read it first (ldh get --accept text/turtle "$LDH_BASE") and start root.ttl from that. A push replaces every document it writes, so a re-push converges. It stops at the first failed request, and stdout lists what was written before it.

bash
ldh push --dry-run --dir my-app "$LDH_BASE" # parses every file, prints the plan, sends nothing
ldh push --dir my-app "$LDH_BASE"
Install order

An installable app is an install.sh that runs, in this order:

  1. ldh admin make-public (only if anonymous visitors should read it). Each run adds another

authorization, so run it once.

  1. Install the namespace ontology and any ldh packages add (ldh:dataset).
  2. ldh push --dir "$app_dir" "$LDH_BASE", which creates the containers the imports write into.
  3. ldh import csv|rdf per data file (ldh:dataset), then wait for each import to finish.

The script is self-contained. It needs ldh and nothing else, so it runs from wherever the app directory is copied. The demo apps in LinkedDataHub-Apps source a shared lib/ldh-app.sh, which is internal to that repository. Do not copy that source line, and do not reference any other checkout by path.

bash
#!/usr/bin/env bash
# Installs the app with the ldh CLI. Reads LDH_BASE, LDH_CERT_FILE, LDH_CERT_PASSWORD (and LDH_PROXY).
set -euo pipefail
: "${LDH_BASE:?}" "${LDH_CERT_FILE:?}" "${LDH_CERT_PASSWORD:?}"
app_dir="$(cd "$(dirname "$0")" && pwd)"
ldh admin make-public
ldh admin import ontology --file "$app_dir/admin/model/ns.ttl" # ldh:dataset
ldh push --dir "$app_dir" "$LDH_BASE"
# the target document the import is added to; --wait returns once the data is in, and fails if it is not
ldh import csv --title "Books" --query-file "$app_dir/books/books.rq" \
--csv-file "$app_dir/books/books.csv" --wait "${LDH_BASE}books/"

Every command takes the end-user base. The admin commands find the admin app from it, so the script never derives an admin. URI. Keep admin/, *.sh, *.rq and *.csv in .ldhignore so push does not upload them.

Finer access control: ldh admin create group, ldh admin add agent, ldh admin create authorization (https://docs.linkeddatahub.com/reference/administration/acl/). An authorization names documents with --to, and a subtree with --default <container>, which covers every document below the container. --to-all-in takes a class, but not a document class (dh:Item, dh:Container, def:Root or one above them), which the admin refuses: those classes are shared with the admin documents.

Building incrementally instead

For one-off changes to a live dataspace, the create/add commands each make one request and print the new URL:

bash
products=$(ldh create container --parent "$LDH_BASE" --title "Products" --slug products)
ldh add select --uri "${products}#select-products" --title "Select products" \
--query-file "$PWD/select-products.rq" "$products"
ldh add view --uri "${products}#products-view" --title "All products" \
--query "${products}#select-products" --mode https://w3id.org/atomgraph/client#TableMode "$products"
ldh add object-block --value "${products}#products-view" --title "All products" "$products"
ldh add xhtml-block --value '<div xmlns="http://www.w3.org/1999/xhtml"><p>…</p></div>' "$products"
ldh add result-set-chart --query <select URI> --chart-type https://w3id.org/atomgraph/client#BarChart \
--category-var-name categoryName --series-var-name productCount --title "…" "$products"
ldh remove block --block "${products}#products-block" "$products" # without --block: removes ALL blocks

Pass --uri so later commands can reference what you added; without it the resource is a blank node. If the app is meant to be reinstalled, put the change in the directory too, since the next push overwrites the document.

An object block whose value is an external URI loads it through the Linked Data proxy. When the dataspace describes that resource itself, in one of its own documents, --graph <document> makes the block read the description from there (ldh:graph), with no proxy request. Edits to the embedded resource then write to that document.

Verify

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 90 of 100Pattern checks on the skill's files.
  • Model reviewPassed

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

7-day trend score
0

Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/atomgraph/linkeddatahub/app/badge)](https://verified-skill.com/skills/atomgraph/linkeddatahub/app)

Something wrong with this skill or its page? Tell us.