Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
objectstack-ai avatar

Objectstack Formula

  • 129 installs
  • 18 repo stars
  • Updated August 5, 2026
  • objectstack-ai/framework

Helps with ai & agent building tasks.

About

objectstack-formula is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.

  • objectstack-formula
  • AI & Agent Building
  • AI-coding skill

Objectstack Formula by the numbers

  • 129 all-time installs (skills.sh)
  • +4 installs in the week ending Aug 4, 2026 (Skillselion tracking)
  • Ranked #3,702 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
  • Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/objectstack-ai/framework --skill objectstack-formula

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs129
repo stars18
Last updatedAugust 5, 2026
Repositoryobjectstack-ai/framework

What it does

Helps with ai & agent building tasks.

Files

SKILL.mdMarkdownGitHub ↗

Expressions (CEL) — ObjectStack Formula Protocol

ObjectStack has one expression language across every domain that needs computation or boolean predicates: CEL (Google Common Expression Language). This skill is the canonical reference for AI authors emitting formula / condition / predicate / dynamic-seed metadata.

Strategic context. The future authors of metadata are AI agents.
CEL was chosen because it has (a) a formal grammar, (b) a public training
corpus, (c) AST-first persistence, and (d) sandboxed bounded execution.
The previous custom Salesforce-flavor engine was deleted in M9.5.

>

**Predicates / formulas are bare CEL — never wrap field references in {…}
braces.** The #1 authoring mistake (root cause of #1491) is a condition like
{record.rating} >= 4: in CEL, {…} is a map literal, so it is a parse
error. Write bare CEL: record.rating >= 4. Braces are only for {{ … }}
text templates (see Template surfaces).

>

As of 7.6 (ADR-0032) a malformed expression no longer fails silently.
It used to evaluate to null/false (a flow "fired" but did nothing). Now
objectstack build fails with a located, corrective, schema-aware message
(unknown record.<field> → did-you-mean), and at runtime the engine throws
(the flow/rule fails loudly). The validate_expression agent tool runs the
same shared validator so you can check an expression before saving.

---

Skill Boundaries

NeedUse instead
Define a type: 'formula' fieldobjectstack-data (and embed CEL via F\...\``)
Define seed recordsobjectstack-data (use cel\...\`` for dynamic dates)
Author flow / automation stepobjectstack-automation (use P\...\` for condition`)
Author L2 hook body (TS code)objectstack-data
Cron scheduleobjectstack-automation (schedule.expression is cron dialect)
SQL fragmentdriver-native; not unified into the expression registry

---

Core contract

Every expression in metadata is the same envelope:

type Expression = {
  dialect: 'cel' | 'js' | 'cron' | 'template';
  source?: string;
  ast?: unknown;
  meta?: { rationale?: string; generatedBy?: string };
};

Four registered dialects (M9.9):

DialectEnginePurposeHelperExample
cel@marcbachmann/cel-jsComputed values + boolean predicates` cel... / F... / P... `` celrecord.amount * 1.1 `
cronbuilt-in validatorRecurring schedules` cron... `` cron0 6 MON `
templatebuilt-in interpolator{{path}} text interpolation (notif/prompt/title)` tmpl... `` tmplHello {{record.first_name}} `
js(sandboxed, future)Edge cases needing arbitrary JS — avoidn/areserved

Authors emit the right dialect for the surface. Bare strings on cron and template fields are auto-wrapped at validate time, but emitting the full envelope is preferred for clarity. cron and template use the same variable scope as CEL — you do not learn three languages.

AI authors: when emitting structured-output JSON for metadata, always
emit the full envelope { dialect, source } — do not emit bare strings.
After M9.7 lands, you will emit ast directly. Until then, emit source
and let objectstack compile parse it.

---

CEL syntax cheat-sheet

