Recurrence Validator
time.validate_recurrence
Overview
Parses an RFC 5545 recurrence rule with ical4j and reports whether it is valid, what it means in a fixed English rendering, which parts it declares, and up to 10 preview occurrences. Structural observations are returned as findings: a BYMONTHDAY that skips short months, a BYYEARDAY=366 that skips common years, a BYWEEKNO whose meaning depends on an undeclared WKST, and a rule with no COUNT or UNTIL. A malformed rule is a finding with valid: false, not an error, because evaluating the rule succeeded. This validates rule syntax and structure — it does not judge whether the schedule is the one you intended.
A valid RRULE can still skip months or repeat forever — we parse with ical4j, surface structural findings, and preview a few occurrences before you materialize the full series.
Reliability
Same rule, same verdict — for a given ical4j and IANA time-zone database version, both reported in _meta. The explanation comes from a fixed template rather than generated prose, so it is identical across locales and releases, and we read no clock. A rule that does not parse, or that is too expensive to expand, comes back as an ordinary success with valid false and an error-severity finding — so branch on valid rather than on your error path, and remember that valid true means the syntax holds, not that the schedule is the one you meant. Only two conditions stop us evaluating at all. RRULE_UNSUPPORTED_PART — trigger: the rule carries a part name outside the supported subset, most often a calendar-file property pasted in alongside the rule such as DTSTART, RDATE or EXDATE, or a misspelling like BYWEEKDAY; retryable: no, the same text always fails; next: the message names the part — drop it or correct the spelling, and move a DTSTART into startDateTime. Note that this one is a failure rather than a finding, so an unsupported part and a malformed value reach you by different routes. TIMEZONE_UNKNOWN — trigger: the zone field is not a zone we can resolve, which covers a typo, a three-letter abbreviation such as CST, and a fixed offset such as +02:00; retryable: no; next: send a region-based identifier like Europe/Brussels, or UTC.
Context footprint
One call replaces a rule-reading exercise and returns a short verdict, a sentence, and a handful of instants. A successful call costs 1 execution credit.
When to use it
When a rule arrives from a user or upstream system and you need parse validity, a fixed-template explanation, and a short preview.
Do not use to materialize a long series (use time.generate_recurrence), parse a .ics file, or author a rule from natural language.
Powered by ical4j, the reference-grade RFC 5545 iCalendar library for recurrence-rule parsing and validation.
Scope
Non-goals
- Does not expand a full series (use time.generate_recurrence)
- Does not author a rule from a natural-language description
- Does not correct an invalid rule or suggest a replacement
- Does not judge whether the schedule matches the caller's intent — only its syntax and structure
- Does not read the system clock
Properties
Capabilities
Cost & billing
Flat rate per invocation
File handling
How we handle your data
What this means
This skill may receive sensitive content and may return submitted, transformed, extracted, or reformatted content.
Squillify guarantees
- Runs inside Squillify
- This skill runs inside Squillify.
- Payloads are not sent to an external service by this skill.
- Squillify does not permanently store submitted payloads.
- Telemetry records metadata only, not submitted content.
- Submitted payloads are not used for training.
- Submitted payloads are not reviewed by humans.
- This skill does not target sensitive data.
- The response may contain submitted or transformed content.
Your responsibility
Squillify does not determine whether submitted content is sensitive. Your application is responsible for deciding what data it is allowed to submit. Your application is responsible for securing the response.
Input schema
| Field | Type | Required | Description |
|---|---|---|---|
| rrule | string | Yes | Supported RFC 5545 RRULE body. |
| startDateTime | string | Yes | Zone-less recurrence seed. |
| zone | string | Yes | IANA recurrence zone or UTC. |
| previewCount | integer | No | Preview cap; omit it for the published default. |
Output schema
| Field | Type | Required | Description |
|---|---|---|---|
| valid | boolean | Yes | Whether the rule parses and expands safely. |
| findings | array | Yes | From 0 through 16 bounded recurrence findings, each a closed code, message, path, and severity. |
| explanation | string | Yes | Fixed-template English explanation. |
| parts | object | Yes | Purpose-bound bounded map of declared RFC 5545 part names to parsed values. |
| normalized | string | No | Canonical normalized RRULE. |
| nextOccurrences | array | Yes | From 0 through the requested preview cap of ISO offset occurrences in ascending order. |
| infinite | boolean | Yes | Whether the valid rule is unbounded. |
| _meta | object | No | Closed recurrence-engine version receipt with tzdbVersion, engine identity, and engineVersion. |
API contract
POST /v1/executecurl -sS -X POST "$BASE/v1/execute" -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d '{"skillKey":"time.validate_recurrence","input":{"rrule":"FREQ=MONTHLY;BYMONTHDAY=31","startDateTime":"2026-01-31T09:00:00","zone":"UTC"}}'import { Squillify } from "@squillify/sdk";
const sq = new Squillify({ apiKey: process.env.SQUILLIFY_API_KEY });
const { output } = await sq.execute("time.validate_recurrence", {"zone":"UTC","startDateTime":"2026-01-31T09:00:00","rrule":"FREQ=MONTHLY;BYMONTHDAY=31","previewCount":3});Errors
| Code | HTTP | Retryable | Description |
|---|---|---|---|
| VALIDATION_ERROR | 400 | No | The request payload failed schema validation. |
| UNAUTHORIZED | 401 | No | Missing or invalid API key. |
| SKILL_NOT_ACCESSIBLE | 403 | No | The API key is not granted access to this skill. |
| SKILL_NOT_AVAILABLE_FOR_PLAN | 403 | No | This skill is not included in your current plan. |
| PLAN_ELIGIBILITY_UNAVAILABLE | 503 | Yes | Plan eligibility could not be decided. Retry after backoff. |
| SKILL_NOT_FOUND | 404 | No | The requested skill or version does not exist. |
| TENANT_RATE_LIMITED | 429 | Yes | Request rate limit exceeded. Retry after backoff. |
| QUOTA_EXCEEDED | 429 | Yes | Plan quota exhausted. Retryable once the quota period resets. |
| EXECUTION_TIMEOUT | 504 | Yes | Execution exceeded the allowed time budget. |
| RRULE_UNSUPPORTED_PART | 400 | No | The skill does not support the requested variant. |
| TIMEZONE_UNKNOWN | 400 | No | The request payload failed this skill's input validation. |
Other limits
| Limit | Value |
|---|---|
| Recommended inline bytes | 32 KiB |
Limitations
- Does not expand a full series (use time.generate_recurrence)
- Does not author a rule from a natural-language description
- Does not correct an invalid rule or suggest a replacement
- Does not judge whether the schedule matches the caller's intent — only its syntax and structure
- Does not read the system clock
- Materializing a long series (use time.generate_recurrence)
- Parsing an .ics file
- Turning prose into a rule
How to call
POST /v1/execute (requires authentication)
{
"skillKey": "time.validate_recurrence",
"input": {
"zone": "UTC",
"startDateTime": "2026-01-31T09:00:00",
"rrule": "FREQ=MONTHLY;BYMONTHDAY=31",
"previewCount": 3
}
}Examples
A monthly rule that skips short months
{
"zone": "UTC",
"startDateTime": "2026-01-31T09:00:00",
"rrule": "FREQ=MONTHLY;BYMONTHDAY=31",
"previewCount": 3
}{
"explanation": "Every month on day 31",
"findings": [
{
"code": "RRULE_SKIPS_SHORT_MONTHS",
"message": "BYMONTHDAY=31 yields no occurrence in months that are too short.",
"path": "/rrule",
"severity": "warning",
"details": {
"monthDays": [
"31"
]
}
},
{
"code": "RRULE_UNBOUNDED",
"message": "The rule declares neither COUNT nor UNTIL and therefore repeats indefinitely.",
"path": "/rrule",
"severity": "info",
"details": {}
}
],
"valid": true,
"nextOccurrences": [
"2026-01-31T09:00:00Z",
"2026-03-31T09:00:00Z",
"2026-05-31T09:00:00Z"
],
"parts": {
"BYMONTHDAY": "31",
"FREQ": "MONTHLY"
},
"infinite": true,
"_meta": {
"engine": "ical4j",
"engineVersion": "4.3.0",
"tzdbVersion": "2026b"
},
"normalized": "FREQ=MONTHLY;BYMONTHDAY=31"
}A malformed rule is reported, not thrown
{
"startDateTime": "2026-03-02T09:00:00",
"zone": "UTC",
"rrule": "FREQ=WEEKLY;BYDAY=XX"
}{
"findings": [
{
"code": "RRULE_MALFORMED",
"message": "Part 'BYDAY' has a value the RFC 5545 grammar does not accept.",
"path": "/rrule",
"severity": "error",
"details": {}
}
],
"_meta": {
"engine": "ical4j",
"engineVersion": "4.3.0",
"tzdbVersion": "2026b"
},
"valid": false,
"explanation": "",
"nextOccurrences": [],
"infinite": false,
"parts": {}
}Recommended use cases
- Check a user-entered recurrence rule before storing it
- Show a human-readable summary of a stored rule in a UI
- Discover that BYMONTHDAY=31 silently skips February, April, June, September, and November
Avoid for
- Materializing a long series (use time.generate_recurrence)
- Parsing an .ics file
- Turning prose into a rule
Related skills
Composable with
Safety & privacy
- Runs without calling any external service.
- Your payloads are never stored permanently.
- Telemetry captures metadata only — never your payload contents.
- We never train models on your payloads.
- No human reviews your payloads.