agentleFS
OpenLinkSoftwareOpenLinkSoftware/ai-agent-skills/resource-access-offers-generator/SKILL.md

resource-access-offers-generator

Skill42 starsChanged 50 days agoMITView on GitHub

Generate RDF Turtle offer/license/price bundles for File Access, Graph Access, API Access, and Chat Service products. Trigger on phrases like 'generate offers', 'create license bundle', 'make a file access offer', 'generate graph access offer', 'create API access offers', 'create chat service offers', or any request to produce resource-access offers in Turtle format.

The file

We split this skill into the rules it gives an agent

4 rules. About 0 are common, 0 are distinctive, and 4 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈0

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 4

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Run Post-Generation Checklist, save to the output directory the user specifies for this session (ask if not already given — this is …

    line 22
  2. Very specific:

    All the literal values shown ("File Access"@en, "ACL controlled file access"@en, "WebDAV Service"@en) are fixed conventions, including the …

    line 70
  3. Very specific:

    Every Offer needs skos:prefLabel (gate-enforced for API Access specifically — this isn't required on Offers generally, only on Licenses).

    line 215
  4. Very specific:

    Every Offer needs skos:prefLabel (same as API Access — not required on Offers generally).

    line 243

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Resource Access Offers Generator

Generate valid RDF Turtle documents containing File Access, Graph Access, API Access, or Chat Service offers.

Minimum Input Requirements

InputRequiredExample
License resource IRIYes (not for Chat Service — see its own section)URL of protected file/graph
PriceYes2.99
DescriptionYesFree text — name/prefLabel/comment auto-derived
Host platformNo (default: URIBurner)linkeddata.uriburner.com

Host Platform Profiles

URIBurner (linkeddata.uriburner.com), ODS-QA (ods-qa.openlinksw.com), Localhost (localhost)

Workflow

  1. Elicit, as separate required inputs: resource type (file/graph/api/chatservice), pricing model (one-time purchase vs. recurring subscription — a distinct question from resource type; never infer it silently from how the price happens to be phrased), host, license IRI, price (plus billing interval/unit when the pricing model is subscription), description
  2. Derive name, prefLabel, comment from description
  3. Load prompt template from prompts/
  4. Substitute placeholders, generate Turtle
  5. Validate syntax (rdflib), validate SHACL (scripts/validate-offers-shacl.py)
  6. Run Post-Generation Checklist, save to the output directory the user specifies for this session (ask if not already given — this is user/machine-specific, e.g., previously /Users/jacqui/source/ttl/my_git_forks/virtuoso-82-offers-description/ for JacquiHand's machine, which won't exist on another contributor's machine), provide loading instructions

Document Metadata Block (source:)

Immediately after the @prefix declarations, every generated file MUST open with a schema:CreativeWork block describing the Turtle file being generated itself — never the licensed resource (file/graph/API being sold). Declare:

@prefix source: <http://virtuoso.openlinksw.com/data/turtle/{output-filename}> .
@prefix :       <#> .

source: a schema:CreativeWork ;
    schema:name "{Document title}" ;
    schema:description "{What this document's offers cover}" ;
    schema:author <http://www.openlinksw.com/#this> ;
    cc:attributionName "OpenLink Software" ;
    schema:datePublished "{dateTime}"^^xsd:dateTime ;
    schema:dateModified "{dateTime}"^^xsd:dateTime .

{output-filename} is the actual filename this Turtle file will be saved as (matching the convention in the two prior art files: http://virtuoso.openlinksw.com/data/turtle/FileAccessOffers-License-Prices.ttl, http://virtuoso.openlinksw.com/data/turtle/OPALOffers-Licenses-Prices-ods-qa-SPARQL-API.ttl) — this host/path is fixed regardless of which host platform profile the licensed resource lives on. Every generated Offer, License, and PriceSpecification node MUST carry wdrs:describedby source: pointing at this document node — that relation is what "identifies" the document as their source. The licensed resource itself is never separately described as a schema:CreativeWork — it's referenced only via opllic:uriParameter/opllic:graphParameter. The SHACL gate enforces wdrs:describedby (sh:class schema:CreativeWork) as required on Offer, License, and PriceSpecification shapes, and DocumentShape requires schema:name/schema:author/schema:datePublished/schema:dateModified on whatever node that resolves to.

Entity IRI Naming Convention

The Offer, License, and PriceSpecification each get their own dedicated path segment — see references/offer-iri-patterns.md for the full template table and host_suffix derivation:

  • PriceSpecification: http://data.openlinksw.com/oplweb/offer-unitprice/{OfferIdentifier}PriceSpecification#this — no host suffix.
  • License: http://data.openlinksw.com/oplweb/license/{OfferIdentifier}License{host_suffix}#this
  • Offer: http://data.openlinksw.com/oplweb/offer/{OfferIdentifier}Offer{host_suffix}#this

{OfferIdentifier} is a PascalCase name for the specific thing being offered (e.g., DataTwinglerSpecificModuleEntryLevel), derived from the offer's subject — not an ad-hoc slug and not reusing the Product's IRI shape. {host_suffix} is host_short with its first letter capitalized (URIBurner → URIBurner, ods-qa → Ods-qa, localhost → Localhost). The SHACL gate enforces the path segment and substring for each (sh:pattern on the shape itself, checked against the focus node's own IRI) — an Offer/License/PriceSpecification IRI that doesn't follow this shape now fails, even if every other property is correct.

File Access Product (WebAPI) Description Template

The Product node for a File Access offer MUST follow this full template — not a minimal stub:

<http://data.openlinksw.com/oplweb/{host_short}FA#this> a schema:WebAPI ;
    schema:name "File access via {host_short}"@en ;
    schema:applicationCategory "File Access"@en ;
    schema:applicationSubCategory "ACL controlled file access"@en ;
    schema:description """Attribute-based, access control constrained access to a File hosted by {host_hostname}."""@en ;
    skos:related <http://data.openlinksw.com/oplweb/{host_short}OPAL#this> ;
    schema:provider [
        a schema:Organization ;
        schema:name "OpenLink Software"@en ;
        schema:url <https://www.openlinksw.com/> ;
    ] ;
    schema:url <https://{host_hostname}/> ;
    schema:hasPart [
        a schema:WebAPI ;
        schema:name "WebDAV Service Endpoint"@en ;
        schema:description """WebDAV Endpoint for file interactions."""@en ;
        schema:serviceType "WebDAV Service"@en ;
        schema:url <https://{host_hostname}/DAV>
    ] .

Notes:

  • All the literal values shown ("File Access"@en, "ACL controlled file access"@en, "WebDAV Service"@en) are fixed conventions, including the @en language tag — the gate checks exact term equality, so a plain string without the tag fails.
  • schema:url on the top-level Product is the host root (https://{host_hostname}/), not the /DAV path — the /DAV endpoint belongs to the nested schema:hasPart sub-resource's schema:url.
  • skos:related points at the OPAL ACL server Product (http://data.openlinksw.com/oplweb/{host_short}OPAL#this, see references/offer-iri-patterns.md) — required because an OPAL server is what actually creates the ACLs backing file access.
  • The SHACL gate targets this shape via sh:targetSubjectsOf schema:applicationCategory rather than sh:targetClass schema:WebAPI — the nested WebDAV sub-resource is also a schema:WebAPI but has no schema:applicationCategory, so it's correctly excluded from this shape and validated only by its own (lighter) constraints (schema:name, schema:description, schema:serviceType "WebDAV Service"@en, schema:url).

Graph Access Product (WebAPI+Service) Description Template

The Product node for a Graph Access offer MUST follow this full template — not a minimal stub. It mirrors the File Access template one-for-one, substituting the SPARQL/knowledge-graph vocabulary:

<http://data.openlinksw.com/oplweb/{host_short}DA#this> a schema:WebAPI, schema:Service ;
    schema:name "Knowledge graph access via {host_short}"@en ;
    schema:applicationCategory "Data Access"@en ;
    schema:applicationSubCategory "ACL controlled knowledge graph access"@en ;
    schema:description """Attribute-based, access control constrained SPARQL access to a named graph hosted by {host_hostname}."""@en ;
    skos:related <http://data.openlinksw.com/oplweb/{host_short}OPAL#this> ;
    schema:provider [
        a schema:Organization ;
        schema:name "OpenLink Software"@en ;
        schema:url <https://www.openlinksw.com/> ;
    ] ;
    schema:url <https://{host_hostname}/> ;
    schema:hasPart [
        a schema:WebAPI ;
        schema:name "SPARQL Query Service Endpoint"@en ;
        schema:description """HTTP SPARQL Query Service Endpoint providing access to the named graph."""@en ;
        schema:serviceType "SPARQL Query Service"@en ;
        schema:url <https://{host_hostname}/sparql>
    ] .

Notes (identical reasoning to the File Access template, see references/offer-iri-patterns.md for the {host_short}DA#this / {host_short}OPAL#this IRI patterns):

  • Fixed-convention literals ("Data Access"@en, "ACL controlled knowledge graph access"@en, "SPARQL Query Service"@en) all need the @en tag — exact term equality.
  • schema:url on the top-level Product is the host root, not /sparql — the SPARQL endpoint path belongs on the nested schema:hasPart sub-resource's schema:url.
  • The Product is typed both schema:WebAPI and schema:Service (unlike File Access, which is schema:WebAPI only).
  • Same sh:targetSubjectsOf schema:applicationCategory scoping trick as File Access — the nested SPARQL sub-resource is also a schema:WebAPI but has no applicationCategory, so it's excluded from this shape and validated only by its own lighter nested constraints.
  • The License's opllic:graphParameter should be the actual named graph IRI being sold access to (e.g., urn:jch:test), not the SPARQL endpoint URL — the endpoint URL belongs on the Product's hasPart sub-resource. A sample opllic:uriParameter query URL scoping default-graph-uri to that graph is good practice (see the two prior-art reference files) but not gate-enforced.

Graph Access Authorization Block (ConditionalGroup + acl:Authorization)

Every generated Graph Access offer file MUST also include, in the same output file, a self-contained authorization block granting the purchaser actual read access to the sold graph — otherwise the offer/license/price bundle exists but nothing enforces it. This mirrors the pattern in the prior-art Offers-authorizations-and-restrictions.ttl (see oplacl:demo-graph-access-world-cup-meshup / <.../group/DemoGraphAccessUsersWorldCupMeshup#this>), reusing the same oplofr:{OfferIdentifier}Offer resource-specific type from the Entity IRI/Type conventions above as the purchase-check predicate:

<http://data.openlinksw.com/oplweb/group/{OfferIdentifier}GraphAccessUsers#this> a oplacl:ConditionalGroup ;
    foaf:name "Users identified using a NetID based Identifier who have purchased the {offer name} offer and whose license is in date" ;
    oplacl:hasCondition [
        a oplacl:GroupCondition, oplacl:QueryCondition ;
        oplacl:hasQuery """prefix oplprchs: <http://www.openlinksw.com/ontology/purchases#>
          prefix opllic:  <http://www.openlinksw.com/ontology/licenses#>
          prefix oplofr:  <http://www.openlinksw.com/ontology/offers#>
          prefix schema:  <http://schema.org/>
          prefix oplshop: <http://www.openlinksw.com/ontology/shop#>
          prefix owl:     <http://www.w3.org/2002/07/owl#>
          ask where {
            { graph <urn:openlinksw.com.shop:registry> { ?shop oplshop:hasPurchaseCache ?cg } }
            union
            { bind(<urn:openlinksw.com:shop:purchases:cache> as ?cg) }
            graph ?cg {
              { ^{uri}^ oplprchs:madePurchase ?purchase . }
              union
              { ?o owl:sameAs ^{uri}^ ; oplprchs:madePurchase ?purchase . }
              ?purchase oplprchs:contains ?offer ;
                        oplprchs:purchaseDate ?purchaseDate .
              ?offer a oplofr:{OfferIdentifier}Offer ;
                     schema:itemOffered ?license .
              ?license opllic:hasDuration ?duration .
              optional {?duration opllic:durationYears ?years .}
              filter ((?duration like <http://data.openlinksw.com/oplweb/license/License-Duration#ongoing-subscription> ) or
                      (bif:dateadd ('day', xsd:integer(?years) * 365, ?purchaseDate) > bif:now()) ) .
            }
          }"""
    ] .

<http://data.openlinksw.com/oplweb/acl/{OfferIdentifier}GraphAccess#this> a acl:Authorization ;
    schema:name "Rule to allow access to the <{graph IRI}> graph" ;
    acl:accessTo <{graph IRI}> ;
    oplacl:hasAccessMode oplacl:Read ;
    oplacl:hasRealm oplacl:DefaultRealm ;
    oplacl:hasScope oplacl:PrivateGraphs ;
    acl:agent <http://data.openlinksw.com/oplweb/group/{OfferIdentifier}GraphAccessUsers#this> ;
    wdrs:describedby source: .

Notes:

  • {graph IRI} in acl:accessTo MUST be the exact same IRI as the License's opllic:graphParameter — this is what actually ties the purchase to real access. The SHACL gate enforces this cross-link with a sh:sparql constraint on GraphAccessOfferShape: it fails if no acl:Authorization exists with acl:accessTo equal to the Offer's License's graphParameter and oplacl:hasAccessMode oplacl:Read.
  • The embedded SPARQL ASK query text inside oplacl:hasQuery is NOT gate-validated for content (it's a plain string literal, and string-matching against arbitrarily-formatted embedded SPARQL would be too fragile) — get the oplofr:{OfferIdentifier}Offer reference right by hand, matching the resource-specific type actually used on the Offer.
  • oplacl:ConditionalGroup (needs foaf:name and oplacl:hasCondition/oplacl:GroupCondition/oplacl:hasQuery) and acl:Authorization (needs acl:accessTo, oplacl:hasAccessMode oplacl:Read, oplacl:hasRealm oplacl:DefaultRealm, oplacl:hasScope oplacl:PrivateGraphs, acl:agent pointing at the group, wdrs:describedby) are both independently gate-enforced as structural shapes.
  • The prior-art reference file also includes an additional filter (sql:stripe_customer_has_active_product_subscription(...)) clause for Stripe-backed recurring subscriptions — include it when the offer is a recurring subscription (see Subscription Pricing above), matching the style of oplacl:demo-graph-access-world-cup-meshup's group condition.
  • This authorization block is currently required for Graph Access offers only (per explicit scope decision) — File Access and API Access offers don't need it yet.

API Access Product (WebAPI) Description Template + 4-Tier Offer Structure

Unlike File/Graph Access (one specific resource per offer), an API Access offer file describes ONE shared OPAL Chat API Product per host, sold as exactly 4 separate Offer/License/PriceSpecification triples — Entry (free), Medium ($9.99/mo), Advanced ($19.99/mo), Max ($49.99/mo) — all sharing the same Product. This matches the two dedicated prior-art files (OPALOffers-Licenses-Prices-ODS-QA-API.ttl, OPALOffers-Licenses-Prices-URIBurner-API.ttl). There is no user-supplied "resource identifier" the way File/Graph Access needs one — the tier names themselves (ApiAccessEntryLevel / ApiAccessMediumLevel / ApiAccessAdvancedLevel / ApiAccessMax) ARE the {OfferIdentifier} for each of the 4 offers. Product template (one per host, shared across all 4 tiers):

<http://data.openlinksw.com/oplweb/{host_short}OPAL-API#this> a schema:WebAPI ;
    schema:name "OPAL Chat API via {host_short}"@en ;
    schema:description """API endpoint for interacting with OPAL on {host_short}, enabling programmatic Machine-Centric Programming (MCP) chat sessions."""@en ;
    schema:serviceType "MCP-compliant AI Chat Agent API"@en ;
    schema:url <https://{host_hostname}/chat/api> ;
    skos:related <http://data.openlinksw.com/oplweb/{host_short}OPAL#this> ;
    schema:provider [
        a schema:Organization ;
        schema:name "OpenLink Software"@en ;
        schema:url <https://www.openlinksw.com/> ;
    ] ;
    schema:documentation <https://opal.openlinksw.com/install/> ;
    schema:exampleOfWork <https://www.openlinksw.com/data//html/opal-system-functions-or-agents-apis-developer-journey-gemini-17.html> ;
    schema:hasPart [
        a schema:EntryPoint ;
        schema:name "OPAL Chat Endpoint" ;
        schema:urlTemplate "https://{host_hostname}/chat/api" ;
        schema:httpMethod "POST" ;
        schema:contentType "application/json" ;
        schema:actionPlatform <http://schema.org/OnlinePlatform>
    ] ;
    schema:potentialAction [
        a schema:CommunicateAction ;
        schema:name "SendMessage" ;
        schema:target [
            a schema:EntryPoint ;
            schema:urlTemplate "https://{host_hostname}/chat/api" ;
            schema:httpMethod "POST"
        ] ;
        schema:expects [ a schema:Text ; schema:description "JSON payload containing message content and metadata for OPAL" ] ;
        schema:result [ a schema:Text ; schema:description "JSON response containing the OPAL chat reply or status" ]
    ] .

Each of the 4 tiers is its own Offer/License/PriceSpecification, e.g., for Entry:

<http://data.openlinksw.com/oplweb/offer/ApiAccessEntryLevelOffer{host_suffix}#this>
    a schema:Offer, oplofr:SubscriptionOffer, oplofr:ApiAccessEntryLevelOffer ;
    schema:itemOffered <http://data.openlinksw.com/oplweb/license/ApiAccessEntryLevelLicense{host_suffix}#this> ;
    schema:priceSpecification <http://data.openlinksw.com/oplweb/offer-unitprice/ApiAccessEntryLevelPriceSpecification#this> ;
    schema:url <https://{host_hostname}/chat/api> ;
    skos:prefLabel "OPAL API Access on {host_short} — Entry Level (BYOK)" ;
    ... (offerNumber, name, comment, description, businessFunction gr:Sell, isMemberOf OfferGroupApiAccess, validFrom/Through, potentialAction cart URL, wdrs:describedby — same as any other Offer) .

<http://data.openlinksw.com/oplweb/offer-unitprice/ApiAccessEntryLevelPriceSpecification#this>
    a schema:UnitPriceSpecification ;
    schema:priceCurrency "USD"^^xsd:string ;
    schema:price "0.00"^^xsd:decimal ;
    ... (validFrom/Through, wdrs:describedby — NO oplofr:interval/intervalCount, since Entry is free) .

<http://data.openlinksw.com/oplweb/license/ApiAccessEntryLevelLicense{host_suffix}#this>
    a opllic:ProductLicense, opllic:Product, opllic:ACLOnly, opllic:SubscriptionLicense ;
    opllic:productLicenseOf <http://data.openlinksw.com/oplweb/{host_short}OPAL-API#this> ;
    skos:related <https://{host_hostname}/oauth/applications.vsp> ;
    ... (name, description, prefLabel, image, partOf, hasDuration, OS/DB family, wdrs:describedby — same as any other License; NO opllic:uriParameter/graphParameter for API Access) .

The other 3 tiers (Medium $9.99, Advanced $19.99, Max $49.99) follow the identical structure with oplofr:interval "month"^^xsd:string + oplofr:intervalCount "1"^^xsd:integer added to their PriceSpecification (required for any non-$0.00 price — the gate enforces this pairing via ApiAccessPaidPriceShape's sh:or). Notes:

  • schema:image on the License: prefer <https://www.openlinksw.com/DAV/oplweb3/images/opal-license.png> (the API-Access-specific convention from both prior-art files) over the generic fallback image.
  • schema:documentation and schema:exampleOfWork are fixed, host-independent values (identical across both prior-art files) — the gate enforces them as exact matches.
  • skos:related is required on both the Product (→ the host's OPAL server Product, same as File/Graph) and the License (→ the host's OAuth applications page, https://{host_hostname}/oauth/applications.vsp) — these are two different things despite the same predicate.
  • Every Offer needs skos:prefLabel (gate-enforced for API Access specifically — this isn't required on Offers generally, only on Licenses).
  • The tier types (oplofr:ApiAccessEntryLevelOffer etc.) already satisfy the universal "resource-specific offer type" rule on their own — no separate generic marker class is needed (unlike File/Graph's DemoFileAccessOffer/DemoGraphAccessOffer + resource-specific-type combo).
  • Bug fixed while building this out: the pre-existing ApiAccessPaidPriceShape unconditionally required oplofr:interval/intervalCount on every API Access price, which would have wrongly rejected the free Entry tier. It's now conditional (sh:or: either price is exactly "0.00"^^xsd:decimal, or interval+intervalCount are present) — also fixed the interval value itself from a bare "month" to "month"^^xsd:string to match this skill's own established literal-typing convention (see priceCurrency rule below), which the bare form silently failed to match.
  • 4-tier completeness gate: ApiAccessProductShape includes a sh:sparql constraint requiring that, for the shared Product, all 4 tiers exist — a License with opllic:productLicenseOf this Product whose opllic:partOf-linked Offer is typed oplofr:ApiAccessEntryLevelOffer, another for oplofr:ApiAccessMediumLevelOffer, another for oplofr:ApiAccessAdvancedLevelOffer, another for oplofr:ApiAccessMaxOffer. Generating only some of the 4 tiers now fails the gate. Note on SHACL-SPARQL: VALUES clauses are disallowed inside sh:sparql constraints per the SHACL spec (confirmed via pyshacl: "A SPARQL Constraint must not contain a VALUES clause") — the check is written as a UNION of 4 separate FILTER NOT EXISTS branches instead. Checking by exact tier type (not price point) was a deliberate choice — the tier's identity is what matters, and prices are a business detail that could reasonably change later without affecting the invariant "all 4 tiers exist."

Chat Service Product/License/Offer Description Template

Unlike the other three types, a Chat Service offer is NOT tied to a specific file/graph/API-endpoint resource — it sells interactive use of the OPAL AI agent platform itself (module-specific assistants, or generic Bring-Your-Own-Key/system-wide tiers). There is no "License resource IRI" input for this type. Structurally it is the most open-ended of the four: unlike API Access's rigid single-Product/4-fixed-tiers bundle, Chat Service can have an arbitrary number of independent offer families (e.g., Data Twingler, Support Assistant, generic BYOK/Max), each typically at 4 tiers (Entry/Plus/Pro/Max), all sharing ONE Product per host. Product template (one per host, shared across every family/tier):

<http://data.openlinksw.com/oplweb/{host_short}OPAL#this> a schema:Service ;
    schema:name "OPAL via {host_short}"@en ;
    schema:applicationCategory "AI Agent Platform"@en ;
    schema:applicationSubCategory "LLM-Integrated AI Agent Middleware"@en ;
    schema:description """OPAL (OpenLink AI Layer) instance hosted by {host_short}, enabling AI Agent interaction with Data Spaces using natural language and the MCP protocol."""@en ;
    schema:softwareVersion "1.0"@en ;
    schema:provider [ a schema:Organization ; schema:name "OpenLink Software"@en ; schema:url <https://www.openlinksw.com/> ] ;
    schema:url <https://{host_hostname}/> ;
    schema:hasPart
        [ a schema:WebAPI ; schema:name "OPAL Chat Endpoint"@en ; schema:description "..."@en ; schema:serviceType "MCP-compliant AI Chat Agent"@en ; schema:url <https://{host_hostname}/chat> ; schema:documentation <https://opal.openlinksw.com/install/> ; schema:termsOfService <https://opal.openlinksw.com/tos/> ],
        [ a schema:WebAPI ; schema:name "OPAL Assist-Metal Endpoint"@en ; schema:description "..."@en ; schema:serviceType "MCP-compliant AI Assist Agent"@en ; schema:url <https://{host_hostname}/assist-metal> ; schema:documentation <https://opal.openlinksw.com/install/> ; schema:termsOfService <https://opal.openlinksw.com/tos/> ],
        [ a schema:WebAPI ; schema:name "MCP Server (Streamable HTTP)"@en ; schema:description "..."@en ; schema:serviceType "MCP Server (Streamable HTTP)"@en ; schema:url <https://{host_hostname}/chat/mcp/messages> ; schema:documentation <https://opal.openlinksw.com/install/> ; schema:termsOfService <https://opal.openlinksw.com/tos/> ],
        [ a schema:WebAPI ; schema:name "MCP Server (Server Sent Events)"@en ; schema:description "..."@en ; schema:serviceType "MCP Server (SSE)"@en ; schema:url <https://{host_hostname}/chat/mcp/sse> ; schema:documentation <https://opal.openlinksw.com/install/> ; schema:termsOfService <https://opal.openlinksw.com/tos/> ] .

This is the same shared {host_short}OPAL#this IRI already documented in references/offer-iri-patterns.md as the OPAL ACL server referenced via skos:related from File/Graph/API Access Products — here it plays its primary role, as the licensed Product itself. Each tier (of each family) is a standard Offer/License/PriceSpecification triple with oplofr:isMemberOf <http://data.openlinksw.com/oplweb/OfferGroupChatService#this>, opllic:productLicenseOf pointing at the shared {host_short}OPAL#this node, and its own resource-specific oplofr:{OfferIdentifier}Offer type (e.g., oplofr:DataTwinglerSpecificModuleEntryLevelOffer, oplofr:BringYourOwnKeyPlusOffer, oplofr:MaxOffer). Unlike API Access, the Offer does NOT carry schema:url directly (only the Product/its hasPart sub-resources do). Notes:

  • schema:hasPart requires (gate-enforced) all 4 named sub-endpoints present — Chat, Assist-Metal, MCP Streamable HTTP, MCP SSE — confirmed identical across both prior-art host files (only the URL host differs). Uses sh:qualifiedValueShape/sh:qualifiedMinCount (one clause per required serviceType) rather than a rigid hasPart count, so adding a 5th endpoint later won't break the shape.
  • schema:image on the License: same opal-license.png convention as API Access.
  • License does NOT need skos:related (unlike API Access's OAuth-applications-page requirement) — Chat Service licenses in prior art don't carry it.
  • Every Offer needs skos:prefLabel (same as API Access — not required on Offers generally).
  • No completeness gate for the multi-family structure (deliberately, this round): unlike API Access's fixed single bundle, Chat Service's open-ended family/tier structure doesn't map cleanly onto a fixed exclusion-list or fixed-class SPARQL check. If a future request wants "all 4 tiers of a given family must exist," that's a natural follow-up, matching how Graph Access's authorization block and API Access's completeness gate were each separately requested after their initial template work.
  • Bugs fixed in the underlying prior-art file while building this out (same due-diligence pass as the File/Graph/API Access work): one License (BringYourOwnKeyProLicenseOds-qa) was missing opllic:partOf entirely; all 12 offers' schema:potentialAction cart URLs were missing the /shop/ path segment (shop.openlinksw.com/cart.vsp instead of shop.openlinksw.com/shop/cart.vsp) — both fixed when regenerating from this template.
  • Validation note: scripts/validate-offers-shacl.py --type chatservice (or bare auto-detect) now validates against chat-service-offer-shape.ttl + common-offer-shape.ttl.

Auto-Derivation

  • name: First sentence ≤80 chars
  • pref_label: Abbreviated ≤60 chars
  • comment: "Purchasing this offer grants {description}"
  • description: The Offer node MUST also carry its own schema:description (distinct from schema:comment above) — required, e.g., "{Offer type} offer providing {description}." Omitting it is not just a SHACL gate failure: OPLSHOP.DBA.offers_sparql_base() in the live shop's opl_shop_offer_ui.sql requires schema:description as a non-optional triple in its main catalog listing query — an Offer without it is silently excluded from every offer listing in the shop, even though nothing else about the file is wrong.
  • itemOffered: schema:itemOffered on the Offer MUST point at the License IRI (the opllic:ProductLicense node), never at the Product (the schema:WebAPI node). The shop's detail-population queries (get_offer_info, get_offer_dbms_info, get_subscription_info, get_eval_action, get_service_uri in opl_shop_offer_ui.sql) all do ?offer schema:itemOffered ?lic and then read License-only properties off ?lic (schema:image, oplsof:hasOperatingSystemFamily, opllic:productLicenseOf, opllic:hasDuration, ...). Pointing itemOffered at the Product instead makes every one of those AJAX calls return empty, breaking the offer card's detail rendering even if the offer otherwise appears in the catalog list.
  • duration: If price is a recurring monthly price ("per month"), opllic:hasDuration MUST be the canonical <http://data.openlinksw.com/oplweb/license/License-Duration#ongoing-subscription> IRI (see references/offer-iri-patterns.md) — never invent/type a local Duration node.
  • duration (one-time purchase): If the price is a one-time, non-recurring charge, opllic:hasDuration MUST be the canonical <http://data.openlinksw.com/oplweb/license/License-Duration#perpetual> IRI — also never invent/type a local Duration node. Before using any Duration IRI not already listed in references/offer-iri-patterns.md, query the live store for existing opllic:Duration instances (SELECT ?d ?name WHERE { GRAPH ?g { ?d a opllic:Duration . OPTIONAL { ?d schema:name ?name } } }) rather than assuming one exists or minting a fresh one — added 2026-09-08 after confirming only #ongoing-subscription and #annual existed live at the time.
  • potentialAction: schema:potentialAction on the Offer MUST always be a direct "add to cart" IRI on the production shop, regardless of which host platform profile the licensed resource itself lives on (URIBurner, ODS-QA, Localhost — the cart is always shop.openlinksw.com): https://shop.openlinksw.com/shop/cart.vsp?command=add&item={percent-encoded Offer IRI}. Percent-encode the full Offer IRI (: → %3A, / → %2F, # → %23) as the item value. Never emit a blank-node schema:Action/EntryPoint structure — the shop's listing query reads schema:potentialAction as a flat triple and expects the value itself to be the clickable cart URL.
  • subscription pricing: Whenever the input price is phrased as "per <unit>" (per month, per year, ...) — i.e., it recurs on a billing cadence rather than being a one-time charge — the PriceSpecification MUST carry oplofr:interval "<unit>"^^xsd:string (e.g., "month") and oplofr:intervalCount "<n>"^^xsd:integer (e.g., 1), and the Offer MUST carry an additional rdf:type oplofr:SubscriptionOffer alongside schema:Offer and the offer-type-specific classes (oplofr:DAVOffer/oplofr:DemoFileAccessOffer etc.). One-time, non-recurring prices carry neither oplofr:interval nor the SubscriptionOffer type. The SHACL gate enforces the pairing both ways: a PriceSpecification with oplofr:interval but no oplofr:intervalCount fails, and an Offer whose price has oplofr:interval but isn't typed oplofr:SubscriptionOffer fails.
  • validity: Unless the user gives an explicit validity window, default schema:validFrom on both the Offer and its PriceSpecification to the file's creation date/time (the same instant used for the source: document's schema:datePublished), and default schema:validThrough to exactly 3 months after that. Keep the Offer's and the PriceSpecification's validity window identical to each other unless told otherwise.
  • image: The License MUST carry schema:image. If the user doesn't specify a particular image for this offer, default to <https://www.openlinksw.com/DAV/oplweb3/images/controlled-access-to-data-assets.jpg>.
  • license typing: Every License (File Access, Graph Access, or API Access — all three) MUST always be typed opllic:ProductLicense, opllic:Product, opllic:ACLOnly. opllic:SubscriptionLicense is a fourth, conditional type: include it if and only if the License's own Offer is typed oplofr:SubscriptionOffer (i.e., a genuinely recurring price — see the "subscription pricing" rule below). A one-time, non-recurring offer's License must NOT carry opllic:SubscriptionLicense. Bug found and fixed 2026-09-10: this rule previously said SubscriptionLicense was required "regardless of whether the price is one-time or recurring" — that was wrong, and it caused every one-time File Access offer generated under the old rule to be mistyped as a subscription license. That mistyping is also the confirmed root cause of a live ods-qa payment-completion bug: the shop's Stripe-SPT completion procedures branch on the License's rdf:type to decide which settlement path to use, so a one-time License wrongly carrying SubscriptionLicense gets routed into subscription-only settlement logic and fails to complete. The SHACL gate now enforces the corrected, conditional rule via a sh:sparql constraint on BaseLicenseShape comparing the Offer's oplofr:SubscriptionOffer typing against the License's opllic:SubscriptionLicense typing — the two must always agree.
  • priceCurrency literal form: schema:priceCurrency MUST always be written with an explicit ^^xsd:string datatype annotation — schema:priceCurrency "USD"^^xsd:string ; — never the bare/plain form (schema:priceCurrency "USD" ;). This is a serialization convention only, not something the SHACL gate can check: a plain string and an explicitly-typed "USD"^^xsd:string are the same RDF term under RDF 1.1 simple-literal semantics, so sh:datatype xsd:string already accepts both and can't tell them apart. Get it right at generation time.
  • schema.org prefix name: the bound prefix for http://schema.org/ MUST be schema: — never schema1:. Found 2026-09-08: the two live prior-art reference files (whatever process originally wrote them) use schema1:, an artifact of their own serialization, not a naming convention to imitate — SKILL.md's own templates above have always used schema:. Regenerating or editing an offer file that currently has @prefix schema1: should rename it to schema: throughout (a safe global find/replace, since schema1 never appears as a substring of anything else in these files).
  • language tags on prose literals: every natural-language string value on schema:name, schema:description, schema:comment, skos:prefLabel, skos:altLabel, foaf:name, and cc:attributionName MUST carry @en — e.g., schema:name "Food Bookmark Collection (RDF-Turtle) — File Access on URIBurner"@en. This applies everywhere those predicates appear with free-text prose: on the Offer, License, PriceSpecification, the shared Product/WebAPI blocks (already @en-tagged in prior art — keep as is), acl:Authorization's schema:name, and oplacl:ConditionalGroup's foaf:name. Two required exceptions, both because the SHACL gate pins the datatype: the source: document's own schema:name (DocumentShape requires sh:datatype xsd:string) and the opllic:Duration node's schema:name (DurationShape requires the same) must stay untagged plain/xsd:string literals — adding @en there gives the literal rdf:langString datatype instead of xsd:string and fails the gate. Never add a language tag to schema:price (xsd:decimal), schema:priceCurrency/schema:unitCode/oplofr:interval/oplofr:offerNumber (xsd:string codes, not prose), oplofr:intervalCount (xsd:integer), or any xsd:dateTime value — language tags belong on natural-language text, not codes, numbers, or dates.
  • resource-specific offer type: Every Offer (all three offer types) MUST carry an additional rdf:type in the oplofr: namespace named oplofr:{OfferIdentifier}Offer — specific to the resource being sold, not to the host serving it. {OfferIdentifier} is the exact same PascalCase identifier used in the Offer/License/PriceSpecification IRIs (see Entity IRI Naming Convention) — no {host_suffix}. Example: for the Offer at .../offer/JchTestGraphOfferOds-qa#this (selling access to urn:jch:test), add oplofr:JchTestGraphOffer alongside schema:Offer, oplofr:DemoGraphAccessOffer, oplofr:SubscriptionOffer. The SHACL gate enforces this on BaseOfferShape via a qualified value shape: at least one rdf:type value must be an oplofr: IRI ending in Offer that is NOT one of the known host-generic classes (oplofr:DAVOffer, oplofr:DemoFileAccessOffer, oplofr:DemoGraphAccessOffer, oplofr:SubscriptionOffer) — if a new host-generic offer class is ever added (e.g., for API Access), add it to that exclusion list in shacl/common-offer-shape.ttl's BaseOfferShape, or a real resource-specific offer would incorrectly satisfy the check by accident, or worse, a missing resource-specific type could go undetected.
  • offer-level product family: Every Offer (all four offer types) MUST carry its own skos:related — separate from, and in addition to, any skos:related already required on the Product/License — whose object is a blank node with exactly one oplpro:hasFamily triple pointing at a fixed product-family IRI, chosen by which oplofr:isMemberOf OfferGroup the offer belongs to: OfferGroupFileAccess or OfferGroupGraphAccess → skos:related [ oplpro:hasFamily <http://data.openlinksw.com/oplweb/product_family/loac#this> ]; OfferGroupApiAccess or OfferGroupChatService → skos:related [ oplpro:hasFamily <http://data.openlinksw.com/oplweb/product_family/opal#this> ]. This is confirmed by every current prior-art file across all four offer types. The SHACL gate enforces it per type on each *OfferShape with sh:nodeKind sh:BlankNode (a named-IRI object, the shape otherwise used for Product/License skos:related, now fails here) plus a nested sh:hasValue check on oplpro:hasFamily for the type-appropriate family IRI.

GATE: 0 FAIL

python3 scripts/validate-offers-shacl.py output.ttl --type {file|graph|api|chatservice} — must pass before delivery.

Server-Side Generation (ProtectedOffer DET)

Virtuoso's ProtectedOffer DAV DET applies the File Access contract automatically on upload, then notifies the instance admin so the offer can be loaded into each shop. See references/det-integration.md. Known gap: the three prompts/*-offer-prompt.md files are empty (0 bytes). Until they are written, SKILL.md and shacl/ are the operative contract.

Loading into Shop

SPARQL define get:soft "no-sponge" LOAD <file:///path/to/output.ttl> INTO <urn:opl:shop:offering:sponging:cache:official> ;

License

AGPL-3.0

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.