Sign inTry it

Find Swiss businesses by trade and place on local.ch.

Searches local.ch, the Swiss business and phone directory, for a trade in a place: business name, categories, address, public phone number, map position, whether it is open now, and a link to the entry, 20 per page with the total number of matches. The trade must be a category local.ch knows; free-text terms go to its full-text search, which this recipe does not use (the run then fails with a redirect).

No reports yetRead-only, no login
Site
local.ch, Switzerland
Version
v1
Runs, last 7 days
No reports yet
sha256
b3a80b585bfd…

Run it with your agent

after installing Wield

Ask in plain words. Your agent finds this recipe and calls it:

“Find Swiss businesses by trade and place on local.ch.”

Read-only: it changes nothing on the site. No login needed.

{
  "tool": "run",
  "arguments": {
    "recipe_id": "local.ch/search-businesses",
    "params": {
      "what": "zahnarzt",
      "where": "zürich"
    }
  }
}

Requests

sent from your browser, in order
  1. 1GET/de/q/{{f.where | urlencode}}/{{f.what | urlencode}}local.ch's search page /de/q/<place>/<what> with umlauts written ae/oe/ue (the form's /de/q?what=…&where=… answers 301 and then 308 to that path, and those redirects can add an rid parameter; calling the final path directly avoids them; places are written as the site's slugs, e.g. 'st. gallen' → st-gallen). Its ld+json lists the 20 businesses; the cards add categories and the open/closed badge.repeats, max 1

Parameters

from your agent
whatstringrequiredTrade or business name in German, e.g. 'zahnarzt', 'elektriker', 'coiffeur'
wherestringrequiredSwiss place, e.g. 'zürich', 'bern', 'basel', 'luzern'

Returns

to your agent
businessesobject[]address, categories, latitude, longitude, name, open_now, opening, phone, url
totalinteger
whatstring
wherestring

Where it can send

  • www.local.ch

Anything else is blocked by the extension.

Reads from your session

Nothing. Your browser attaches the site's cookies by itself.

Used only in requests to the addresses on the left. Never sent to wield.run or your agent.

Checks

a run passes when
  • length(output.businesses) == `20`
  • output.total > `100`
  • length(output.businesses[?starts_with(url, 'https://www.local.ch/de/d/')]) == length(output.businesses)
  • length(output.businesses[?phone != `null`]) > `15`
  • length(output.businesses[?contains(address, 'Zürich')]) > `15`
  • length(output.businesses[?categories != `null`]) == length(output.businesses)

Raw recipe

Served as canonical JSON from /api/v1/recipes/local.ch/search-businesses, so its sha256 matches the ETag and the index.

