This project automatically generates a single daily personal development journaling question and emails it to you every morning using:
- GitHub Actions (scheduler + runner)
- OpenAI API (question generation)
- Brevo (transactional email delivery)
- Railway (feedback endpoint)
The system:
- Rotates through 4 languages in a randomised, non-repeating cycle (Serbian, Turkish, French, Russian)
- Maintains a historical log of all prior questions
- Feeds previous questions back to the model for continuity
- Learns from your 👍 / 👎 feedback to shape future questions
- Commits updated history and feedback to the repository automatically
- How It Works
- Languages and Cycle
- Repository Structure
- Required Accounts
- Setup
- All GitHub Actions Secrets
- Customising Languages
- Customising Prompt Behaviour
- Changing the LLM
- Theme Categories
- API Cost Estimate
- GitHub Actions runs daily at 05:00 MSK (02:00 UTC)
- The script picks today's language from a shuffled 4-language cycle (no language repeats until all 4 have been used)
- It loads recent question history and your feedback ratings
- OpenAI generates the next question — guided by history, theme categories, and feedback
- The question is emailed via Brevo with 👍 / 👎 feedback buttons
- Clicking a button hits the Railway server, which writes your rating to
data/feedback.csv - Tomorrow's generation reads that feedback and adjusts accordingly
- GitHub commits updated history, language state, and feedback automatically
Each day one language is chosen at random. Once all 4 have been used, the cycle resets with a new random order. To add, remove, or swap a language see Customising Languages.
| Language | Script |
|---|---|
| Српски | Serbian Cyrillic |
| Türkçe | Turkish |
| Français | French |
| Русский | Russian |
.github/workflows/journal_prompt.yml # Daily scheduler
scripts/daily_prompt.py # Question generation + email
feedback_server/server.py # Flask feedback endpoint (Railway)
feedback_server/requirements.txt # Feedback server dependencies
data/journal_questions.jsonl # Question history (auto-committed)
data/journal_questions_lang_state.json # Language cycle state (auto-committed)
data/feedback.csv # Thumbs up/down ratings (auto-committed)
requirements.txt # Python dependencies
Procfile # Railway start command
- GitHub — runs the daily workflow
- OpenAI — generates questions
- Brevo — sends the email
- Railway — hosts the feedback server
- Go to platform.openai.com → API Keys
- Create a new secret key
- Add to GitHub as secret:
OPENAI_API_KEY
- Create an account at brevo.com
- Go to Transactional → Settings → API Keys and create a key
- Add and verify a sender address under Senders, Domains & Dedicated IPs
Add these as GitHub Actions secrets:
| Secret | Value |
|---|---|
BREVO_API_KEY |
Your Brevo API key |
BREVO_SENDER_EMAIL |
Verified sender address |
BREVO_SENDER_NAME |
Display name for the sender |
BREVO_TO_EMAIL |
Your email address |
BREVO_TO_NAME |
Your name |
SUBJECT_PREFIX |
e.g. Daily Prompt (optional) |
The feedback server is a small Flask app that receives 👍/👎 clicks from your email and writes ratings to data/feedback.csv via the GitHub API.
- GitHub → Settings → Developer Settings → Fine-grained tokens
- New token with:
- Repository:
personal-development-journal - Permission: Contents → Read and write
- Repository:
- Copy the token
Use the link above to automatically deploy on Railway (recommended), or follow the steps below for a custom deployment.
- Go to railway.app → New Project → Deploy from GitHub repo
- Select this repository
- In the service → Settings → Build & Deploy, set:
- Start command:
gunicorn --bind 0.0.0.0:$PORT feedback_server.server:app
- Start command:
- In the service → Variables, add:
| Variable | Value |
|---|---|
FEEDBACK_TOKEN |
A long random secret (openssl rand -hex 32) |
GH_PAT |
The PAT from step A |
GH_REPO |
kghamilton89/personal-development-journal |
GH_BRANCH |
main |
FEEDBACK_CSV_PATH |
data/feedback.csv |
- Go to Settings → Networking → Generate Domain and copy the URL
| Secret | Value |
|---|---|
FEEDBACK_BASE_URL |
https://your-service.up.railway.app |
FEEDBACK_TOKEN |
Same value as Railway's FEEDBACK_TOKEN |
- Go to Actions tab → Daily journaling question
- Click Run workflow
- Confirm you receive an email with a question and 👍/👎 buttons
- Click 👍 or 👎 in the email
- Confirm
data/feedback.csvappears in the repo
| Secret | Purpose |
|---|---|
OPENAI_API_KEY |
Question generation |
BREVO_API_KEY |
Email delivery |
BREVO_SENDER_EMAIL |
Sender address |
BREVO_SENDER_NAME |
Sender display name |
BREVO_TO_EMAIL |
Recipient address |
BREVO_TO_NAME |
Recipient name |
SUBJECT_PREFIX |
Email subject prefix |
FEEDBACK_BASE_URL |
Railway server URL |
FEEDBACK_TOKEN |
Shared secret for feedback auth |
The language pool is defined in two places. Both must be updated together whenever you add, remove, or change a language.
Each entry needs three fields:
| Field | Purpose |
|---|---|
code |
Internal identifier (lowercase, no spaces) |
instruction |
Exact phrase passed to the model, e.g. "Italian" or "Arabic (Modern Standard)" |
label |
Native-script display name shown in the email, e.g. "Italiano" |
LANGUAGES = [
{"code": "serbian", "instruction": "Serbian (Cyrillic script)", "label": "Српски"},
{"code": "turkish", "instruction": "Turkish", "label": "Türkçe"},
{"code": "french", "instruction": "French", "label": "Français"},
{"code": "russian", "instruction": "Russian", "label": "Русский"},
# Add a new language like this:
# {"code": "italian", "instruction": "Italian", "label": "Italiano"},
]Add a matching entry for every language code you add to LANGUAGES. Each entry needs translations for both the thumbs-up and thumbs-down thank-you page. The tuple format is (html_lang_tag, title, body).
RESPONSES = {
...
# New entry matching the code in daily_prompt.py:
"italian": {
"up": ("it", "Grazie per il pollice su!", "Bene — ne arriveranno altre così."),
"down": ("it", "Annotato.", "Feedback registrato — la prossima domanda punterà più in alto."),
},
}You can look up the correct html_lang_tag value for any language at r12a.github.io/app-subtags.
Whenever you change the language list, delete the old cycle state file so a clean queue is generated on the next run:
git rm --ignore-unmatch data/journal_questions_lang_state.json
git commit -m "chore: reset lang state after language change"
git pushIf the file doesn't exist yet this command is a no-op — that's fine.
All prompt logic lives in build_instructions() in scripts/daily_prompt.py. The function constructs the system prompt that is sent to the model before every generation. You can edit it freely — no other file needs to change.
The seven content themes are listed inline in build_instructions(). To add, remove, or reword a category, find this block and edit it directly:
"- Draw from the following theme categories, cycling through them so that NO single\n"
" category dominates the sequence over any 7-day window:\n"
" 1. Goals & disciplined execution — ...\n"
" 2. Philosophy — ...\n"
# Add or remove numbered entries hereKeep the numbering sequential and leave the cycling instruction intact, otherwise the model has no guidance on variety.
The lines below the theme list control register and quality. Adjust them to suit your preferences:
"- Must be intellectually serious and specific — no vague generalities.\n"
"- Avoid therapy clichés, motivational fluff, and self-help platitudes.\n"
"- Avoid repeating prior structure, framing, or wording.\n"
"- Maintain long-term conceptual progression across days...\n"For example, to make questions more personal and less academic you might add:
"- Favour questions grounded in lived experience over abstract theory.\n"The number of previous questions passed to the model as context is controlled by the HISTORY_TAIL environment variable (default: 120). Set it in the GitHub Actions workflow or as a repo secret:
| Value | Effect |
|---|---|
30 |
Short memory — faster, lower token cost, less risk of repetition detection |
120 |
Default — strong continuity and de-duplication across ~4 months |
365 |
Long memory — most context, highest token cost |
The paragraph that instructs the model how to interpret your 👍/👎 ratings is also in build_instructions(), inside the feedback_block construction. If you want the model to weight feedback more or less heavily, edit the framing text there.
The model is called in generate_question() in scripts/daily_prompt.py. Swap the provider by replacing the client and API call — the rest of the pipeline (history, email, feedback) is completely decoupled from the model choice.
1. Install the new SDK — add it to requirements.txt:
# Example: replace or add alongside the existing openai entry
some-other-sdk>=1.0.0
2. Replace the client and call in generate_question():
# Current (OpenAI)
client = OpenAI(api_key=must_getenv("OPENAI_API_KEY"))
resp = client.responses.create(
model="gpt-5.2",
instructions=instructions,
input=user_input,
)
return normalize_output(resp.output_text)
# Replace with your provider's equivalent, for example:
client = AnotherProviderClient(api_key=must_getenv("OTHER_API_KEY"))
resp = client.messages.create(
model="their-model-name",
system=instructions,
messages=[{"role": "user", "content": user_input}],
max_tokens=256,
)
return normalize_output(resp.content[0].text)The exact method names and response shape vary by provider — consult their SDK docs.
3. Update the API key secret — add the new key to GitHub Actions secrets and reference it in the workflow env block:
env:
OTHER_API_KEY: ${{ secrets.OTHER_API_KEY }}You can remove OPENAI_API_KEY from the workflow once you have switched over.
Everything outside generate_question() is provider-agnostic: language selection, history logging, feedback loading, the email format, and the Railway feedback server are all unaffected by which model you use.
The model cycles through 7 content themes across days:
- Goals & disciplined execution — long-term aims, systems, strategic assumptions
- Philosophy — metaphysics, epistemology, ethics as theory, the examined life
- Personal reflection — identity, memory, relationships, values in practice
- Historical counterfactuals — pivotal moments, contingency, alternative histories
- Intellectual curiosity — science, mathematics, language, cross-disciplinary ideas
- Ethics & values in practice — moral dilemmas, competing obligations, integrity
- Creativity & meaning — aesthetics, craft, narrative, meaningful work
No single category dominates any 7-day window.
Usage is extremely low — one API call per day with a small context window. Estimated cost: < $1/month under normal usage.
If history grows too large, reduce HISTORY_TAIL (recommended range: 80–150).