MiscSubjects

Add, edit and query graph objects from Google Sheets

One object has one stable ID, stored once. Its page, JSON endpoint, embedded cards and query results resolve that ID. A person, house, parcel, source listing, social account and post are separate objects connected by named relationships.

Execution contract

Raw execution contract · Google Sheets Apps Script · Existing object schema

Paste these six headers into A1:F1 of an empty Google Sheet tab. Each subsequent row is one REST request. E and F are results written by the script.

text
method	path	body_json	if_match	http_status	response_json
Line 1, Column 1Plain Text

In Extensions → Apps Script, paste the script from the link above, save, and reload the Sheet. Use Graph → Set my access key to enter your existing terminal key; it is stored in your user properties, not in cells. Google asks you to authorize the script on its first run. Select a request row, then Graph → Run selected request. There is no automatic on-edit action or recurring trigger. A row with an existing result must be reviewed and cleared before rerunning. Large responses are split into a response tab without losing their contents.

All requests use this origin: https://miscsubjects.com. These are the HTTP headers; the placeholders are not credentials:

http
Content-Type: application/json
X-Terminal-Key: <your existing terminal key>
If-Match: <hash returned by GET, for an edit or delete>
Line 3, Column 1http

Public JSON and query reads need no key. Writes and the owner API require your existing key. A Sheet is an interchangeable REST client; it does not become a second graph database.

Smallest starting person

json
{
  "slug": "new-person",
  "title": "New person",
  "kind": "person"
}
Line 5, Column 1JSON

Smallest starting house

json
{
  "slug": "new-house",
  "title": "New house",
  "kind": "house"
}
Line 5, Column 1JSON

slug and title are required by the existing schema; kind supplies the object type. These minimal records default to draft. The same pattern accepts a permit with kind: "permit". IDs must be stable, lowercase, and use letters, digits and hyphens. Use a new ID only for a different object. Lookup the existing object before adding it; the API prevents duplicate IDs, but different IDs are not automatically deduplicated by name or address.

Add a visible listing

Use POST in method, /api/jarticles in path, this JSON in body_json, and leave if_match empty. Change the example ID, title and facts to the actual object before executing.

json15 lines
{
  "slug": "new-house",
  "title": "New house listing",
  "kind": "house",
  "status": "published",
  "facts": {
    "street": "Vichy Avenue"
  },
  "body": [
    {
      "type": "entity",
      "ref": "#"
    }
  ]
}
Line 15, Column 1JSON

A person uses the same request with kind: "person". The self-reference draws its existing object renderer. 201 creates the object. 409 means that ID already exists: read and edit it instead. A published object has /a/new-house and /a/new-house.json. Draft objects remain unavailable publicly.

Read, edit and enrich

First run GET /api/jarticles/new-house. Copy the response hash into column D on the edit row. Then run PATCH /api/jarticles/new-house with:

json18 lines
{
  "ops": [
    {
      "op": "set",
      "path": "facts.street",
      "value": "Vichy Avenue"
    },
    {
      "op": "set",
      "path": "facts.source_observation",
      "value": {
        "source_url": "<actual source URL>",
        "observed_at": "<actual observation timestamp>",
        "source_date": null
      }
    }
  ]
}
Line 18, Column 1JSON

Enrichment is an edit to the existing object or an added source object with a relationship to it. Keep original source dates, current observation dates and derived values distinct. Do not make up missing dates. A stale hash returns 412: read the latest record and reconcile the edit. A PATCH only changes the named paths; setting an array replaces that array, so preserve existing entries when adding a relationship.

For a current residence, edit the person’s facts.relations to include the relationship below along with its existing relationships. Replace the evidence with the actual source. A prior address uses previously_resided_at and temporal_scope: "historical"; a workplace never implies residence.

json
{
  "ref": "new-house",
  "predicate": "resides_at",
  "temporal_scope": "current",
  "as_of": "2026-11-04",
  "valid_from": null,
  "valid_to": null,
  "evidence": {
    "type": "owner_statement",
    "statement": "Use the actual residence statement here.",
    "source_date": "2026-11-04",
    "observed_at": "<actual observation timestamp>"
  }
}
Line 14, Column 1JSON

For source enrichment, store the source account name, exact query, discovered URL, observation time and edge meaning in the evidence. A handle match means a handle match, not verified shared ownership. Direct owner statements and independently observed source facts remain separately labeled.

One house, multiple views

Cyrus Massoumi and 337 Melrose Avenue demonstrate this. The person stores a previously_resided_at relationship to the house. The house reads the incoming relationship as its prior resident; it does not store a copied person or copied house. Zillow and Redfin are their own source-listing objects linked to that house; the parcel remains distinct.

To display that same house anywhere, add this block to the existing page’s body:

json
{
  "type": "entity",
  "ref": "337-melrose-avenue"
}
Line 4, Column 1JSON

The renderer resolves its current data. Editing the house updates subsequent renders wherever it is referenced. Each card’s Object JSON control reads the same canonical JSON. The JSON response may include a generated relationship view; that view is not an independently stored duplicate.

Query from any Sheet

Use POST /api/jarticles/query, with a query object in body_json. A public browser can also read /graph/query.json?q=<URL-encoded query JSON>. Results include object IDs, page and JSON URLs, matching relationship evidence, total count, provider coverage and a next query. Use next as the following request body until it is null. limit is 1–200.

People whose graph records say they currently live on Vichy Avenue:

json
{
  "kind": "person",
  "related": {
    "predicate": "resides_at",
    "to": {
      "kind": "house",
      "where": {
        "facts.street": "Vichy Avenue"
      }
    },
    "current": true
  },
  "scope": "all"
}
Line 14, Column 1JSON

View current Vichy Avenue residents. This tests resides_at, not former residence, a text mention or a workplace address. A zero result means no matching stored published assertion.

All Gmail-domain email objects:

json
{
  "kind": "email",
  "where": {
    "facts.domain": "gmail.com"
  },
  "scope": "all"
}
Line 7, Column 1JSON

View Gmail addresses. massoumi.cyrus@gmail.com matches. cc@llmasos.com and cyrus@dsco.co are separate email objects; a custom domain is not gmail.com even if Google hosts its mailbox.

All LinkedIn profile objects:

json
{
  "kind": "social_profile",
  "where": {
    "facts.platform": "linkedin"
  },
  "scope": "all"
}
Line 7, Column 1JSON

View LinkedIn profiles. This includes person and organization accounts. The County of Marin LinkedIn account belongs to the government organization; the geographical county remains a distinct object.

scope: "all" reads both canonical article objects and the existing Atlas inventory. It omits private or excluded records and does not count raw discovery observations as merged people. Articles precede Atlas in combined pagination; sorting applies within articles, while Atlas uses stable ID order. Social feeds below use canonical post objects and source publication time.

Categories and connections

People · Houses · Facebook profiles · Instagram profiles · LinkedIn profiles · Email addresses · X profiles. Categories resolve their current matching records, rather than retaining copies.

To list direct subsidiaries or contained objects, query {"related":{"predicate":"part_of","to":"<parent-id>"}}. Query owned_by for ownership, works_at for employment, or account_of for profiles. These predicates have different meanings. A category is not automatically an ownership relationship.

William Kasel has separate Instagram and Facebook profiles. Your Instagram follows his and his follows yours. The Facebook friend relationship is separately recorded. The cross-platform identity link is owner asserted, supported by the observed name William Kasel; a shared name alone does not prove identity. His observed Instagram bio mentions Loop.health. Your statement that you both currently work at DSCO is a separate, dated employment assertion. People at DSCO resolves the two people through those links.

Query the links connecting Cyrus and Will

This query requires all four paths: following each other on Instagram, Facebook friendship, and both people having a current work relationship to DSCO. The result carries the matched edges and their evidence; it does not claim that equal names independently verify identity.

json49 lines
{
  "kind": "person",
  "related": [
    {
      "predicate": "has_account",
      "to": {
        "kind": "social_profile",
        "where": {
          "facts.platform": "instagram"
        },
        "related": [
          {
            "predicate": "follows",
            "to": "cyrus-massoumi-instagram"
          },
          {
            "predicate": "follows",
            "from": "cyrus-massoumi-instagram"
          }
        ]
      }
    },
    {
      "predicate": "has_account",
      "to": {
        "kind": "social_profile",
        "where": {
          "facts.platform": "facebook"
        },
        "related": {
          "predicate": "friend_of",
          "to": "cyrus-massoumi-facebook"
        }
      }
    },
    {
      "predicate": "works_at",
      "current": true,
      "to": {
        "id": "dsco",
        "related": {
          "predicate": "works_at",
          "from": "cyrus-massoumi",
          "current": true
        }
      }
    }
  ]
}
Line 49, Column 1JSON

View the shared connections

Posts, tags, place and recency

Each post is stored once and references its author account with posted_by. That account references its person or organization with account_of. A person’s associated_with_place relationship can connect an otherwise untagged post to Marin. This does not rewrite the post as mentioning Marin.

A tagged Marin IJ account uses tags_account; the account links to the Marin Independent Journal organization. The literal hashtag uses has_hashtag. These are independent paths. Add future posts through the same create request, giving each its source ID/URL, text, author-account reference, source publication time, timestamp precision, observation time and relationships. The existing queries include them on the next read; they do not themselves crawl social networks.

Last nine posts by Marin people · Last nine including their organizations · Posts tagging Marin IJ · Posts containing #marincounty · Facebook posts containing #marincounty. The current example has six personal-account posts and nine when MiscSubjects’ X account is included.

Posts containing #marincounty across networks:

json
{
  "kind": "social_post",
  "related": {
    "predicate": "has_hashtag",
    "to": "hashtag-marincounty"
  },
  "sort": "facts.published_at",
  "direction": "desc"
}
Line 9, Column 1JSON

Posts tagging Marin IJ across networks:

json15 lines
{
  "kind": "social_post",
  "related": {
    "predicate": "tags_account",
    "to": {
      "kind": "social_profile",
      "related": {
        "predicate": "account_of",
        "to": "marin-independent-journal"
      }
    }
  },
  "sort": "facts.published_at",
  "direction": "desc"
}
Line 15, Column 1JSON

Add "where":{"facts.platform":"facebook"} (or linkedin / x) to filter a network. Results also include network counts. Use "sort":"facts.published_at","direction":"desc","limit":9 for latest nine. Facebook times are observed to the minute; LinkedIn timestamps are derived from activity IDs and labeled as such; X timestamps come from its time element. Tied or imprecise timestamps do not establish exact ordering.

REST export and continuity

For a current query feed, repeat the same request whenever you want a refresh. Use updated_since for objects edited since an ISO timestamp; this means graph edit time, not original post time. Follow every next query to finish a feed.

For a complete mirror of public canonical article objects, read the manifest, follow every next URL verbatim, and compare each ID and hash with your destination. Fetch changed JSON URLs. After one complete snapshot, remove IDs absent from that manifest from your mirror. If the manifest changes between pages, the server returns 409; restart the snapshot. This detects deletions and unpublishing that an edit-time query alone would miss. Atlas can be exported by paginating the appropriate scoped queries; the article manifest explicitly excludes Atlas.

Write the returned JSON to your chosen system using that system’s API. Any Sheets script or external client can perform these requests. The supplied script runs selected rows only; it installs no background sync and sends nothing to social networks.

Apps Script uses Google’s UrlFetchApp and PropertiesService.