Chambers, Legislators, and Connections

getting started with Open States GraphQL

New users of GraphQL should “play along” with this post. Open the Open States GraphiQL page. Whenever a query is presented here, paste it into the left side panel, and press the “Run” button at the top. You should see the results described here in the right panel.

For experienced GraphQL users, this post will guide you through some rough spots in the Open States schema that will cost you time if you aren’t expecting them.

Chambers

When you want to retrieve all the legislators for a chamber, you’ll need the chamber ID, which is an organization ID that looks like

ocd-organization/172e46ae-bd24-406a-a754-e243e5fab319

All the states (and Puerto Rico) have two chambers, an “upper” and “lower” one, except for Nebraska and Washington, D.C., which just have one.

Here’s a query for getting all the chambers of all the states:

{
jurisdictions {
edges {
node {
id
name
organizations(
classification:["upper", "lower", "legislature"],
first:3) {
edges {
node {
id
name
classification
} } } } } } }

For each state, this will return either one or three organizations. If it returns just one, it will have a classification of “legislature” and be the single chamber for Nebraska or Washington, D.C..

For the other states, it returns three, one each of classifications [“upper”, “lower”, “legislature”]. The “legislature” is not a chamber in this case.

Legislators

Fetching all the legislators for a chamber isn’t hard, but there are wrong ways to do it that you need to avoid.

There will be limits on how much data you can retrieve in one query from Open States. The limit hasn’t been settled on, but the tentative plan is to restrict a query to 5000 returned field instances. So, for instance, if you try to get more than a dozen fields apiece on all 400 members of the NH House of Representatives in one query, you’re likely to be rebuffed. A dozen fields may seem like a lot, but isn’t once you consider all the nested info you’re likely to want about legislators, like office locations, committee memberships, bill votes, etc.

So you need to use a method that allows you to restrict the number of legislators fetched at one time. The best way to do that is with apeople() query:

{ people(
memberOf: "ocd-organization/98d43d46-9571-4a40-9007-581d84d41bb8",
first: 5) {
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
totalCount
edges {
node {
id
name
} } } }

This will return the first 5 legislators in the Colorado Senate, along with the totalCount of all the legislators in the Senate, and something called pageInfo:

{
"data": {
"people": {
"pageInfo": {
"hasNextPage": true,
"hasPreviousPage": false,
"startCursor": "YXJyYXljb25uZWN0aW9uOjA=",
"endCursor": "YXJyYXljb25uZWN0aW9uOjQ="
},
"totalCount": 35,
"edges": [
{
"node": {
"id": "ocd-person/01d42abe-6bf4-4e30-b58c-a52a29b8c809",
"name": "Ray Scott"
}
},
(3 more Senators omitted)
{
"node": {
"id": "ocd-person/0aa23426-e455-4250-b246-ed302688ecea",
"name": "Larry Crowder"
}
}
]
} } }

Connections

The Open States GraphQL schema uses a design pattern called Relay Connections to support paging through large collections of data, both forward and backward. Wherever you see something in the schema that ends with Collection, like PersonConnection, or an edges field, or a field that has parameters of first, last, before, and after, think “connection”.

Paging

In the legislator query result’s pageInfo above, you’ll see that hasNextPage is true, meaning that there’s more data after this. There’s also an endCursor field, which contains the cursor (similar to a database cursor) of the last Person returned in this page. If we want to retrieve the next page of data, we can do another query, providing the endCursor value as the after parameter:

{
people(
memberOf: "ocd-organization/98d43d46-9571-4a40-9007-581d84d41bb8",
first: 5,
after: "YXJyYXljb25uZWN0aW9uOjQ=") {
pageInfo {
hasNextPage
endCursor
}
edges {
node {
id
name
} } } }

To page backward, use the same pattern reversed. First use the hasPreviousPagefield to check whether you’re already at the beginning, then just ask for the last: 5 instead of the first: 5, and say that you want the page before the startCursor value of the next page:

{people(
memberOf: "ocd-organization/98d43d46-9571-4a40-9007-581d84d41bb8",
last: 5,
before: "YXJyYXljb25uZWN0aW9uOjU=") {
pageInfo {
hasPreviousPage
startCursor
}
edges {
node {
id
name
} } } }

Note that cursor values are not necessarily permanent and should not be saved for use as an object id. I worry that the cursors used in the last two queries may become invalid.

Legislator Fields: Offices, Districts, Committees, Party, and Votes

Offices

Offices are found in the contactDetails array member of PersonNode . Their availability and field values vary widely from state to state.

Districts

Districts are found by a membership navigation, e.g.,

{people(
memberOf: "ocd-organization/98d43d46-9571-4a40-9007-581d84d41bb8",
first: 1) {
edges {
node {
id
name
currentMemberships {
post {
division {
id
name
} } } } } } }

Unfortunately, this returns an empty PostNode for every non-district membership of the legislator, e.g.,

{  "people": {
"edges": [
{
"node": {
"id": "ocd-person/01d42abe-6bf4-4e30-b58c-a52a29b8c809",
"name": "Ray Scott",
"currentMemberships": [
{ "post": null },
{ "post": null }, {
"post": {
"division": {
"id": "ocd-division/country:us/state:co/sldu:7",
"name": "Colorado State Senate district 7"
}
}
},
{ "post": null },
{ "post": null },
{ "post": null },
{ "post": null },
{ "post": null } ]
} } ] } }

Just filter out the ghost posts. If you’re also fetching committee memberships (below), you may want to fetch them together with the districts.

Committees and Party

Committee and party memberships can be retrieved similarly, but even better, they can be filtered for:

{people(
memberOf: "ocd-organization/98d43d46-9571-4a40-9007-581d84d41bb8",
first: 1) {
edges {
node {
id
name
currentMemberships(classification: ["committee", "party"]) {
classification
role
organization {
id
name
party
} } } } } }

This returns pretty much what you’d expect (excerpted here):

"currentMemberships": [{
"role": "",
"organization": {
"classification": "party",
"id": "ocd-organization/cc568975-7e60-4e1a-8799-63028b9ad960",
"name": "Republican"
}
},
{
"role": "vice-chair",
"organization": {
"classification": "committee",
"id": "ocd-organization/a50e1198-0925-4e7e-b7b6-c2d2d776d7fc",
"name": "Transportation"
}
},
{
"role": "member",
"organization": {
"classification": "committee",
"id": "ocd-organization/8f9517e1-c0ed-44c2-9e42-220c703582ff",
"name": "Transportation Legislation Review Committee"
}
}]

Votes

If you’re retrieving lots of Open States data to store for later use, you may find it easier to collect votes from bills, rather than legislators. In my usage, at least, it’s much more concise, and is via a Connection, so it can be paged.

The votes on a legislator are not accessed via a Connection (see above), but instead are in a simple array. Some states have thousands of votes per session. As a result, you may run into response-size limits (see Legislator section, above) that you have no way to avoid. See this Discourse topic for updates on this.

Next

By the end of this post, new GraphQL users may be frustrated by the lack of depth about GraphQL. You’ve learned how to copy and paste some queries, but without much insight into how to extend this to writing your own queries, or getting those queries into HTTP. The next two posts in this series will scratch this itch: