> ## Documentation Index
> Fetch the complete documentation index at: https://docs.meter.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Schedules

> Automate scraping with recurring jobs and change notifications

# Schedules

A **schedule** automatically runs scrape jobs at specified intervals or cron times. Schedules are perfect for monitoring websites for changes without manual intervention.

## What is a schedule?

A schedule combines:

* **A strategy**: What to extract
* **A URL**: Where to scrape
* **A timing rule**: When to scrape (interval or cron)
* **Optional webhook**: Where to send change notifications

Once created, schedules run automatically, creating jobs at the specified times.

## Creating schedules

### Interval-based schedules

Run jobs at regular intervals:

```python theme={null}
from meter_sdk import MeterClient

client = MeterClient(api_key="sk_live_...")

# Run every hour (3600 seconds)
schedule = client.create_schedule(
    strategy_id="your-strategy-uuid",
    url="https://example.com/page",
    interval_seconds=3600
)

print(f"Schedule ID: {schedule['schedule_id']}")
print(f"Next run: {schedule['next_run_at']}")
```

Common intervals:

* **15 minutes**: `900` seconds
* **1 hour**: `3600` seconds
* **6 hours**: `21600` seconds
* **Daily**: `86400` seconds

### Cron-based schedules

Use cron expressions for precise scheduling:

```python theme={null}
# Run daily at 9 AM
schedule = client.create_schedule(
    strategy_id="your-strategy-uuid",
    url="https://example.com/page",
    cron_expression="0 9 * * *"
)

# Run every weekday at 8 AM
schedule = client.create_schedule(
    strategy_id="your-strategy-uuid",
    url="https://example.com/page",
    cron_expression="0 8 * * 1-5"
)

# Run every 6 hours
schedule = client.create_schedule(
    strategy_id="your-strategy-uuid",
    url="https://example.com/page",
    cron_expression="0 */6 * * *"
)
```