Check it yourself
curl -s https://wield.run/api/v1/recipes/local.ch/search-businesses | shasum -a 256
# b3a80b585bfdf51d303430d283cb9f0ab8ba36a3ca4468b276cc3cf0e0052a76
local.ch/search-businesses.json, v1
{
  "description": "Searches local.ch, the Swiss business and phone directory, for a trade in a place: business name, categories, address, public phone number, map position, whether it is open now, and a link to the entry, 20 per page with the total number of matches. The trade must be a category local.ch knows; free-text terms go to its full-text search, which this recipe does not use (the run then fails with a redirect).",
  "effect": "read",
  "id": "local.ch/search-businesses",
  "keywords": [
    "local.ch",
    "telefonbuch",
    "branchen",
    "firmen",
    "adresse",
    "telefonnummer",
    "öffnungszeiten",
    "zahnarzt",
    "schweiz",
    "zürich",
    "bern",
    "annuaire",
    "local business",
    "directory",
    "switzerland"
  ],
  "output": {
    "schema": {
      "properties": {
        "businesses": {
          "items": {
            "properties": {
              "address": {
                "description": "Street, postcode and place",
                "type": [
                  "string",
                  "null"
                ]
              },
              "categories": {
                "description": "e.g. 'Zahnarzt • Dentalhygiene'",
                "type": [
                  "string",
                  "null"
                ]
              },
              "latitude": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "longitude": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "name": {
                "type": "string"
              },
              "open_now": {
                "description": "From the card badge; null when the card shows none",
                "type": [
                  "boolean",
                  "null"
                ]
              },
              "opening": {
                "description": "Badge text, e.g. 'Geschlossen', 'Geöffnet'",
                "type": [
                  "string",
                  "null"
                ]
              },
              "phone": {
                "description": "The business's public number, international format",
                "type": [
                  "string",
                  "null"
                ]
              },
              "url": {
                "type": "string"
              }
            },
            "type": "object"
          },
          "type": "array"
        },
        "total": {
          "type": [
            "integer",
            "null"
          ]
        },
        "what": {
          "type": "string"
        },
        "where": {
          "type": "string"
        }
      },
      "required": [
        "what",
        "where",
        "businesses"
      ],
      "type": "object"
    },
    "value": "let $s = steps.search[0], $cards = steps.search[0].cards || `[]` in {what: params.what, where: params.where, total: ($s.count && to_number(replace(split($s.count, ' ')[0], '\\'', ''))) || `0`, businesses: map(&(let $p = @ in let $c = $cards[?join('', ['https://www.local.ch', href]) == $p.url] | [0] in {name: $p.name, categories: $c.categories || `null`, address: join(', ', [$p.street || '', join(' ', [$p.zip || '', $p.city || ''])]), phone: $p.phone || `null`, open_now: (`[[\"xenon-theme__danger\", false], [\"xenon-theme__success\", true]]`[?contains($c.badge_class || '', @[0])] | [0][1]), opening: $c.badge || `null`, latitude: ($p.lat && to_number($p.lat)) || `null`, longitude: ($p.lon && to_number($p.lon)) || `null`, url: $p.url}), $s.places || `[]`)}"
  },
  "params": {
    "additionalProperties": false,
    "properties": {
      "what": {
        "description": "Trade or business name in German, e.g. 'zahnarzt', 'elektriker', 'coiffeur'",
        "maxLength": 60,
        "minLength": 2,
        "type": "string"
      },
      "where": {
        "description": "Swiss place, e.g. 'zürich', 'bern', 'basel', 'luzern'",
        "maxLength": 60,
        "minLength": 2,
        "type": "string"
      }
    },
    "required": [
      "what",
      "where"
    ],
    "type": "object"
  },
  "provenance": {
    "method": "browser-inspect",
    "notes": "The search form's /de/q?what=zahnarzt&where=zürich answers 301 to /de/q/<where>/<what> and then 308 to the transliterated path (zürich → zuerich); some entry URLs also 307 to add an rid parameter. The recipe calls /de/q/<where>/<what> with ae/oe/ue directly, which answers 200. The page's ld+json has an ItemList of 20 LocalBusiness entries (name, url, address, telephone, geo). The cards (article.xenon-entry-card, rendered twice for two layouts, so the first 20 are taken) add the categories line (h2 + span) and a badge whose theme class is xenon-theme__danger when closed; cards are joined to the ld+json by URL. The total is the text after the h1 ('406 Ergebnisse'). A term that is not a category answers 308 to /de/s/<place>/<term>, the full-text search robots.txt disallows; the executor does not follow it, so such a run fails. No card was labelled as an ad or premium entry. Ratings are drawn as stars without text and are left out.",
    "recorded_at": "2026-11-07T22:37:00+01:00",
    "recorded_by": "C4"
  },
  "requires_login": false,
  "schema": "https://wield.run/schema/recipe/v1.json",
  "site": {
    "country": "CH",
    "id": "local.ch",
    "language": "de",
    "name": "local.ch",
    "origin": "https://www.local.ch",
    "origins": [
      "https://www.local.ch"
    ]
  },
  "steps": [
    {
      "description": "local.ch's search page /de/q/<place>/<what> with umlauts written ae/oe/ue (the form's /de/q?what=…&where=… answers 301 and then 308 to that path, and those redirects can add an rid parameter; calling the final path directly avoids them; places are written as the site's slugs, e.g. 'st. gallen' → st-gallen). Its ld+json lists the 20 businesses; the cards add categories and the open/closed badge.",
      "expect": {
        "content_type": "html",
        "status": [
          200
        ]
      },
      "extract": {
        "embedded_json": {
          "required": false,
          "selector": "script[type=\"application/ld+json\"]",
          "vars": {
            "places": "[?\"@type\" == 'ItemList'] | [0].itemListElement[].{name: name, url: url, street: address.streetAddress, zip: address.postalCode, city: address.addressLocality, phone: telephone, lat: geo.latitude, lon: geo.longitude}"
          }
        },
        "html": {
          "count": {
            "selector": "h1 + div"
          }
        },
        "html_items": {
          "cards": {
            "fields": {
              "badge": {
                "selector": ".xenon-badge"
              },
              "badge_class": {
                "attr": "class",
                "selector": ".xenon-badge"
              },
              "categories": {
                "selector": "h2 + span"
              },
              "href": {
                "attr": "href",
                "selector": "a"
              }
            },
            "max": 20,
            "selector": "article.xenon-entry-card"
          }
        }
      },
      "foreach": {
        "as": "f",
        "in": "[{where: replace(replace(replace(replace(replace(replace(replace(lower(trim(params.where)), 'ä', 'ae'), 'ö', 'oe'), 'ü', 'ue'), 'é', 'e'), 'è', 'e'), '.', ''), ' ', '-'), what: replace(replace(replace(replace(replace(replace(replace(lower(trim(params.what)), 'ä', 'ae'), 'ö', 'oe'), 'ü', 'ue'), 'é', 'e'), 'è', 'e'), '.', ''), ' ', '-')}]",
        "max": 1
      },
      "id": "search",
      "request": {
        "headers": {
          "Accept": "text/html"
        },
        "method": "GET",
        "url": "/de/q/{{f.where | urlencode}}/{{f.what | urlencode}}"
      }
    }
  ],
  "task": "Find Swiss businesses by trade and place on local.ch",
  "tos": {
    "notes": "robots.txt allows /de/q/ (it disallows /de/s/, map, print and route URLs) and has no rules for Claude agents or AI content signals. One request per call; no calls, reviews or bookings.",
    "reviewed": false
  },
  "verify": {
    "checks": [
      "length(output.businesses) == `20`",
      "output.total > `100`",
      "length(output.businesses[?starts_with(url, 'https://www.local.ch/de/d/')]) == length(output.businesses)",
      "length(output.businesses[?phone != `null`]) > `15`",
      "length(output.businesses[?contains(address, 'Zürich')]) > `15`",
      "length(output.businesses[?categories != `null`]) == length(output.businesses)"
    ],
    "example_params": {
      "what": "zahnarzt",
      "where": "zürich"
    },
    "max_ms": 4000
  },
  "version": 1
}