allium

Give your AI agents something more useful than a prompt. Velocity through clarity.

INSTALLATION
npx skills add https://github.com/juxt/allium --skill allium
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

$27

Task

Tool

When

Writing or reading .allium files

this skill

You need language syntax and structure

Building a spec through conversation

elicit skill

User describes a feature or behaviour they want to build

Extracting a spec from existing code

distill skill

User has implementation code and wants a spec from it

Modifying an existing spec

tend skill

User wants targeted changes to .allium files

Checking spec-to-code alignment

weed skill

User wants to find or fix divergences between spec and implementation

Generating tests from a spec

propagate skill

User wants to generate tests, PBT properties or state machine tests from a specification

Quick syntax summary

Entity

entity Candidacy {

    -- Fields

    candidate: Candidate

    role: Role

    status: pending | active | completed | cancelled   -- inline enum

    retry_count: Integer

    -- Relationships

    invitation: Invitation with candidacy = this         -- one-to-one

    slots: InterviewSlot with candidacy = this           -- one-to-many

    -- Projections

    confirmed_slots: slots where status = confirmed

    pending_slots: slots where status = pending

    -- Derived

    is_ready: confirmed_slots.count >= 3

    has_expired: invitation.expires_at <= now

}

External entity

external entity Role { title: String, required_skills: Set<Skill>, location: Location }

Value type

value TimeRange { start: Timestamp, end: Timestamp, duration: end - start }

Sum type

A base entity declares a discriminator field whose capitalised values name the variants. Variants use the variant keyword.

entity Node {

    path: Path

    kind: Branch | Leaf              -- discriminator field

}

variant Branch : Node {

    children: List<Node?>

}

variant Leaf : Node {

    data: List<Integer>

    log: List<Integer>

}

Lowercase pipe values are enum literals (status: pending | active). Capitalised values are variant references (kind: Branch | Leaf). Type guards (requires: or if branches) narrow to a variant and unlock its fields.

Module given

Declares the entity instances a module's rules operate on. All rules inherit these bindings. Not every module needs one: rules scoped by triggers on domain entities get their entities from the trigger. given is for specs where rules operate on shared instances that exist once per module scope.

given {

    pipeline: HiringPipeline

    calendar: InterviewCalendar

}

Imported module instances are accessed via qualified names (scheduling/calendar) and do not appear in the local given block. Distinct from surface context, which binds a parametric scope for a boundary contract.

Rule

rule InvitationExpires {

    when: invitation: Invitation.expires_at <= now

    requires: invitation.status = pending

    let remaining = invitation.proposed_slots where status != cancelled

    ensures: invitation.status = expired

    ensures:

        for s in remaining:

            s.status = cancelled

    @guidance

        -- Non-normative implementation advice.

}

Trigger types

  • External stimulus: when: CandidateSelectsSlot(invitation, slot) — action from outside the system
  • State transition: when: interview: Interview.status transitions_to scheduled — entity changed state (transition only, not creation)
  • State becomes: when: interview: Interview.status becomes scheduled — entity has this value, whether by creation or transition
  • Temporal: when: invitation: Invitation.expires_at <= now — time-based condition (always add a requires guard against re-firing)
  • Derived condition: when: interview: Interview.all_feedback_in — derived value becomes true
  • Entity creation: when: batch: DigestBatch.created — fires when a new entity is created
  • Chained: when: AllConfirmationsResolved(candidacy) — subscribes to a trigger emission from another rule's ensures clause

All entity-scoped triggers use explicit var: Type binding. Use _ as a discard binding where the name is not needed: when: _: Invitation.expires_at <= now, when: SomeEvent(_, slot).

Rule-level iteration

A for clause applies the rule body once per element in a collection:

rule ProcessDigests {

    when: schedule: DigestSchedule.next_run_at <= now

    for user in Users where notification_setting.digest_enabled:

        let settings = user.notification_setting

        ensures: DigestBatch.created(user: user, ...)

}

Ensures patterns

Ensures clauses have four outcome forms:

  • State changes: entity.field = value
  • Entity creation: Entity.created(...) — the single canonical creation verb
  • Trigger emission: TriggerName(params) — emits an event for other rules to chain from
  • Entity removal: not exists entity — asserts the entity no longer exists

These forms compose with for iteration (for x in collection: ...), if/else conditionals and let bindings.

Entity creation uses .created() exclusively. Domain meaning lives in entity names and rule names, not in creation verbs.

In state change assignments, the right-hand expression references pre-rule field values. Conditions within ensures blocks (if guards, creation parameters, trigger emission parameters) reference the resulting state.

Surface