<Tip>
  Use [crontab.guru](https://crontab.guru/) to build and test cron expressions.
</Tip>

## Webhooks

Receive real-time notifications when jobs complete:

```python theme={null}
schedule = client.create_schedule(
    strategy_id="your-strategy-uuid",
    url="https://example.com/products",
    interval_seconds=3600,
    webhook_url="https://your-app.com/webhooks/meter"
)
```

Meter will POST job results to your webhook URL. See the [webhooks guide](/guides/webhooks) for payload details and implementation.

### Webhook types

Meter supports two webhook formats: `standard` (full JSON payload) and `slack` (formatted for Slack incoming webhooks). The type is auto-detected from the URL — Slack URLs are automatically detected.

```python theme={null}
# Slack webhook (auto-detected from URL)
schedule = client.create_schedule(
    strategy_id="your-strategy-uuid",
    url="https://example.com/products",
    interval_seconds=3600,
    webhook_url="https://hooks.slack.com/services/T.../B.../xxx"
)
```

### Webhook metadata

Attach custom JSON metadata to every webhook payload. This is useful for routing, tagging, or identifying schedules:

```python theme={null}
schedule = client.create_schedule(
    strategy_id="your-strategy-uuid",
    url="https://example.com/products",
    interval_seconds=3600,
    webhook_url="https://your-app.com/webhooks/meter",
    webhook_metadata={"project": "price-monitor", "env": "prod"}
)
```

### Webhook secrets

Every schedule with a webhook URL gets an auto-generated secret (prefixed `whsec_`). The secret is sent in the `X-Webhook-Secret` header on every delivery, allowing you to verify requests are from Meter.

The secret is returned **once** when the schedule is created — store it securely. If compromised, regenerate it:

```python theme={null}
result = client.regenerate_webhook_secret(schedule_id)
new_secret = result["webhook_secret"]
```

See the [webhooks guide](/guides/webhooks#webhook-secrets) for verification examples.

## Pull-based change detection

Instead of webhooks, poll for changes:

```python theme={null}
# Create schedule without webhook
schedule = client.create_schedule(
    strategy_id="your-strategy-uuid",
    url="https://example.com/products",
    interval_seconds=3600
)

# Later, check for changes
changes = client.get_schedule_changes(
    schedule_id=schedule['schedule_id'],
    mark_seen=True  # Mark changes as seen after reading
)

if changes['count'] > 0:
    print(f"Found {changes['count']} jobs with changes")
    for change in changes['changes']:
        print(f"Job {change['job_id']}: {change['item_count']} items")
        # Process change['results']
```

<Tip>
  Set `mark_seen=False` to preview changes without marking them as read.
</Tip>

This is useful for:

* **Batch processing**: Check for changes once per hour, process in bulk
* **Webhook alternatives**: When webhooks aren't feasible
* **Manual review**: Preview changes before processing

## Managing schedules

### Listing schedules

```python theme={null}
schedules = client.list_schedules()

for schedule in schedules:
    print(f"Schedule {schedule['schedule_id']}:")
    print(f"  Enabled: {schedule['enabled']}")
    print(f"  Type: {schedule['schedule_type']}")  # 'interval' or 'cron'
    print(f"  Next run: {schedule['next_run_at']}")
```

### Updating schedules

```python theme={null}
# Disable a schedule temporarily
client.update_schedule(
    schedule_id,
    enabled=False
)

# Change the interval
client.update_schedule(
    schedule_id,
    interval_seconds=7200  # Every 2 hours instead
)

# Switch to cron
client.update_schedule(
    schedule_id,
    cron_expression="0 10 * * *"  # Daily at 10 AM
)

# Update webhook URL
client.update_schedule(
    schedule_id,
    webhook_url="https://your-new-domain.com/webhooks/meter"
)

# Remove webhook (use pull-based instead)
client.update_schedule(
    schedule_id,
    webhook_url=None
)
```

### Deleting schedules

```python theme={null}
# Delete a schedule (stops future jobs)
client.delete_schedule(schedule_id)
```

<Note>
  Deleting a schedule doesn't delete past jobs. Use `list_jobs()` to access historical data.
</Note>

## Monitoring schedules

### Check recent runs

```python theme={null}
# Get jobs created by this schedule
jobs = client.list_jobs(
    strategy_id=schedule['strategy_id'],
    limit=20
)

for job in jobs:
    print(f"Job {job['job_id']} ({job['created_at']}):")
    print(f"  Status: {job['status']}")
    print(f"  Items: {job['item_count']}")
```

### Detect failures

```python theme={null}
# Check for recent failures
failed_jobs = client.list_jobs(
    strategy_id=schedule['strategy_id'],
    status='failed',
    limit=5
)

if len(failed_jobs) > 0:
    print(f"Warning: {len(failed_jobs)} recent failures")
    print(f"Error: {failed_jobs[0]['error']}")
```

## Best practices

<AccordionGroup>
  <Accordion title="Choose appropriate intervals">
    Balance freshness with cost and load:

    **Every 15-30 minutes**:

    * Stock prices, sports scores
    * Time-sensitive monitoring
    * High-value data

    **Every 1-6 hours**:

    * E-commerce products
    * News articles
    * Job listings
    * Most monitoring use cases

    **Daily**:

    * Documentation, policies
    * Blog posts
    * Low-frequency content
  </Accordion>

  <Accordion title="Use webhooks for real-time updates">
    Webhooks are ideal when:

    * Changes need immediate action
    * Building real-time systems
    * Triggering downstream workflows

    Pull-based is better when:

    * Batch processing changes
    * Webhooks aren't feasible (firewall, no public endpoint)
    * Manual review before processing
  </Accordion>

  <Accordion title="Monitor schedule health">
    Set up alerts for schedule failures:

    ```python theme={null}
    import time

    def check_schedule_health(schedule_id, threshold=3):
        """Alert if >threshold failures in recent jobs"""
        failed = client.list_jobs(
            strategy_id=schedule['strategy_id'],
            status='failed',
            limit=10
        )

        if len(failed) >= threshold:
            send_alert(f"Schedule {schedule_id} has {len(failed)} failures")
    ```
  </Accordion>

  <Accordion title="Pause schedules during maintenance">
    Temporarily disable schedules when doing maintenance:

    ```python theme={null}
    # Disable before maintenance
    client.update_schedule(schedule_id, enabled=False)

    # Do maintenance work
    update_strategy_or_database()

    # Re-enable after
    client.update_schedule(schedule_id, enabled=True)
    ```
  </Accordion>
</AccordionGroup>

## Change detection workflow

Schedules automatically compare jobs to detect changes:

```mermaid theme={null}
graph TD
    A[Schedule Runs Job] --> B[Extract Data]
    B --> C[Generate Content Hash]
    C --> D{Compare with Previous}
    D -->|Different Hash| E[Mark as Changed]
    D -->|Same Hash| F[Mark as Unchanged]
    E --> G[Available in Changes API]
    E --> H[Send Webhook if configured]
    F --> I[Not returned by Changes API]
```

When you call `get_schedule_changes()`, Meter returns only jobs where content actually changed.

## Troubleshooting

<AccordionGroup>
  <Accordion title="Schedule isn't running">
    **Possible causes:**

    * Schedule is disabled
    * Cron expression is incorrect
    * Server issues

    **Solutions:**

    * Check `enabled` field: `client.get_schedule(schedule_id)`
    * Verify cron expression at [crontab.guru](https://crontab.guru/)
    * Check `next_run_at` to see when it's scheduled
  </Accordion>

  <Accordion title="No changes detected">
    **Problem:** `get_schedule_changes()` returns 0 results but you expect changes

    **Possible causes:**

    * Content genuinely hasn't changed
    * Changes already marked as seen
    * Looking at wrong schedule

    **Solutions:**

    * Use `mark_seen=False` to check without marking
    * Compare jobs manually: `client.compare_jobs(job1_id, job2_id)`
    * Verify schedule ID is correct
  </Accordion>

  <Accordion title="Too many webhook failures">
    **Problem:** Webhooks aren't being received

    **Solutions:**

    * Verify webhook URL is publicly accessible
    * Check endpoint responds with 200 OK within 30 seconds
    * Test webhook with tools like [webhook.site](https://webhook.site)
    * Switch to pull-based if webhooks aren't working
  </Accordion>
</AccordionGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="Change Detection" icon="code-compare" href="/concepts/change-detection">
    Learn how Meter detects content changes
  </Card>

  <Card title="Webhooks Guide" icon="webhook" href="/guides/webhooks">
    Implement webhook endpoints for real-time updates
  </Card>

  <Card title="Pull-Based Monitoring" icon="download" href="/guides/pull-based-monitoring">
    Use the changes API for batch processing
  </Card>

  <Card title="Python SDK Reference" icon="code" href="/api-reference/python/schedules">
    Explore all schedule methods
  </Card>
</CardGroup>

## Need help?

Email me at [mckinnon@meter.sh](mailto:mckinnon@meter.sh)
