Skeleton scripts

The blank-page problem kills more hobby projects than any technical wall does. Pick a language where it matters — most of these ship in more than one. Free to take, no strings, no sign-up.

Servo Sweep & Position Control

Get a servo moving predictably before you touch your real project.

motor & servoembedded
View skeleton script
#include <Servo.h>

// ── CONFIG ────────────────────────────────
const int SERVO_PIN   = 9;     // TODO: your PWM-capable pin
const int MIN_ANGLE   = 0;
const int MAX_ANGLE   = 180;
const int STEP_DELAY  = 15;    // ms between steps

Servo myServo;
int angle = MIN_ANGLE;
int direction = 1;

void setup() {
  myServo.attach(SERVO_PIN);
  myServo.write(angle);
}

void loop() {
  myServo.write(angle);
  angle += direction;

  if (angle >= MAX_ANGLE || angle <= MIN_ANGLE) {
    direction *= -1;
  }
  delay(STEP_DELAY);

  // TODO: replace the sweep with your real logic
}

void moveTo(int targetAngle) {
  targetAngle = constrain(targetAngle, MIN_ANGLE, MAX_ANGLE);
  myServo.write(targetAngle);
}
import time
import board
import pwmio
from adafruit_motor import servo

# ── CONFIG ────────────────────────────────
SERVO_PIN = board.D9      # TODO: your PWM-capable pin
MIN_ANGLE = 0
MAX_ANGLE = 180
STEP_DELAY = 0.015

pwm = pwmio.PWMOut(SERVO_PIN, duty_cycle=2 ** 15, frequency=50)
my_servo = servo.Servo(pwm)

angle = MIN_ANGLE
direction = 1

def move_to(target_angle):
    target_angle = max(MIN_ANGLE, min(MAX_ANGLE, target_angle))
    my_servo.angle = target_angle

while True:
    my_servo.angle = angle
    angle += direction

    if angle >= MAX_ANGLE or angle <= MIN_ANGLE:
        direction *= -1

    time.sleep(STEP_DELAY)

    # TODO: replace the sweep with your real logic

DC Motor — PWM Driver

Direction and speed control through a standard H-bridge (L298N / TB6612-style).

motor & servoembedded
View skeleton script
// ── CONFIG ────────────────────────────────
const int PIN_IN1 = 5;   // TODO: H-bridge input 1
const int PIN_IN2 = 6;   // TODO: H-bridge input 2
const int PIN_PWM = 9;   // TODO: enable / speed pin

void setup() {
  pinMode(PIN_IN1, OUTPUT);
  pinMode(PIN_IN2, OUTPUT);
  pinMode(PIN_PWM, OUTPUT);
  stopMotor();
}

void loop() {
  // TODO: replace this demo pattern with real control logic
  forward(180);   delay(1500);
  stopMotor();    delay(400);
  backward(180);  delay(1500);
  stopMotor();    delay(400);
}

void forward(int speed) {
  digitalWrite(PIN_IN1, HIGH);
  digitalWrite(PIN_IN2, LOW);
  analogWrite(PIN_PWM, constrain(speed, 0, 255));
}

void backward(int speed) {
  digitalWrite(PIN_IN1, LOW);
  digitalWrite(PIN_IN2, HIGH);
  analogWrite(PIN_PWM, constrain(speed, 0, 255));
}

void stopMotor() {
  digitalWrite(PIN_IN1, LOW);
  digitalWrite(PIN_IN2, LOW);
  analogWrite(PIN_PWM, 0);
}
import RPi.GPIO as GPIO
import time

# ── CONFIG ────────────────────────────────
PIN_IN1 = 17
PIN_IN2 = 27
PIN_PWM = 18

GPIO.setmode(GPIO.BCM)
GPIO.setup(PIN_IN1, GPIO.OUT)
GPIO.setup(PIN_IN2, GPIO.OUT)
GPIO.setup(PIN_PWM, GPIO.OUT)
pwm = GPIO.PWM(PIN_PWM, 1000)
pwm.start(0)

def forward(speed):
    GPIO.output(PIN_IN1, GPIO.HIGH)
    GPIO.output(PIN_IN2, GPIO.LOW)
    pwm.ChangeDutyCycle(max(0, min(100, speed)))