surface InterviewerDashboard {

    facing viewer: Interviewer

    context assignment: SlotConfirmation where interviewer = viewer

    exposes:

        assignment.slot.time

        assignment.status

    provides:

        InterviewerConfirmsSlot(viewer, assignment.slot)

            when assignment.status = pending

    related:

        InterviewDetail(assignment.slot.interview)

            when assignment.slot.interview != null

}

Surfaces define contracts at boundaries. The facing clause names the external party, context scopes the entity. The remaining clauses use a single vocabulary regardless of whether the boundary is user-facing or code-to-code: exposes (visible data, supports for iteration over collections), provides (available operations with optional when-guards), contracts: (references module-level contract declarations with demands/fulfils direction markers), @guarantee (named prose assertions about the boundary), @guidance (non-normative advice), related (associated surfaces reachable from this one), timeout (references to temporal rules that apply within the surface's context).

The facing clause accepts either an actor type (with a corresponding actor declaration and identified_by mapping) or an entity type directly. Use actor declarations when the boundary has specific identity requirements; use entity types when any instance can interact (e.g., facing visitor: User). For integration surfaces where the external party is code, declare an actor type with a minimal identified_by expression. Actors that reference within in their identified_by expression must declare the expected context type: within: Workspace.

Surface-to-implementation contract

The exposes block is the field-level contract: the implementation returns exactly these fields, the consumer uses exactly these fields. Do not add fields not listed. Do not omit fields that are listed.

Contract

contract Codec {

    serialize: (value: Any) -> ByteArray

    deserialize: (bytes: ByteArray) -> Any

    @invariant Roundtrip

        -- deserialize(serialize(value)) produces a value

        -- equivalent to the original for all supported types.

}

Contracts are module-level declarations referenced by name in surface contracts: clauses (demands Codec, fulfils EventSubmitter). See Contracts for declaration syntax and referencing rules.

Expressions

Navigation: interview.candidacy.candidate.email, reply_to?.author (optional), timezone ?? "UTC" (null coalescing). Collections: slots.count, slot in invitation.slots, interviewers.any(i => i.can_solo), for item in collection: item.status = cancelled, permissions + inherited (set union), old - new (set difference). Comparisons: status = pending, count >= 2, status in {confirmed, declined}, provider not in providers. Boolean logic: a and b, a or b, not a, a implies b.

Modular specs

use "github.com/allium-specs/google-oauth/abc123def" as oauth

Qualified names reference entities across specs: oauth/Session. Coordinates are immutable (git SHAs or content hashes). Local specs use relative paths: use "./candidacy.allium" as candidacy.

Config

config {

    invitation_expiry: Duration = 7.days

    max_login_attempts: Integer = 5

    extended_expiry: Duration = invitation_expiry * 2              -- expression-form default

    sync_timeout: Duration = core/config.default_timeout           -- config parameter reference

}

Rules reference config values as config.invitation_expiry. For default entity instances, use default.

Defaults

default Role viewer = { name: "viewer", permissions: { "documents.read" } }

Invariant

invariant NonNegativeBalance {

    for account in Accounts:

        account.balance >= 0

}

Expression-bearing invariants (invariant Name { expression }) assert properties over entity state. They are logical assertions, not runtime checks. Distinct from prose annotations (@invariant Name) in contracts, which use the @ sigil to mark content the checker does not evaluate. See Invariants.

Transition graph (v3)

entity Order {

    status: pending | confirmed | shipped | delivered | cancelled

    transitions status {

        pending -> confirmed

        confirmed -> shipped

        shipped -> delivered

        pending -> cancelled

        confirmed -> cancelled

        terminal: delivered, cancelled

    }

}

State-dependent field presence (v3)

entity Order {

    status: pending | confirmed | shipped | delivered | cancelled

    customer: Customer

    total: Money

    tracking_number: String when status = shipped | delivered

    shipped_at: Timestamp when status = shipped | delivered

    transitions status {

        pending -> confirmed

        confirmed -> shipped

        shipped -> delivered

        pending -> cancelled

        confirmed -> cancelled

        terminal: delivered, cancelled

    }

}

Deferred specs

deferred InterviewerMatching.suggest    -- see: detailed/interviewer-matching.allium

Open questions

open question "Admin ownership - should admins be assigned to specific roles?"

Verification

When the allium CLI is installed, a hook validates .allium files automatically after every write or edit. Fix any reported issues before presenting the result. If the CLI is not available, verify against the language reference.

References

  • Language reference — full syntax for entities, rules, expressions, surfaces, contracts, invariants and validation
  • Patterns — 9 worked patterns: auth, RBAC, invitations, soft delete, notifications, usage limits, comments, library spec integration, framework integration contract
BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card