vhs-search
Search VHS Berlin courses using natural language. Converts queries like 'Find A2 German evening courses in Neukölln' into structured VHS searches, extracts results, and presents them in a clean format. Use when the user wants to find specific VHS Berlin courses.
What this skill does
# VHS Berlin Course Search
Convert natural language queries into VHS Berlin course searches, extract structured results, and present them cleanly.
## Workflow
### Step 1: Parse the User Query
Extract structured parameters from the natural language query:
**Location/District**: Mitte, Pankow, Neukölln, Tempelhof-Schöneberg, Charlottenburg-Wilmersdorf, Spandau, Steglitz-Zehlendorf, Treptow-Köpenick, Marzahn-Hellersdorf, Lichtenberg, Reinickendorf, Friedrichshain-Kreuzberg
**Category/Topic**: German language, integration courses, pottery, art, cooking, career, health, languages (Spanish, English, French, etc.), digital skills, crafts, music, dance, etc.
**Time**: morning, afternoon, evening, weekend, weekdays, specific day (Monday, Tuesday, etc.)
**Level** (for language courses): A1, A2, B1, B2, C1, C2
**Format**: online, in-person, hybrid
**Price range**: under €X, between €X and €Y
**Start date**: next month, this month, specific date range
**Course ID** (if user provides one): direct course lookup
### Step 2: Build Search Query
Check `config/query-registry.yaml` for known URL patterns and parameters.
**Priority order:**
1. **Direct course ID** → use `id=` parameter if available
2. **District + location** → use `direkt=1&bezirk=...&lehrstaette=...` if validated
3. **Keyword search** → use `stichw=` if pattern exists
4. **Browser-driven search** → navigate to VHS search page and fill form
Load `config/query-registry.yaml`:
```yaml
base_url: https://vhsit.berlin.de
patterns:
direct_course:
template: "{{base_url}}/VHSKURSE/...?id={{course_id}}"
confidence: verified
tested_at: "2026-04-18"
district_location:
template: "{{base_url}}/VHSKURSE/...?direkt=1&bezirk={{district_code}}&lehrstaette={{location_code}}"
confidence: likely
tested_at: "2026-04-18"
keyword_search:
template: "{{base_url}}/VHSKURSE/...?stichw={{keyword}}"
confidence: observed
tested_at: null
note: "Seen in public snippets, not fully validated"
districts:
mitte: {code: "01", variants: ["Mitte", "Berlin-Mitte"]}
pankow: {code: "03", variants: ["Pankow"]}
neukoelln: {code: "08", variants: ["Neukölln", "Neukoelln"]}
# ... (complete list in actual file)
```
### Step 3: Execute Search
**Before fetching any course URL**, check qurl first:
```bash
qurl get "<vhs_course_url>"
```
- Exit 0: use cached course data, skip fetch. Course pages are stable for weeks.
- Exit 1: fetch the URL, then index:
```bash
echo "<fetched_content>" | qurl add "<vhs_course_url>" --source vhs-berlin \
--tags "<level>,<district>,<category>"
```
To search cached courses: `qurl query "A2 German Mitte evening" --source vhs-berlin`
#### Option A: URL-driven (preferred)
```bash
curl -s "{{constructed_url}}" | extract_courses
```
#### Option B: Browser-driven (fallback)
Use Browser MCP to:
1. Navigate to VHS search page
2. Detect page type from `config/page-map.yaml`
3. Fill search form
4. Extract results
Load `config/page-map.yaml`:
```yaml
pages:
search_form:
url_pattern: "vhsit.berlin.de/VHSKURSE.*suche"
identifier: "form.search-form"
fields:
keyword: "input[name='stichw']"
district: "select[name='bezirk']"
category: "select[name='kategorie']"
submit: "button[type='submit']"
verified_at: "2026-04-18"
results_list:
url_pattern: "vhsit.berlin.de/VHSKURSE.*ergebnisse"
identifier: ".course-list"
item_selector: ".course-item"
fields:
title: "h3.course-title"
course_id: "a.course-link@href"
district: ".course-district"
location: ".course-location"
dates: ".course-dates"
price: ".course-price"
status: ".booking-status"
pagination: ".pagination a.next"
verified_at: "2026-04-18"
course_detail:
url_pattern: "vhsit.berlin.de/VHSKURSE.*id="
identifier: ".course-detail"
# ... (extraction rules for detail page)
```
### Step 4: Extract and Normalize Results
Use `scripts/normalize-course.js` to structure the data:
```javascript
{
source_url: "https://vhsit.berlin.de/...",
source_course_id: "VHS-12345",
title: "Deutsch als Fremdsprache A2.2",
district: "Mitte",
location: "VHS Linienstraße",
category: "Sprachen > Deutsch",
level: "A2",
start_date: "2026-05-15",
end_date: "2026-07-20",
schedule_text: "Mo+Mi 18:00-20:15",
price_text: "€135",
booking_status: "Anmeldung möglich",
special_rules: null, // or "Beratung erforderlich"
extracted_at: "2026-04-18T17:30:00Z",
raw_hash: "sha256..."
}
```
### Step 5: Check for Special Registration Rules
If the course category matches known consultation-required patterns, surface the rule:
**Integration courses**: "Registration only possible after personal consultation and assessment"
**German language courses** (certain types): May require placement test
Check `config/extraction-rules.yaml` for rule patterns.
### Step 6: Store Results (Optional)
If the query seems reusable or the user wants to watch it, offer to save:
```sql
INSERT INTO courses (...) VALUES (...);
INSERT INTO watched_searches (label, query_type, query_payload, created_at)
VALUES ('A2 German evening Neukölln', 'natural_language', '{"text": "..."}', datetime('now'));
```
Use SQLite MCP:
```
mcp__sqlite__write_query("INSERT INTO courses ...")
mcp__sqlite__create_table("CREATE TABLE IF NOT EXISTS ...") # on first use
```
### Step 7: Present Results
Return a clean summary:
```
## Found 5 matching courses:
### Deutsch als Fremdsprache A2.2 — Mitte
- **Where**: VHS Linienstraße
- **When**: May 15 – Jul 20, Mon+Wed 18:00-20:15
- **Price**: €135
- **Status**: Anmeldung möglich
- **Link**: [View details](https://vhsit.berlin.de/VHSKURSE/...)
### Deutsch Intensivkurs A2 — Neukölln
- **Where**: VHS Neukölln, Boddinstraße
- **When**: May 20 – Jun 28, Mon-Fri 09:00-12:15
- **Price**: €210
- **Status**: Warteliste
- **Link**: [View details](...)
---
**Want to watch this search for changes?** Let me know and I'll save it as a watchlist item.
```
### Step 8: Handle Errors Gracefully
If selectors fail or the page structure changed:
```
⚠️ Some course details could not be verified from the live page.
The VHS website structure may have changed.
I recommend opening the live search page for confirmation:
[Open VHS search](https://vhsit.berlin.de/...)
**What I could extract:**
- Found 3 course titles
- Locations and dates incomplete
- Booking status unavailable
```
---
## Tips
- **URL-first**: Try direct URLs before browser automation
- **Page map is your friend**: Always check `page-map.yaml` before scraping
- **Confidence levels**: Mark query params as verified / likely / observed
- **Selector fallbacks**: If primary selector fails, try secondary
- **Timestamps**: Always record `extracted_at` for freshness tracking
- **Special rules**: Surface consultation-required courses early
## See Also
- `vhs-watch` skill for monitoring saved searches
- `vhs-digest` skill for awareness summaries
- `config/query-registry.yaml` for URL patterns
- `config/page-map.yaml` for page structure
- `config/extraction-rules.yaml` for special rules
Related in General
modeling-omnistudio-epc-catalog
IncludedSalesforce Industries CME EPC product-modeling skill for Product2-based catalog creation. Use when creating EPC products, configuring product attributes, building offer bundles with Product Child Items, or reviewing EPC DataPack JSON metadata for product catalog changes. TRIGGER when: user creates or updates Product2 EPC records, AttributeAssignment payloads, AttributeMetadata/AttributeDefaultValues, Offer bundles, or ProductChildItem relationships. DO NOT TRIGGER when: designing OmniScripts/FlexCards/Integration Procedures (use building-omnistudio-omniscript, building-omnistudio-flexcard, or building-omnistudio-integration-procedure), implementing Apex business logic (use generating-apex), or troubleshooting deployment pipelines (use deploying-metadata).
relationship-science-coach
IncludedUse this skill for direct, practical adult relationship coaching: couples conflict, repair, trust, marriage, dating, flirting, attachment patterns, emotional connection, sex, desire differences, eroticism, kink negotiation, affection, love languages, breakups, and long-term passion. Draw on Gottman, EFT and Hold Me Tight, attachment science, modern sex research, Perel, Nagoski, Kerner, Schnarch, Love and Stosny, and flexible love-language tools. Be concrete and low-hedge. Redirect only for imminent danger, abuse, coercive control, minors, non-consent, self-harm, stalking, or medical/legal/psychiatric decisions.
building-sf-integrations
IncludedSalesforce integration architecture and runtime plumbing with 120-point scoring. Use this skill to set up Named Credentials, External Credentials, External Services, REST/SOAP callout patterns, Platform Events, and Change Data Capture. TRIGGER when: user sets up Named Credentials, External Services, REST/SOAP callouts, Platform Events, CDC, or touches .namedCredential-meta.xml files. DO NOT TRIGGER when: Connected App/OAuth config (use configuring-connected-apps), Apex-only logic (use generating-apex), or data import/export (use handling-sf-data).
venue-templates
IncludedAccess comprehensive LaTeX templates, formatting requirements, and submission guidelines for major scientific publication venues (Nature, Science, PLOS, IEEE, ACM), academic conferences (NeurIPS, ICML, CVPR, CHI), research posters, and grant proposals (NSF, NIH, DOE, DARPA). This skill should be used when preparing manuscripts for journal submission, conference papers, research posters, or grant proposals and need venue-specific formatting requirements and templates.
let-fate-decide
IncludedDraws the 12 Houses of the Zodiac Tarot spread to inject entropy into planning when prompts are vague, ambiguous, or casually delegated. Interprets the spread to guide next steps. Use when the user says 'let fate decide', 'YOLO', 'whatever', 'idk', or other nonchalant phrases, makes Yu-Gi-Oh references, or when you are about to arbitrarily pick between multiple reasonable approaches. Prefer over ask-questions-if-underspecified when the user's tone is casual or playful rather than precision-seeking.
net-ops
IncludedCross-platform network troubleshooting (Windows, macOS, Linux) via local or remote shell. Use for: DNS broken, can't resolve hostnames, nslookup/dig works but apps fail, NRPT, WFP, scutil, /etc/resolver, systemd-resolved, /etc/resolv.conf, NetworkManager, VPN DNS leak residue (ProtonVPN/Mullvad/WireGuard/AnyConnect), AV/firewall blocking DNS or DoH, Tailscale DNS interaction, intermittent connectivity, remote diagnostics over SSH.