Skip to main content

Proposals

Proposals are how apps request to modify user data. Instead of directly changing data, apps propose changes that users review and approve.

Why Proposals?

In the OfSelf model, users own their data. Apps don't just write to the user's vault—they propose changes that users consciously approve. This ensures:

  • Transparency: Users see exactly what an app wants to add/change
  • Control: Users can modify proposals before approving
  • Trust: No surprise data injected without consent
  • Audit trail: Complete history of what was proposed and approved

Proposal Flow

┌─────────────┐         ┌─────────────┐         ┌─────────────┐
│ App │ │ OfSelf │ │ User │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
│ 1. Create proposal │ │
│──────────────────────>│ │
│ │ │
│ 2. Proposal saved │ │
│<──────────────────────│ │
│ │ │
│ │ 3. Show in dashboard │
│ │──────────────────────>│
│ │ │
│ │ 4. User reviews │
│ │<──────────────────────│
│ │ │
│ │ 5. User approves │
│ │<──────────────────────│
│ │ │
│ 6. Webhook: approved │ │
│<──────────────────────│ │
│ │ │

Proposal Structure

{
"id": "prop_a1b2c3d4",
"owner_id": "user_123",
"third_party_id": "app_myapp",
"proposal_type": "create_node",
"status": "pending",
"canonical_data": {
"entities": [{
"title": "Meeting Notes",
"value": "Discussed roadmap...",
"node_type": "note",
"tags": ["work", "meetings"]
}]
},
"raw_data": {
"source": "my-meeting-app",
"original_transcript": "..."
},
"created_at": "2024-01-15T10:30:00Z",
"reviewed_at": null
}

Proposal Types

TypeDescription
create_nodeCreate new nodes
update_nodeModify existing nodes
delete_nodeRemove nodes
merge_nodesCombine multiple nodes into one
create_relationshipAdd connections between nodes
create_tagCreate new tags

Creating Proposals

Create Node Proposal

proposal = client.proposals.create(
user_id="user-123",
proposal_type="create_node",
canonical_data={
"entities": [{
"title": "Weekly Review",
"value": "This week I accomplished...",
"node_type": "note",
"tags": ["journal", "weekly"]
}]
},
raw_data={
"source": "my-journal-app",
"generated_at": "2024-01-15T10:30:00Z"
}
)

print(f"Proposal created: {proposal['id']}")
print(f"Status: {proposal['status']}") # "pending"

Create Multiple Nodes

proposal = client.proposals.create(
user_id="user-123",
proposal_type="create_node",
canonical_data={
"entities": [
{"title": "Contact: John", "node_type": "entity"},
{"title": "Contact: Jane", "node_type": "entity"},
{"title": "Meeting Notes", "node_type": "note"}
]
}
)

Create Relationship Proposal

proposal = client.proposals.create(
user_id="user-123",
proposal_type="create_relationship",
canonical_data={
"relationships": [{
"source_node_id": "node_a",
"target_node_id": "node_b",
"relationship_type": "references"
}]
}
)

Merge Nodes Proposal

proposal = client.proposals.create(
user_id="user-123",
proposal_type="merge_nodes",
canonical_data={
"source_node_ids": ["node_1", "node_2", "node_3"],
"merged_node": {
"title": "Combined Notes",
"value": "Merged content from multiple notes...",
"node_type": "note"
},
"merge_reason": "These notes cover the same topic"
}
)

Proposal Status

StatusDescription
pendingAwaiting user review
approvedUser approved, changes applied
rejectedUser declined

Listing Proposals

# List pending proposals
result = client.proposals.list(
user_id="user-123",
status="pending"
)

for prop in result['items']:
print(f"[{prop['status']}] {prop['proposal_type']}")
print(f" Entities: {len(prop['canonical_data'].get('entities', []))}")

Approving/Rejecting (Users)

Users approve or reject via the dashboard or API:

# Approve a proposal
result = client.proposals.approve(
user_id="user-123",
proposal_id="prop_abc"
)
# Result includes created node IDs

# Reject a proposal
client.proposals.reject(
user_id="user-123",
proposal_id="prop_abc",
reason="Not relevant to me"
)

Approve with Modifications

Users can modify data before approving:

result = client.proposals.approve(
user_id="user-123",
proposal_id="prop_abc",
modifications={
"entities": [{
"title": "Updated Title", # User changed the title
"value": "...",
"node_type": "note"
}]
}
)

Canonical Data vs Raw Data

FieldPurpose
canonical_dataStructured data that will be created (required)
raw_dataOriginal source data for reference (optional)

Example:

proposal = client.proposals.create(
user_id="user-123",
proposal_type="create_node",
canonical_data={
"entities": [{
"title": "Meeting Summary",
"value": "Key points: 1. Launch date set for March...",
"node_type": "note"
}]
},
raw_data={
"meeting_transcript": "Full transcript here...",
"participants": ["John", "Jane", "Bob"],
"duration_minutes": 45,
"ai_model": "gpt-4"
}
)

The user sees the clean canonical_data. The raw_data is stored for audit/debugging.

Batch Proposals

Create multiple proposals at once:

result = client.proposals.create_batch(
user_id="user-123",
proposals=[
{
"proposal_type": "create_node",
"canonical_data": {"entities": [{"title": "Note 1", ...}]}
},
{
"proposal_type": "create_node",
"canonical_data": {"entities": [{"title": "Note 2", ...}]}
}
]
)

Webhooks for Proposals

Get notified when proposals are approved/rejected:

# Configure webhook for proposal events
client.webhooks.subscribe(
url="https://myapp.com/webhooks",
events=["proposal.approved", "proposal.rejected"]
)

Webhook payload:

{
"event": "proposal.approved",
"proposal_id": "prop_abc",
"user_id": "user_123",
"created_nodes": ["node_1", "node_2"],
"timestamp": "2024-01-15T10:35:00Z"
}

Best Practices

1. Provide Clear Titles

Users see proposal content. Make it understandable:

# Good
{"title": "Meeting Notes: Q4 Planning - Jan 15"}

# Bad
{"title": "mtg_20240115_transcript_v2_final"}

2. Include Source Context

Help users understand where data came from:

{
"canonical_data": {...},
"raw_data": {
"source": "my-meeting-recorder",
"meeting_url": "https://zoom.us/...",
"participants": ["John", "Jane"]
}
}

If creating related nodes, batch them:

# Good - one proposal with related entities
{
"entities": [
{"title": "Meeting Notes", ...},
{"title": "Action Items", ...},
{"title": "Follow-up Tasks", ...}
]
}

# Less ideal - many separate proposals

4. Handle Rejection Gracefully

Some proposals will be rejected. Don't retry immediately:

if proposal['status'] == 'rejected':
# Log for analytics
# Maybe adjust future proposals
# Don't spam the user

Next Steps