def backward(speed):
    GPIO.output(PIN_IN1, GPIO.LOW)
    GPIO.output(PIN_IN2, GPIO.HIGH)
    pwm.ChangeDutyCycle(max(0, min(100, speed)))

def stop_motor():
    GPIO.output(PIN_IN1, GPIO.LOW)
    GPIO.output(PIN_IN2, GPIO.LOW)
    pwm.ChangeDutyCycle(0)

try:
    # TODO: replace this demo pattern with real control logic
    while True:
        forward(70);  time.sleep(1.5)
        stop_motor(); time.sleep(0.4)
        backward(70); time.sleep(1.5)
        stop_motor(); time.sleep(0.4)
except KeyboardInterrupt:
    pwm.stop()
    GPIO.cleanup()

Addressable LED Pattern

A rotating rainbow across a NeoPixel/WS2812 strip — swap the pattern math for your own.

lightingembedded
View skeleton script
#include <FastLED.h>

// ── CONFIG ────────────────────────────────
#define LED_PIN     6      // TODO: your data pin
#define NUM_LEDS    30
#define BRIGHTNESS  80

CRGB leds[NUM_LEDS];

void setup() {
  FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS);
  FastLED.setBrightness(BRIGHTNESS);
}

void loop() {
  // TODO: replace with your real pattern
  static uint8_t hue = 0;
  for (int i = 0; i < NUM_LEDS; i++) {
    leds[i] = CHSV(hue + (i * 256 / NUM_LEDS), 255, 255);
  }
  FastLED.show();
  hue++;
  delay(20);
}
import board
import neopixel
import time

# ── CONFIG ────────────────────────────────
LED_PIN = board.D6
NUM_LEDS = 30
BRIGHTNESS = 0.3

pixels = neopixel.NeoPixel(LED_PIN, NUM_LEDS, brightness=BRIGHTNESS, auto_write=False)

def wheel(pos):
    if pos < 85:
        return (255 - pos * 3, pos * 3, 0)
    elif pos < 170:
        pos -= 85
        return (0, 255 - pos * 3, pos * 3)
    else:
        pos -= 170
        return (pos * 3, 0, 255 - pos * 3)

