This is a submission for the Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle
What I Built
A hundred years ago, offices kept a "tickler file": 43 folders, one for each day of the month plus one for each month. You dropped a note into a future folder and it resurfaced right before its date. Nothing got missed, because paper does not forget.
Tickler is that file for lease documents. And like the paper version, it never forgets.
Leases bury their most expensive sentences in normal prose. "The tenant shall give written notice at least sixty (60) days before the end of the term." Miss that window and your property rolls into a new term on someone else's rules, or you eat a penalty that was written in plain English the whole time. Small landlords do not have property managers. Tenants definitely do not. They have a PDF in a downloads folder and their memory.
Most tools treat this as extract and hope: pull the text out, let a model guess the dates, show a table. One invented date is enough to make the tool useless, because the wrong date is exactly the one that costs money.
Tickler runs on a different rule: an answer exists only if it can point to the sentence it came from. Here is the whole flow:
-
Upload a lease. Tickler reads the PDF page by page and stores the text in a knowledge graph. Open-weight models (
gpt-oss-20bandgpt-oss-120b, Apache 2.0, served on Groq) pull out the terms: clauses, fields, deadlines. -
Prove it. Every extracted fact must carry a quote and a page. A few lines of TypeScript verify the quote appears word for word on that page. Anything that fails never becomes a fact. It becomes
unknownwith a review flag, becauseunknownbeats a confident guess. - Approve. You review each field next to its source sentence. One click opens the actual PDF page with that sentence highlighted. Your corrections are kept as history, never silently overwritten. Approving computes the deadlines with a pure TypeScript function and schedules the reminder workflows.
- Forget about it. Temporal workflows sleep 90, 30, and 7 days before every deadline. Kill the process, restart the machine, the timer still fires. That sentence is the product.
- Agents take the night shift. When a timer fires, the Watcher agent reads the graph and decides: alert, snooze, or escalate. Escalation hands off to the Drafter agent, which writes your notice letter using only the quotes stored in the graph. Letters are born pending. Nothing sends without your click. Alerts also go out by email.
Tickler reads, reminds, and drafts. The human decides.
Demo
Live, deployed, running right now: tickler-web.onrender.com
Three demo leases ship with the deployment in different states: one waiting for review, one with real 90/30/7 day reminders scheduled, one in demo mode where the first alert lands about 24 seconds after you approve. The "Load sample lease" button gives you a fresh one in a single click.
Honest note about email: alert emails currently reach only my own inbox. Sending to arbitrary recipients needs a verified sending domain: add the domain to the SMTP provider (Resend, here), publish its SPF and DKIM records, and wait for DNS to propagate and verify, which took about 24 hours in my testing. That window risked crossing the submission deadline, so I shipped with the owner-scoped sender and left domain verification as a documented next step. The sending code is identical either way; only the verified from-domain is missing. Everything else works for any visitor: upload, extraction, review, chat, reminders, drafts.
Code
abubakar-ahmed-dev
/
tickler
Tickler: a lease-deadline agent that proves every answer. FalkorDB knowledge graph + Temporal durable timers + open-weight agents. Every fact cites the exact lease sentence it came from.
Tickler
An agent that reads your lease documents, remembers the deadlines hiding in them, and never lets one slip.
Leases bury their important dates in prose: renewal notice windows, rent escalation schedules, deposit return deadlines. Tickler extracts those terms into a knowledge graph, anchors every fact to the exact sentence it came from, schedules durable reminders that survive crashes, and drafts the notice letters you'll eventually need, with the source text cited line-by-line.
How it works
flowchart LR
A["lease PDF"] --> B["ingestion + grounded extraction"]
B --> C[("FalkorDB graph")]
C <--> D["Concierge agent<br/>(chat, cited answers)"]
C --> E["Temporal<br/>durable reminder timers"]
E -->|fires| F["Watcher agent"]
F -->|escalates| G["Drafter agent"]
G --> H["grounded letter<br/>(pending human approval)"]
F --> I["email alert"]
- FalkorDB is the only datastore: pages, clauses, quotes, deadlines, reminders, agent memory, drafts. The agent's tools are Cypher queries; its context is the graph. Remove FalkorDB and nothing works, by…
The README has the full graph map and setup. docs/graph-model.md is the schema, docs/cypher.md is every query the product depends on.
How I Built It
The proof rule: no fact without a sentence
After extraction, for every quote, in plain TypeScript:
const norm = (s: string) => s.replace(/\s+/g, " ").trim();
const ok = norm(page.text).includes(norm(quote.text)); // word for word, or it does not ship
If the check fails, the quote is rejected, the field falls back to unknown, and the human sees exactly why. The model is good. The check is better. That two-line loop is the reason I can put a number on accuracy.
The model reads, the code decides
The most important separation in the codebase: the model only extracts normalized values (temperature 0, strict JSON output). Every piece of date arithmetic, like "notice deadline = term end minus 60 days", happens in a pure TypeScript function with no model in the loop. That is what made the eval meaningful: it measures a deterministic function plus verified inputs.
Why the models run on Groq and not on my laptop
I tried local first, because "your lease never leaves your machine" is the best privacy pitch there is. The plan was Ollama. Then the numbers: gpt-oss:20b needs about 14 GB of RAM for its weights, and gpt-oss:120b needs around 65 GB. My laptop has 8 GB, shared with the OS, a browser, and an editor. Ollama loaded a quantized 20b anyway, started swapping to disk, and token generation collapsed to seconds per token while the whole machine crawled. The runs were not slow, they were unusable.
So the same open weights now run on Groq: same Apache 2.0 model files, same OpenAI-compatible API shape, temperature 0. Only the hardware changed. Anyone who wants the fully local path can point the same base URL at their own Ollama host; the code never left that API.
One graph holds the whole product
Pages, clauses, quotes, fields, deadlines, reminders, alerts, drafts, human corrections, and the assistant's memory all live as nodes and edges in one graph. That choice is plumbing rather than the headline, but it pays off in one spot: the chat assistant has no private knowledge. Its six tools (also exposed as an MCP server) are each a parameterized Cypher query, and every answer must carry the quote and page it walked. The query behind every "where did this come from?" moment:
MATCH (l:Lease {id:$id})-[:HAS_CLAUSE]->(c:Clause)-[:YIELDS]->(f:Field {key:$key})
OPTIONAL MATCH (c)-[:GROUNDS]->(q:Quote)
RETURN f.key, f.value, c.type, q.text, q.pageIdx
One query, the whole proof. The agent cannot invent an answer because its only access to the data is that graph. Every other part of the app reads the same nodes, so there is one version of the truth and nothing to keep in sync.
Humans own the decisions
Extraction review happens before any reminders are scheduled, and a notice letter is born pending. There is no code path that approves a letter. The agents recommend; a person confirms. Tickler extracts and drafts; it does not give legal advice, and the product says so.
Measured, not guessed
I generated 12 fully synthetic leases (fictional people, real clause shapes: rent steps, leap-year terms, deposit windows) with the correct answers computed at generation time. Nine were the dev set. Three were locked away and opened exactly once, after every prompt was frozen.
Dev set: 99 of 99 fields, 29 of 29 deadlines correct. Sealed set, run once: 33 of 33 fields, 11 of 11 deadlines, zero ungrounded quotes, about 4 seconds per lease. The honest caveat: that is synthetic prose, which is regular. Real attorney-drafted leases are the next frontier.
What I got wrong (so you do not have to)
-
I ran a production build while the dev server was running. The build folder corrupted, the app hung, and I spent an hour hunting a bug in my own code. The bug was me. Now I type-check with
tsc --noEmitand never build over a live server. -
A secret that lives in two places is a bug waiting for a redeploy. The database password sat in one variable on the database and another on the clients. They drifted. FalkorDB answered every query with
WRONGPASS. Fix and lesson below in the Render section. - Things I cut on purpose: no OCR (text PDFs only, with a clear error otherwise), no concurrency benchmarks (I did not measure it, so I do not claim it), no agent framework (a plain function-calling loop shipped first and stayed).
How we used Render (and why this stack only works there)
The full product deploys from one render.yaml. One file stands up five resources:
-
tickler-web: the Next.js app, with a persistent disk at
/data/uploadsso uploaded leases survive redeploys. - tickler-falkordb: FalkorDB as a private service on its own persistent disk. The graph is the entire database, and it never touches the public internet.
- tickler-temporal: Temporal's official auto-setup image, as a private service, persisted to Render Postgres.
- tickler-worker: the reminder runner, as a background worker polling Temporal over the private network.
- tickler-temporal-db: managed Postgres underneath Temporal.
- type: pserv
name: tickler-falkordb
runtime: image
image:
url: docker.io/falkordb/falkordb:latest
disk:
name: tickler-graph
mountPath: /data
sizeGB: 1
That block is the entire database deployment. A stranger can fork the repo, press the Deploy to Render button, answer the secret prompts once, and have the whole product running in about ten minutes. Pushes to main auto-deploy.
Now the honest part. Deploying took an evening. Debugging the deployment taught me more about distributed systems than the previous month. Every one of these was a real outage on the live site:
| What we saw | What it looked like | The real cause | The fix |
|---|---|---|---|
| Live dashboard stuck on a loading screen | A frontend bug | The blueprint read the port from Render's service properties, which returns the outside proxy port (10000), not the port inside the private network (6379) | Pinned FALKORDB_PORT=6379 and stopped asking Render to guess |
Every query answered WRONGPASS invalid username-password pair
|
Someone changed the password | The password lived in two variables (one on the database, one on the clients) and had drifted apart after an edit | Reset both in one sitting, database first, then every client, and wrote the order down as a runbook step |
Temporal restart-looping: Unsupported driver specified: 'DB=postgresql12'
|
A broken image | Temporal's setup script wants postgres12, not postgresql12
|
Renamed the value. One word fixed a crash loop |
Worker crash: Cannot find package '@temporalio/worker'
|
A bad deploy | The worker has its own package.json, and the root npm ci never touched it |
Build command: npm ci && cd worker && npm ci
|
A patching script kept writing broken render.yaml files |
A bug in the script | Windows line endings (\r) hiding inside the strings being matched |
Normalize line endings before matching |
The pattern in all five: the error pointed away from the cause. The loading screen was not the frontend. WRONGPASS was not a changed password. Reading Render's service addresses panel, instead of trusting what the blueprint resolved, is what cracked the port bug.
The whole stack costs about $35 a month, which fits inside the challenge's $50 Render credit with room to spare. Two of those resources are disks, which is the quiet feature that made this a real product: uploaded leases and the graph survive redeploys. Without them, every deploy would eat the user's documents.
How we used Temporal (the part that cannot forget)
A deadline product that can forget is worse than no product at all. A cron job dies with the process. A queue empties on restart. So reminders are Temporal workflows, and this is not an add-on, it is the reason the core promise is true.
The anatomy:
- Approving a lease starts one workflow per deadline.
- Inside, three durable timers: 90, 30, and 7 days before the date.
- When a timer fires, an activity runs the Watcher agent, which reads the graph and decides: alert, snooze, or escalate. Escalation runs the Drafter activity, which writes the letter from stored quotes.
- Demo mode: waiting 90 days is not a demo, so the lead times are configurable. Approve a lease and the first alert lands in about 24 seconds.
The proof. This is the demo, and it is the most convincing thing about the product:
| Step | What happens |
|---|---|
| 1 | Approve a lease. Each deadline's workflow sets three timers: 90, 30, and 7 days out. |
| 2 | Kill the worker process mid wait. Not pause. Kill. |
| 3 | The workflow keeps waiting. Its history lives in Temporal, not in our process. |
| 4 | Restart the worker. |
| 5 | Timers fire on schedule. Alerts and letters arrive exactly as if nothing happened. |
The mistake worth admitting: Temporal ships two client packages and they are not interchangeable. The worker needs @temporalio/worker (which brings NativeConnection), while anything that just starts workflows, like the web app's review endpoint, needs Connection from @temporalio/client. I imported the wrong one at first and paid for it in bundle size and confusion. Splitting the two fixed it in minutes once I understood the difference.
Temporal itself runs on Render as a private service (the official temporalio/auto-setup image over Render Postgres), so the reminder engine lives on the same private network as the graph and the worker. Nobody outside the deployment can even reach it.
One line sums up the split: the graph holds what matters. Temporal holds when. Neither is allowed to be the other's problem.
Why Does Open Innovation Matter
- Privacy is the product. A lease is the most sensitive document a landlord owns: names, addresses, money. Extraction runs on open-weight models (gpt-oss, Apache 2.0) served on Groq. Because the weights are open, anyone can audit the exact model or self-host it later. I tried self-hosting first; that attempt is its own story above.
- A paid service, made free. Commercial lease abstraction exists. Vendors charge per lease, so small owners skip it and absorb the misses. An open stack (graph database + Temporal + open weights) makes one lease cost about 4 seconds and a fraction of a cent.
- No lock-in, by architecture. Every component is open source or open weight, and each one is replaceable: a graph database holds state, Temporal runs the timers, the open weights do the reading and the writing. Swap any one of them; nothing collapses into one vendor's API.
- The honesty is auditable. The quote check, the code-computed deadlines, and the citation trail are all in the open repo. You can check exactly why the tool believes a date.
Prize Categories
Best Use of Render. One blueprint file stands up the entire product: web service, background worker, a graph database as a private service with its own disk, Temporal as a private service, managed Postgres, and a second disk so user documents survive redeploys. The database never touches the public internet and the worker talks to Temporal over the private network. We did not just deploy on Render, we debugged on it: proxy ports versus container ports, password pairing across services, driver names, monorepo installs, all documented above with the fixes in the repo. A stranger goes from fork to running product in about ten minutes, and it is live right now for about $35 a month.
Best Use of Temporal. The single thing this product may never do is forget, and forgetting is exactly what ordinary schedulers do on restart. Every deadline gets its own workflow with durable timers at 90, 30, and 7 days. The agents run as activities. We kill the worker mid wait and the timers still fire, on camera. Temporal even runs on Render itself (official auto-setup image), so the reminder engine sits on the same private network as everything else.
For completeness: we are not entering the model or observability categories. Tickler runs open-weight gpt-oss models on Groq rather than locally hosted Gemma or Ollama, and Sentry is not wired in. We would rather enter two categories with a deep, honest story than five with a shallow one.
Tickler reads, reminds, and drafts. The human decides. The graph remembers everything.
Try it: tickler-web.onrender.com · Code: github.com/abubakar-ahmed-dev/tickler









Top comments (0)