ConceptCEL
Current record fieldrecord.first_name
Previous record (update hooks)previous.status
Hook input payloadinput.amount
Identity contextos.user.id, os.org.slug, os.env
Equality== / !=
Logical&& / `\
Ternarycond ? a : b
String literal'single quotes' (always)
Membershiprecord.region in ['us', 'eu']
Key existence (NOT null-safety)has(record.foo)
Null checkrecord.foo == null or isBlank(record.foo)

has() is NOT a null check

has(record.x) is true whenever the key exists, even when its value is null. To check for "value present and non-blank" use the stdlib helper isBlank() or compare to null explicitly.

Null + string throws

CEL has no implicit null coercion. null + 'foo' throws no such overload: dyn<null> + string. Wrap every nullable string operand in coalesce(..., '').

---

ObjectStack CEL standard library

Registered automatically. Source: `packages/formula/src/stdlib.ts`.

The canonical list is CEL_STDLIB_FUNCTIONS in `packages/formula/src/validate.ts` — a test asserts every entry resolves at runtime, so this table stays in sync with it.

Dates

FunctionReturnsNotes
now()timestampCurrent instant. Pinned per evaluation run; deterministic in build
today()timestampUTC start-of-day (midnight)
daysFromNow(n)timestampnow() + n days — keeps the current time-of-day (NOT midnight)
daysAgo(n)timestampnow()n days — keeps the current time-of-day
daysBetween(a, b)intWhole days from a to b (negative if b precedes a). daysBetween(today(), record.due) = days remaining
addDays(d, n)timestampShift any date by n days (negative ok). addDays(record.last_service, record.cycle_days) = next due date
addMonths(d, n)timestampShift any date by n months; clamps to month-end (addMonths(date('2026-01-31'), 1) → Feb 28)
date(s) / datetime(s)timestampParse an ISO date / date-time string to a timestamp

Numbers

FunctionReturnsNotes
abs(x)doubleAbsolute value
round(x)intRound to the nearest integer
min(a, b) / max(a, b)dynSmaller / larger operand (numeric comparison)

Strings

FunctionReturnsNotes
upper(s) / lower(s)stringCase conversion
trim(s)stringStrip surrounding whitespace ('' for null)
contains(s, sub)boolSubstring test
startsWith(s, p) / endsWith(s, p)boolPrefix / suffix test
matches(s, re)boolRegex test
joinNonEmpty(list, sep)stringJoin, dropping null/empty entries

Collections / null-ish

FunctionReturnsNotes
isBlank(v)booltrue for null, undefined, '', []
isEmpty(v)booltrue for null, undefined, empty string / list / map
coalesce(v, fallback)dynv when non-null, else fallback
len(v)intLength of a string / list / map

Plus CEL built-ins: has(x), size(x), int(x), string(x), bool(x), double(x), timestamp(s), duration(s).

If you need a helper that doesn't exist, prefer adding it to the stdlib (small, pure, dependency-free) over inlining a complex CEL expression.

Only the functions above are callable. An UNKNOWN function — PRIOR(), a
legacy ISBLANK(), a typo'd isBlnk()fails `objectstack build` with a
"no matching overload" type error (#1877), rather than silently no-op'ing the
predicate at run time. Use previous.x (not PRIOR()), isBlank() (not ISBLANK()).

---

Mandatory patterns for AI emission

1. Computed text formula — always coalesce nullable operands

Correct

F`coalesce(record.salutation, '') + ' '
  + coalesce(record.first_name, '') + ' '
  + coalesce(record.last_name, '')`

Wrong (CEL throws on null + string)

F`record.salutation + ' ' + record.first_name + ' ' + record.last_name`

2. Conditional numeric formula — guard divisor

F`coalesce(record.cost, 0) > 0
  ? ((coalesce(record.revenue, 0) - record.cost) * 100.0) / record.cost
  : 0.0`

3. Predicate (field rules / visibility / validation)

P`record.status == 'qualified'`
P`record.amount > 10000 && record.region in ['us', 'eu']`
P`!isBlank(record.po_number)`

For field-level conditional rules, emit the canonical field properties: visibleWhen, readonlyWhen, and requiredWhen. Treat conditionalRequired as a read/compatibility alias only.

❌ Salesforce-flavor — will compile but evaluate to null:

"status = 'qualified'"
"amount > 10000 AND region IN ('us', 'eu')"
"NOT(ISBLANK(po_number))"

4. Dynamic seed value — use cel\\` not new Date()`

{ close_date: cel`daysFromNow(45)`, created_at: cel`now()` }

❌ Compile-time evaluation — every customer gets the package author's clock:

{ close_date: new Date(Date.now() + 45 * 86400000), created_at: new Date() }

This is the determinism gate: objectstack build runs twice produce byte-identical dist/objectstack.json only when seed dates use CEL.

5. Update hook condition — previous vs record

P`previous.status != 'escalated' && record.status == 'escalated'`

ISCHANGED-style logic does not exist as a function; use explicit previous comparison.

---

Mechanical translation table (legacy → CEL)

When migrating Salesforce-flavor metadata, apply these rules in order:

LegacyCEL
bare_fieldrecord.bare_field
OLD.xprevious.x
NEW.xrecord.x
= (comparison)==
<>!=
AND&&
OR`\
NOT(x)!x
"abc"'abc'
IF(c, a, b)c ? a : b
ISBLANK(x)isBlank(record.x)
CONCAT(a, b)coalesce(a, '') + coalesce(b, '')
TODAY() / NOW()today() / now()
IN (a, b, c)in [a, b, c]
ISCHANGED(x)previous.x != record.x
MONTH_DIFF, MID, LEFT, RIGHT, SUBSTITUTE_not in stdlib — propose addition_

---

Surfaces that take an Expression

All of these spec fields accept string | Expression. The build normalizes to the envelope.

CEL surfaces (predicates + computed values)

