{"id":48683402,"url":"https://github.com/enginerd-kr/chronis","last_synced_at":"2026-04-11T03:37:01.361Z","repository":{"id":323684005,"uuid":"1090756308","full_name":"enginerd-kr/chronis","owner":"enginerd-kr","description":"Python AI Agent-Friendly Distributed Scheduler","archived":false,"fork":false,"pushed_at":"2026-04-11T01:48:09.000Z","size":625,"stargazers_count":1,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2026-04-11T03:25:01.687Z","etag":null,"topics":["ai","ai-agent","ai-agents","distributed","llm","postgresql","python","redis","scheduler"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/enginerd-kr.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2025-11-06T05:08:26.000Z","updated_at":"2026-04-11T01:47:23.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/enginerd-kr/chronis","commit_stats":null,"previous_names":["enginerd-kr/chronis"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/enginerd-kr/chronis","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/enginerd-kr%2Fchronis","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/enginerd-kr%2Fchronis/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/enginerd-kr%2Fchronis/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/enginerd-kr%2Fchronis/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/enginerd-kr","download_url":"https://codeload.github.com/enginerd-kr/chronis/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/enginerd-kr%2Fchronis/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31668049,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-10T17:19:37.612Z","status":"online","status_checked_at":"2026-04-11T02:00:05.776Z","response_time":54,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ai","ai-agent","ai-agents","distributed","llm","postgresql","python","redis","scheduler"],"created_at":"2026-04-11T03:36:55.892Z","updated_at":"2026-04-11T03:37:01.349Z","avatar_url":"https://github.com/enginerd-kr.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Chronis - AI Agent-Friendly Distributed Scheduler\n\n**Chronis** - Python scheduler designed for AI agents, LLM workflows, and multi-container environments\n\n## Why Chronis?\n\n**Agentic AI needs autonomous scheduling.** For AI agents to truly work independently, they must manage their own schedules - setting reminders, scheduling follow-ups, and orchestrating time-based workflows without human intervention. Traditional schedulers require complex configuration and aren't built for this. Chronis makes it possible with a simple, LLM-friendly API designed for autonomous operation.\n\n**AI-optimized real-time scheduling.** Traditional schedulers optimize for millisecond precision, but when an AI agent schedules \"30 minutes from now,\" human perception doesn't distinguish between 30:00 and 30:08—both feel like \"30 minutes later.\"\n\nThis insight drives our architecture:\n\n- **AI Agent Ready**: Simple API perfect for LLM function calling and agent workflows\n- **Human-Scale Timing**: Configurable polling intervals deliver the responsiveness AI agents need\n- **Architectural Simplicity**: No message brokers or event streams—just configurable polling with your database\n\nChronis also provides:\n\n- **Pluggable Everything**: Bring your own storage and locks (PostgreSQL, Redis, or custom adapters)\n- **Distributed by Default**: No duplicate executions across containers or processes\n- **Timezone Aware**: IANA timezones with automatic DST handling\n- **Async Native**: Built for modern Python with full async/await support\n\n## Installation\n\n```bash\npip install chronis\n\n# Optional: Install with adapter dependencies\npip install chronis[redis]      # Redis storage and locking\npip install chronis[postgres]   # PostgreSQL storage\npip install chronis[all]        # All adapters\n```\n\n## Quick Start\n\n```python\nfrom chronis import PollingScheduler, InMemoryStorage, InMemoryLock\n\n# Setup\nscheduler = PollingScheduler(\n    storage_adapter=InMemoryStorage(),\n    lock_adapter=InMemoryLock(),\n)\n\ndef send_email(**extra):\n    print(\"Sending email...\")\n\nscheduler.register_job_function(\"send_email\", send_email)\n\n# Create jobs with simple fluent API\nscheduler.every(minutes=5).run(\"send_email\")                          # Every 5 minutes\nscheduler.on(hour=9, minute=30).run(\"send_email\")                     # Daily at 9:30\nscheduler.on(day_of_week=\"mon\", hour=9).run(\"send_email\")             # Every Monday at 9:00\nscheduler.once(when=\"2025-12-25T09:00:00\").run(\"send_email\")          # Once at specific time\n\n# With options\nscheduler.every(hours=1).config(retry=3, timeout=300).run(\"send_email\")\nscheduler.on(hour=9).config(timezone=\"Asia/Seoul\").run(\"send_email\")\n\nscheduler.start()\n```\n\n### Accepting Extra Keyword Arguments\n\nWhen registering job functions, always declare `**extra` as a catch-all parameter. This ensures your function gracefully handles any additional keyword arguments that may be passed at runtime:\n\n```python\n# Good - accepts extra keyword arguments\ndef send_email(**extra):\n    print(\"Sending email...\")\n\ndef generate_report(report_type: str, **extra):\n    print(f\"Generating {report_type} report...\")\n\n# Bad - will raise TypeError on unexpected keyword arguments\ndef send_email():\n    print(\"Sending email...\")\n```\n\nThis is especially important when your function is called with keyword arguments via `run()` or `kwargs={}`, as future updates or integrations may introduce additional context.\n\n## API\n\n```python\n# every() - Interval scheduling\nscheduler.every(seconds=30).run(\"task\")\nscheduler.every(minutes=5).run(\"task\")\nscheduler.every(hours=1, minutes=30).run(\"task\")\n\n# on() - Cron scheduling (specific times)\nscheduler.on(minute=5).run(\"task\")                      # Every hour at :05\nscheduler.on(hour=9, minute=30).run(\"task\")             # Daily at 9:30\nscheduler.on(day_of_week=\"mon\", hour=9).run(\"task\")     # Weekly on Monday\nscheduler.on(day=1, hour=0, minute=0).run(\"task\")       # Monthly on 1st\nscheduler.on(month=\"jan\", day=1).run(\"task\")            # Yearly on Jan 1st\n\n# once() - One-time scheduling\nscheduler.once(when=\"2025-12-25T09:00:00\").run(\"task\")\nscheduler.once(when=datetime.now() + timedelta(hours=1)).run(\"task\")\n\n# config() - Options (can be chained in any order before run())\nscheduler.every(minutes=5).config(\n    retry=3,                    # Max retry attempts\n    timeout=300,                # Timeout in seconds\n    timezone=\"Asia/Seoul\",      # Timezone\n    metadata={\"env\": \"prod\"},   # Custom metadata\n).run(\"task\")\n\n# config() first is also valid\nscheduler.config(retry=3).every(minutes=5).run(\"task\")\n```\n\n## AI Agent Example\n\n```python\n# LLM function calling - minimal parameters!\ndef schedule_reminder(message: str, hours_from_now: int):\n    \"\"\"AI agent schedules a reminder.\"\"\"\n    job = scheduler.once(\n        when=datetime.now() + timedelta(hours=hours_from_now)\n    ).run(\"send_notification\", message=message)\n    return f\"Reminder scheduled: {job.job_id}\"\n\nschedule_reminder(\"Check on customer\", 24)\n```\n\n## Job Management\n\n```python\n# Query and manage jobs\njobs = scheduler.query_jobs(filters={\"status\": \"scheduled\"})\njob = scheduler.get_job(job_id)\n\nscheduler.pause_job(job_id)\nscheduler.resume_job(job_id)\nscheduler.delete_job(job_id)\n```\n\n## Callbacks\n\nMonitor and react to job execution results:\n\n```python\n# Global handlers for all jobs\ndef on_job_failure(job_id: str, error: Exception, job_info):\n    logger.error(f\"Job {job_id} failed: {error}\")\n    send_alert(job_id, error)\n\ndef on_job_success(job_id: str, job_info):\n    logger.info(f\"Job {job_id} completed successfully\")\n\nscheduler = PollingScheduler(\n    storage_adapter=storage,\n    lock_adapter=lock,\n    on_failure=on_job_failure,\n    on_success=on_job_success,\n)\n\n# Job-specific handlers\ndef on_critical_failure(job_id: str, error: Exception, job_info):\n    send_urgent_alert(error)\n\ndef on_critical_success(job_id: str, job_info):\n    update_dashboard(job_id)\n\nscheduler.every(hours=1).config(\n    on_failure=on_critical_failure,\n    on_success=on_critical_success,\n).run(\"critical_task\")\n```\n\n## Direct API\n\nFor advanced use cases, the direct API with full parameter control is also available. Use these methods when you need explicit control over all job parameters or when integrating programmatically:\n\n```python\nscheduler.create_interval_job(func=\"task\", seconds=30, max_retries=3)\nscheduler.create_cron_job(func=\"task\", hour=9, minute=0, timezone=\"UTC\")\nscheduler.create_date_job(func=\"task\", run_date=\"2025-12-25T09:00:00\")\n```\n\nThese methods accept all configuration options as explicit parameters, making them suitable for dynamic job creation where parameters are determined at runtime.\n\n## Advanced Options\n\n### Misfire Handling\n\nWhen a scheduler is down or busy, jobs may miss their scheduled execution time. The `if_missed` option controls how Chronis handles these misfired jobs when the scheduler recovers:\n\n- `skip`: Ignore missed executions entirely (default)\n- `run_once`: Execute the job once to catch up, regardless of how many executions were missed\n- `run_all`: Execute all missed runs (use with caution for interval jobs)\n\n```python\nscheduler.on(hour=9).config(if_missed=\"run_once\").run(\"daily_report\")\nscheduler.every(hours=1).config(if_missed=\"skip\").run(\"cleanup\")\n```\n\n### Timeout \u0026 Retry\n\nJobs can fail due to network issues, external service outages, or long-running operations. Configure timeout and retry behavior to handle these scenarios gracefully:\n\n```python\nscheduler.every(minutes=5).config(\n    timeout=60,           # Kill job if it exceeds 60 seconds\n    retry=3,              # Retry up to 3 times on failure\n    retry_delay=60,       # Base delay in seconds (exponential backoff)\n).run(\"sync_external_api\")\n```\n\nRetries use **exponential backoff**: `delay = retry_delay × 2^(attempt-1)`, capped at 3600s. With `retry_delay=60` and `retry=3`, the delays are 60s → 120s → 240s. On success, the retry count resets to 0. The `on_failure` callback fires only after all retries are exhausted.\n\n### Multi-Tenancy \u0026 Metadata\n\nStore custom key-value pairs with jobs using the `metadata` option. This is useful for multi-tenancy (tag jobs by tenant/environment), filtering (query jobs by metadata fields), and passing additional context for logging or debugging.\n\n```python\nscheduler.every(hours=1).config(metadata={\"tenant_id\": \"acme\", \"env\": \"prod\"}).run(\"task\")\njobs = scheduler.query_jobs(filters={\"metadata.tenant_id\": \"acme\"})\n```\n\n## Learn More\n\n- [Adapter Implementation Guide](docs/ADAPTER_GUIDE.md) - Build custom storage/lock adapters\n- [Examples](examples/) - Complete working examples\n- [Contributing](CONTRIBUTING.md) - Development setup and guidelines\n\n## License\n\nMIT License - see [LICENSE](LICENSE) for details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fenginerd-kr%2Fchronis","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fenginerd-kr%2Fchronis","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fenginerd-kr%2Fchronis/lists"}