hue = 0
while True:
    # TODO: replace with your real pattern
    for i in range(NUM_LEDS):
        pixel_index = (i * 256 // NUM_LEDS) + hue
        pixels[i] = wheel(pixel_index & 255)
    pixels.show()
    hue = (hue + 1) % 256
    time.sleep(0.02)

Temperature & Humidity Sensor Reader

Poll a DHT22 on an interval and hand off clean readings, with bad-read retries baked in.

sensorsembedded
View skeleton script
#include <DHT.h>

// ── CONFIG ────────────────────────────────
#define DHT_PIN     2        // TODO: your data pin
#define DHT_TYPE    DHT22
const unsigned long READ_INTERVAL = 2000;  // ms, DHT22 min ~2s

DHT dht(DHT_PIN, DHT_TYPE);
unsigned long lastRead = 0;

void setup() {
  Serial.begin(9600);
  dht.begin();
}

void loop() {
  if (millis() - lastRead >= READ_INTERVAL) {
    lastRead = millis();
    readSensor();
  }
  // TODO: replace the Serial print with your real logic
}

void readSensor() {
  float humidity = dht.readHumidity();
  float tempC = dht.readTemperature();

  if (isnan(humidity) || isnan(tempC)) {
    Serial.println("Read failed, will retry next interval");
    return;
  }

  Serial.print("Temp: "); Serial.print(tempC);
  Serial.print("C  Humidity: "); Serial.print(humidity); Serial.println("%");
}
import time
import board
import adafruit_dht

# ── CONFIG ────────────────────────────────
DHT_PIN = board.D2       # TODO: your data pin
READ_INTERVAL = 2.0      # seconds, DHT22 min ~2s

dht = adafruit_dht.DHT22(DHT_PIN)

def read_sensor():
    try:
        temp_c = dht.temperature
        humidity = dht.humidity
        print(f"Temp: {temp_c}C  Humidity: {humidity}%")
        return temp_c, humidity
    except RuntimeError as e:
        # DHT sensors drop reads often — this is normal, just retry
        print(f"Read failed ({e.args[0]}), will retry next interval")
        return None, None

while True:
    read_sensor()   # TODO: replace with your real logic
    time.sleep(READ_INTERVAL)

Button Input & Debounce

Clean single-press detection from a noisy mechanical switch — no phantom double-fires.

inputembedded
View skeleton script
// ── CONFIG ────────────────────────────────
const int BUTTON_PIN = 2;         // TODO: your input pin
const unsigned long DEBOUNCE_MS = 40;

int lastReading = HIGH;
int stableState = HIGH;
unsigned long lastChangeTime = 0;

void setup() {
  Serial.begin(9600);
  pinMode(BUTTON_PIN, INPUT_PULLUP);   // wired to GND when pressed
}

void loop() {
  int reading = digitalRead(BUTTON_PIN);

  if (reading != lastReading) {
    lastChangeTime = millis();
  }

  if (millis() - lastChangeTime > DEBOUNCE_MS) {
    if (reading != stableState) {
      stableState = reading;
      if (stableState == LOW) {
        onPress();   // TODO: replace with your real logic
      }
    }
  }

  lastReading = reading;
}

void onPress() {
  Serial.println("Button pressed");
}
import RPi.GPIO as GPIO
import time

# ── CONFIG ────────────────────────────────
BUTTON_PIN = 17     # TODO: your input pin
DEBOUNCE_MS = 200

def on_press(channel):
    # TODO: replace with your real logic
    print("Button pressed")

GPIO.setmode(GPIO.BCM)
GPIO.setup(BUTTON_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.add_event_detect(BUTTON_PIN, GPIO.FALLING,
                       callback=on_press, bouncetime=DEBOUNCE_MS)

try:
    while True:
        time.sleep(1)
except KeyboardInterrupt:
    GPIO.cleanup()

Web App Starter, No Build Step

State, render, and event handling in one file — open it and go.

web appno build
View skeleton script
<!-- index.html — open directly in a browser, no server needed -->
<div id="app"></div>
<script>
// ── STATE ─────────────────────────────────
const state = { items: [] };   // TODO: shape this to your data

// ── RENDER ────────────────────────────────
function render() {
  const app = document.getElementById('app');
  app.innerHTML = `
    <h1>My App</h1>
    <button data-action="add">Add item</button>
    <ul>
      ${state.items.map((item, i) =>
        `<li>${item} <button data-action="remove" data-index="${i}">x</button></li>`
      ).join('')}
    </ul>
  `;
}

// ── EVENTS (delegated — survives re-renders) ─
document.getElementById('app').addEventListener('click', (e) => {
  const action = e.target.dataset.action;
  if (action === 'add') {
    state.items.push(`Item ${state.items.length + 1}`);
    render();
  }
  if (action === 'remove') {
    state.items.splice(Number(e.target.dataset.index), 1);
    render();
  }
});

render();
</script>
from flask import Flask, render_template_string, request, jsonify

app = Flask(__name__)

# ── STATE ─────────────────────────────────
state = {"items": []}   # TODO: shape this to your data

PAGE = """
<h1>My App</h1>
<button onclick="addItem()">Add item</button>
<ul id="list"></ul>
<script>
async function refresh() {
  const res = await fetch('/items');
  const data = await res.json();
  document.getElementById('list').innerHTML =
    data.items.map((item, i) => `<li>${item} <button onclick="removeItem(${i})">x</button></li>`).join('');
}
async function addItem() { await fetch('/items', { method: 'POST' }); refresh(); }
async function removeItem(i) { await fetch(`/items/${i}`, { method: 'DELETE' }); refresh(); }
refresh();
</script>
"""

@app.route("/")
def index():
    return render_template_string(PAGE)

@app.route("/items", methods=["GET", "POST"])
def items():
    if request.method == "POST":
        state["items"].append(f"Item {len(state['items']) + 1}")
    return jsonify(state)

@app.route("/items/<int:i>", methods=["DELETE"])
def remove_item(i):
    if 0 <= i < len(state["items"]):
        state["items"].pop(i)
    return jsonify(state)

if __name__ == "__main__":
    app.run(debug=True)

Debounced Live Search / Filter

Filters a list as the user types without hammering re-renders on every keystroke.

web appno build
View skeleton script
<!-- index.html — open directly in a browser, no server needed -->
<input id="search" placeholder="Search..." />
<ul id="results"></ul>
<script>
// ── DATA ──────────────────────────────────
const items = ["Apple", "Banana", "Cherry"];   // TODO: your real data

// ── DEBOUNCE HELPER ───────────────────────
function debounce(fn, delay) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}

function render(list) {
  document.getElementById('results').innerHTML =
    list.map(item => `<li>${item}</li>`).join('');
}

const onSearch = debounce((query) => {
  const filtered = items.filter(i =>
    i.toLowerCase().includes(query.toLowerCase())
  );
  render(filtered);   // TODO: replace with your real filtering logic
}, 200);

document.getElementById('search').addEventListener('input', (e) => {
  onSearch(e.target.value);
});

render(items);
</script>
from flask import Flask, render_template_string, request, jsonify

app = Flask(__name__)

# ── DATA ──────────────────────────────────
ITEMS = ["Apple", "Banana", "Cherry"]   # TODO: your real data

PAGE = """
<input id="search" placeholder="Search..." />
<ul id="results"></ul>
<script>
let timer;
document.getElementById('search').addEventListener('input', (e) => {
  clearTimeout(timer);
  timer = setTimeout(() => search(e.target.value), 200);
});
async function search(query) {
  const res = await fetch(`/search?q=${encodeURIComponent(query)}`);
  const data = await res.json();
  document.getElementById('results').innerHTML =
    data.items.map(i => `<li>${i}</li>`).join('');
}
search('');
</script>
"""

@app.route("/")
def index():
    return render_template_string(PAGE)

@app.route("/search")
def search():
    query = request.args.get("q", "").lower()
    matches = [i for i in ITEMS if query in i.lower()]   # TODO: replace with your real logic
    return jsonify({"items": matches})

if __name__ == "__main__":
    app.run(debug=True)

API Data Fetcher — Loading & Error States

The three-state fetch pattern every UI needs: loading, error, and success, handled once.

web appAPI
View skeleton script
<!-- index.html — open directly in a browser, no server needed -->
<div id="app"></div>
<script>
// ── CONFIG ────────────────────────────────
const API_URL = "https://api.example.com/data";   // TODO: your endpoint

const state = { status: "loading", data: null, error: null };

function render() {
  const app = document.getElementById('app');
  if (state.status === "loading") {
    app.innerHTML = "<p>Loading...</p>";
  } else if (state.status === "error") {
    app.innerHTML = `<p>Something went wrong: ${state.error}</p>`;
  } else {
    // TODO: replace with your real rendering
    app.innerHTML = `<pre>${JSON.stringify(state.data, null, 2)}</pre>`;
  }
}

async function load() {
  state.status = "loading";
  render();
  try {
    const res = await fetch(API_URL);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    state.data = await res.json();
    state.status = "success";
  } catch (err) {
    state.error = err.message;
    state.status = "error";
  }
  render();
}

load();
</script>
import requests
import time

# ── CONFIG ────────────────────────────────
API_URL = "https://api.example.com/data"   # TODO: your endpoint
MAX_RETRIES = 3
RETRY_DELAY = 1.5   # seconds

def load(url):
    last_error = None
    for attempt in range(1, MAX_RETRIES + 1):
        try:
            resp = requests.get(url, timeout=5)
            resp.raise_for_status()
            return resp.json()
        except requests.RequestException as e:
            last_error = e
            print(f"Attempt {attempt} failed: {e}")
            time.sleep(RETRY_DELAY)
    raise RuntimeError(f"Giving up after {MAX_RETRIES} attempts: {last_error}")

if __name__ == "__main__":
    try:
        data = load(API_URL)
        print(data)   # TODO: replace with your real handling
    except RuntimeError as e:
        print(f"Error: {e}")

REST API Starter

Basic CRUD endpoints wired up and ready to point at a real data store.

backendAPI
View skeleton script
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()

# ── MODEL ─────────────────────────────────
class Item(BaseModel):
    name: str          # TODO: shape this to your data

db: list[Item] = []

@app.get("/items")
def list_items():
    return db

@app.post("/items")
def create_item(item: Item):
    db.append(item)
    return item

@app.get("/items/{item_id}")
def get_item(item_id: int):
    if item_id >= len(db):
        raise HTTPException(status_code=404, detail="Not found")
    return db[item_id]

@app.delete("/items/{item_id}")
def delete_item(item_id: int):
    if item_id >= len(db):
        raise HTTPException(status_code=404, detail="Not found")
    return db.pop(item_id)

# Run with: uvicorn main:app --reload
const express = require('express');
const app = express();
app.use(express.json());

// ── STATE ─────────────────────────────────
let items = [];   // TODO: shape this to your data

app.get('/items', (req, res) => {
  res.json(items);
});

app.post('/items', (req, res) => {
  const item = req.body;   // TODO: validate
  items.push(item);
  res.status(201).json(item);
});

app.get('/items/:id', (req, res) => {
  const item = items[req.params.id];
  if (!item) return res.status(404).json({ error: 'Not found' });
  res.json(item);
});

app.delete('/items/:id', (req, res) => {
  if (!items[req.params.id]) return res.status(404).json({ error: 'Not found' });
  const [removed] = items.splice(req.params.id, 1);
  res.json(removed);
});

app.listen(3000, () => console.log('Listening on :3000'));

Realtime WebSocket Server

Broadcasts every message to all connected clients — the base for chat, multiplayer, or live dashboards.

backendrealtime
View skeleton script
import asyncio
import websockets

# ── STATE ─────────────────────────────────
clients = set()

async def handler(websocket):
    clients.add(websocket)
    try:
        async for message in websocket:
            # TODO: replace with your real message handling
            for client in clients:
                if client != websocket:
                    await client.send(message)
    finally:
        clients.remove(websocket)

async def main():
    async with websockets.serve(handler, "0.0.0.0", 8765):
        await asyncio.Future()

if __name__ == "__main__":
    asyncio.run(main())
const { WebSocketServer } = require('ws');
const wss = new WebSocketServer({ port: 8765 });

// ── STATE ─────────────────────────────────
const clients = new Set();

wss.on('connection', (ws) => {
  clients.add(ws);

  ws.on('message', (data) => {
    // TODO: replace with your real message handling
    for (const client of clients) {
      if (client !== ws) client.send(data.toString());
    }
  });

  ws.on('close', () => clients.delete(ws));
});

console.log('WebSocket server on :8765');

SQLite Persistence Layer

A tiny data layer with a real table and connection handling, so you stop losing state on restart.

backenddatabase
View skeleton script
import sqlite3

# ── CONFIG ────────────────────────────────
DB_PATH = "app.db"   # TODO: your database file

def get_connection():
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row
    return conn

def init_db():
    with get_connection() as conn:
        conn.execute("""
            CREATE TABLE IF NOT EXISTS items (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                name TEXT NOT NULL
            )
        """)   # TODO: shape this to your data

def add_item(name):
    with get_connection() as conn:
        cur = conn.execute("INSERT INTO items (name) VALUES (?)", (name,))
        return cur.lastrowid

def list_items():
    with get_connection() as conn:
        rows = conn.execute("SELECT * FROM items").fetchall()
        return [dict(row) for row in rows]

def delete_item(item_id):
    with get_connection() as conn:
        conn.execute("DELETE FROM items WHERE id = ?", (item_id,))

if __name__ == "__main__":
    init_db()
    add_item("Example")   # TODO: replace with your real logic
    print(list_items())
const Database = require('better-sqlite3');

// ── CONFIG ────────────────────────────────
const DB_PATH = 'app.db';   // TODO: your database file

const db = new Database(DB_PATH);

function initDb() {
  db.exec(`
    CREATE TABLE IF NOT EXISTS items (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      name TEXT NOT NULL
    )
  `);   // TODO: shape this to your data
}

function addItem(name) {
  const stmt = db.prepare('INSERT INTO items (name) VALUES (?)');
  const info = stmt.run(name);
  return info.lastInsertRowid;
}

function listItems() {
  return db.prepare('SELECT * FROM items').all();
}

function deleteItem(id) {
  db.prepare('DELETE FROM items WHERE id = ?').run(id);
}

initDb();
addItem('Example');   // TODO: replace with your real logic
console.log(listItems());

File Watcher & Automation

Watches a folder and fires your own logic on any create, edit, or delete.

automationtooling
View skeleton script
import time
import os

# ── CONFIG ────────────────────────────────
WATCH_DIR = "./watched"     # TODO: folder to watch
POLL_SECONDS = 2

def on_change(filename, event):
    # TODO: replace with your real action
    print(f"[{event}] {filename}")

def watch():
    os.makedirs(WATCH_DIR, exist_ok=True)
    seen = {f: os.path.getmtime(os.path.join(WATCH_DIR, f))
            for f in os.listdir(WATCH_DIR)}

    while True:
        time.sleep(POLL_SECONDS)
        current = os.listdir(WATCH_DIR)

        for f in current:
            path = os.path.join(WATCH_DIR, f)
            mtime = os.path.getmtime(path)
            if f not in seen:
                on_change(f, "created")
            elif mtime != seen[f]:
                on_change(f, "modified")
            seen[f] = mtime

        for f in list(seen):
            if f not in current:
                on_change(f, "deleted")
                del seen[f]

if __name__ == "__main__":
    watch()
const fs = require('fs');
const path = require('path');

// ── CONFIG ────────────────────────────────
const WATCH_DIR = './watched';   // TODO: folder to watch

fs.mkdirSync(WATCH_DIR, { recursive: true });

function onChange(filename, eventType) {
  // TODO: replace with your real action
  console.log(`[${eventType}] ${filename}`);
}

fs.watch(WATCH_DIR, (eventType, filename) => {
  if (filename) onChange(filename, eventType);
});

console.log(`Watching ${path.resolve(WATCH_DIR)}...`);

CLI Tool Starter

Argument parsing, verbose flag, and file output already wired up.

CLItooling
View skeleton script
import argparse

def main():
    parser = argparse.ArgumentParser(description="TODO: describe your tool")
    parser.add_argument("input", help="Input file or value")
    parser.add_argument("-o", "--output", default=None, help="Output path")
    parser.add_argument("-v", "--verbose", action="store_true")
    args = parser.parse_args()

    if args.verbose:
        print(f"Processing: {args.input}")

    result = process(args.input)   # TODO: your real logic

    if args.output:
        with open(args.output, "w") as f:
            f.write(result)
    else:
        print(result)

def process(value):
    return value.upper()   # TODO: replace

if __name__ == "__main__":
    main()
#!/usr/bin/env node
const args = process.argv.slice(2);

// ── PARSE ─────────────────────────────────
const input = args[0];
const verbose = args.includes('-v') || args.includes('--verbose');
const outIndex = args.findIndex(a => a === '-o' || a === '--output');
const output = outIndex !== -1 ? args[outIndex + 1] : null;

if (!input) {
  console.error('Usage: mytool <input> [-o output] [-v]');
  process.exit(1);
}

if (verbose) console.error(`Processing: ${input}`);

const result = transform(input);   // TODO: your real logic

if (output) {
  require('fs').writeFileSync(output, result);
} else {
  console.log(result);
}

function transform(value) {
  return value.toUpperCase();   // TODO: replace
}

Scheduled Task Runner

Runs jobs on a recurring schedule without cron or a task scheduler service.

automationtooling
View skeleton script
import time
import schedule   # pip install schedule

# ── JOBS ──────────────────────────────────
def job():
    # TODO: replace with your real task
    print("Running scheduled job...")

# ── SCHEDULE ──────────────────────────────
schedule.every(1).hours.do(job)          # TODO: adjust interval
schedule.every().day.at("09:00").do(job) # TODO: or use a fixed time

if __name__ == "__main__":
    print("Scheduler started, waiting for jobs...")
    while True:
        schedule.run_pending()
        time.sleep(1)
const cron = require('node-cron');   // npm install node-cron

// ── JOBS ──────────────────────────────────
function job() {
  // TODO: replace with your real task
  console.log('Running scheduled job...');
}

// ── SCHEDULE ──────────────────────────────
cron.schedule('0 * * * *', job);     // TODO: adjust — this runs hourly
cron.schedule('0 9 * * *', job);     // TODO: or a fixed daily time

console.log('Scheduler started, waiting for jobs...');