
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-formulaAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 129 |
|---|---|
| repo stars | ★ 18 |
| Last updated | August 5, 2026 |
| Repository | objectstack-ai/framework ↗ |
What it does
Helps with ai & agent building tasks.
Files
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 tonull/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 thesame shared validator so you can check an expression before saving.
---
Skill Boundaries
| Need | Use instead |
|---|---|
Define a type: 'formula' field | objectstack-data (and embed CEL via F\...\``) |
| Define seed records | objectstack-data (use cel\...\`` for dynamic dates) |
| Author flow / automation step | objectstack-automation (use P\...\` for condition`) |
| Author L2 hook body (TS code) | objectstack-data |
| Cron schedule | objectstack-automation (schedule.expression is cron dialect) |
| SQL fragment | driver-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):
| Dialect | Engine | Purpose | Helper | Example |
|---|---|---|---|---|
cel | @marcbachmann/cel-js | Computed values + boolean predicates | ` cel... / F... / P... ` | ` celrecord.amount * 1.1 ` |
cron | built-in validator | Recurring schedules | ` cron... ` | ` cron0 6 MON ` |
template | built-in interpolator | {{path}} text interpolation (notif/prompt/title) | ` tmpl... ` | ` tmplHello {{record.first_name}} ` |
js | (sandboxed, future) | Edge cases needing arbitrary JS — avoid | n/a | reserved |
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 emitastdirectly. Until then, emitsource
and let objectstack compile parse it.---
CEL syntax cheat-sheet
| Concept | CEL |
|---|---|
| Current record field | record.first_name |
| Previous record (update hooks) | previous.status |
| Hook input payload | input.amount |
| Identity context | os.user.id, os.org.slug, os.env |
| Equality | == / != |
| Logical | && / `\ |
| Ternary | cond ? a : b |
| String literal | 'single quotes' (always) |
| Membership | record.region in ['us', 'eu'] |
| Key existence (NOT null-safety) | has(record.foo) |
| Null check | record.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
| Function | Returns | Notes |
|---|---|---|
now() | timestamp | Current instant. Pinned per evaluation run; deterministic in build |
today() | timestamp | UTC start-of-day (midnight) |
daysFromNow(n) | timestamp | now() + n days — keeps the current time-of-day (NOT midnight) |
daysAgo(n) | timestamp | now() − n days — keeps the current time-of-day |
daysBetween(a, b) | int | Whole days from a to b (negative if b precedes a). daysBetween(today(), record.due) = days remaining |
addDays(d, n) | timestamp | Shift any date by n days (negative ok). addDays(record.last_service, record.cycle_days) = next due date |
addMonths(d, n) | timestamp | Shift any date by n months; clamps to month-end (addMonths(date('2026-01-31'), 1) → Feb 28) |
date(s) / datetime(s) | timestamp | Parse an ISO date / date-time string to a timestamp |
Numbers
| Function | Returns | Notes |
|---|---|---|
abs(x) | double | Absolute value |
round(x) | int | Round to the nearest integer |
min(a, b) / max(a, b) | dyn | Smaller / larger operand (numeric comparison) |
Strings
| Function | Returns | Notes |
|---|---|---|
upper(s) / lower(s) | string | Case conversion |
trim(s) | string | Strip surrounding whitespace ('' for null) |
contains(s, sub) | bool | Substring test |
startsWith(s, p) / endsWith(s, p) | bool | Prefix / suffix test |
matches(s, re) | bool | Regex test |
joinNonEmpty(list, sep) | string | Join, dropping null/empty entries |
Collections / null-ish
| Function | Returns | Notes |
|---|---|---|
isBlank(v) | bool | true for null, undefined, '', [] |
isEmpty(v) | bool | true for null, undefined, empty string / list / map |
coalesce(v, fallback) | dyn | v when non-null, else fallback |
len(v) | int | Length 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(), alegacyISBLANK(), a typo'disBlnk()— fails `objectstack build` with a
"no matching overload" type error (#1877), rather than silently no-op'ing the
predicate at run time. Useprevious.x(notPRIOR()),isBlank()(notISBLANK()).
---
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:
| Legacy | CEL |
|---|---|
bare_field | record.bare_field |
OLD.x | previous.x |
NEW.x | record.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)
| Surface | Field | Dialect |
|---|---|---|
Field | formula (when type: 'formula') | cel |
Field | visibleWhen / readonlyWhen / requiredWhen | cel |
Field | conditionalRequired (deprecated alias of requiredWhen) | cel |
Field | visibleOn | cel |
Field | defaultValue (M9.9b) | cel |
ConditionalValidation | when | cel |
ObjectFieldGroup | visibleOn | cel |
View | visibleOn | cel |
View.criteria | filter expression | cel |
Action | disabled | cel (or boolean) |
Hook | condition | cel |
SharingRule | condition | cel |
Flow.decision | expression / edge condition | cel (use vars.<step>.<key>) |
Workflow.Task | dueDate | cel (e.g. cel\daysFromNow(3)\``) |
Workflow | criteria | cel |
GraphQL.ComputedField | expression | cel |
Dataset.records[*] | any value | cel (via cel\\``) |
audit / metrics / tracing | condition / successCriteria | structured \ |
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`, …).
| Surface | Field |
|---|---|
Job.schedule.expression | canonical |
connector.schedule, etl.schedule, sync.schedule | pipelines |
system/cache.schedule | warmup |
system/disaster-recovery.schedule | backup + drill |
automation/execution.cronExpression | scheduled state |
api/export.cronExpression | scheduled exports (×2) |
ai/orchestration.cron | recurring runs |
ai/devops-agent.iterationFrequency | iteration 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):
| Formatter | Example | Output |
|---|---|---|
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 }}`| Surface | Field |
|---|---|
Object.titleFormat | record title |
system/notification | email subject + body, SMS message, push body + message (5 fields) |
ai/model-registry | systemPrompt, userPromptTemplate |
ai/agent-action | subject, message |
ai/nlq.systemPrompt, ai/mcp.systemPrompt | prompt templates |
integration/connector/github | titleTemplate, bodyTemplate (PR + release) |
api/graphql | cache 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 minutesBare 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
astdirectly onceCelExprSchemais published as JSON
Schema for AI constrained decoding (M9.7).
- A visual node-graph editor backed by
CelExprSchemais 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"