SurfaceFieldDialect
Fieldformula (when type: 'formula')cel
FieldvisibleWhen / readonlyWhen / requiredWhencel
FieldconditionalRequired (deprecated alias of requiredWhen)cel
FieldvisibleOncel
FielddefaultValue (M9.9b)cel
ConditionalValidationwhencel
ObjectFieldGroupvisibleOncel
ViewvisibleOncel
View.criteriafilter expressioncel
Actiondisabledcel (or boolean)
Hookconditioncel
SharingRuleconditioncel
Flow.decisionexpression / edge conditioncel (use vars.<step>.<key>)
Workflow.TaskdueDatecel (e.g. cel\daysFromNow(3)\``)
Workflowcriteriacel
GraphQL.ComputedFieldexpressioncel
Dataset.records[*]any valuecel (via cel\\``)
audit / metrics / tracingcondition / successCriteriastructured \

Cron surfaces (recurring schedules)

All accept bare strings (auto-wrapped to {dialect:'cron', source}) or the ` cron... helper. 5- or 6-field cron + aliases (@daily, @hourly`, …).

SurfaceField
Job.schedule.expressioncanonical
connector.schedule, etl.schedule, sync.schedulepipelines
system/cache.schedulewarmup
system/disaster-recovery.schedulebackup + drill
automation/execution.cronExpressionscheduled state
api/export.cronExpressionscheduled exports (×2)
ai/orchestration.cronrecurring runs
ai/devops-agent.iterationFrequencyiteration cadence

Template surfaces ({{ path }} interpolation)

Mustache subset — a field/variable path plus an optional whitelisted formatter: {{ path }} or {{ path | formatter[:arg] }}. No conditionals, no arbitrary logic (move logic into a CEL field). Same variable scope as CEL. Double braces only — single {x} is not a valid hole.

Formatters (7.6) — value→string is defined per formatter (not implicit):

FormatterExampleOutput
currency[:CODE]`{{ record.amount \currency }} / :EUR`
number[:decimals]`{{ record.n \number:2 }}`
percent[:decimals]`{{ record.rate \percent }}` (0.42→)
`date[:short\long\iso] / datetime[:…]`
upper / lower / trim`{{ record.code \upper }}`
truncate:N`{{ record.body \truncate:80 }}`
default:'…'`{{ record.x \default:'N/A' }}`
json`{{ record.obj \json }}`
tmpl`Deal {{ record.name }} — {{ record.amount | currency }} closes {{ record.close_date | date:long }}`
SurfaceField
Object.titleFormatrecord title
system/notificationemail subject + body, SMS message, push body + message (5 fields)
ai/model-registrysystemPrompt, userPromptTemplate
ai/agent-actionsubject, message
ai/nlq.systemPrompt, ai/mcp.systemPromptprompt templates
integration/connector/githubtitleTemplate, bodyTemplate (PR + release)
api/graphqlcache key

JS surface (sandboxed body)

Reserved for L2 hook bodies / mapping transforms. Use TypeScript source.

---

Cron quick reference

import { cron } from '@objectstack/spec';

schedule: cron`0 6 * * MON`        // every Monday at 06:00
schedule: cron`@daily`             // alias — every midnight
schedule: cron`*/15 * * * *`       // every 15 minutes

Bare strings work too on cron-typed fields, but the cron helper makes intent explicit.

---

Template quick reference

import { tmpl } from '@objectstack/spec';

titleFormat: tmpl`{{record.first_name}} {{record.last_name}}`
subject:     tmpl`Welcome to {{os.org.name}}, {{os.user.name}}!`

Missing paths render as empty string. Date instances are ISO-formatted.

---

Determinism contract

Builds are deterministic only if:

1. All seed dynamic values use cel\...\` (no new Date(), no Date.now()). 2. CEL stdlib helpers honor the pinned now from EvalContext`. 3. No expression source contains random / non-pure data.

CI runs objectstack build twice and asserts SHA-1 match.

---

Open questions (track in ROADMAP M9.7+)

  • Authors will emit ast directly once CelExprSchema is published as JSON

Schema for AI constrained decoding (M9.7).

  • A visual node-graph editor backed by CelExprSchema is M9.8 (Studio).

---

Verify your work

A malformed expression no longer fails silently (ADR-0032, see the note near the top of this skill): both os validate and os build run the shared validator over every formula and predicate in the stack — CEL syntax plus record.<field> existence on the target object — and fail non-zero with a did-you-mean. Use os validate as the fast post-edit check (no artifact emitted; npm run validate in a scaffolded project). To check a single expression before saving it, call the validate_expression agent tool, which runs the same validator inline.

---

See also

  • `content/docs/guides/formula.mdx` — human-facing guide
  • `packages/formula/` — engine + stdlib
  • `packages/spec/src/shared/expression.zod.ts`Expression, ExpressionInput, cel / F / P
  • ROADMAP M9 — Expression Unification milestone
  • north-star §8 — "No private expression DSL"

Related skills

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.