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",
"app_id": "app_myapp",
"title": "Create meeting notes",
"type": "CREATE_NODE",
"status": "PENDING",
"graph_view": "identity",
"canonical_data": {
"entities": [{
"title": "Team planning meeting",
"value": "Discussed roadmap...",
"node_type": "EXPERIENCE",
"meaning_level": "CONTEXT",
"tags": ["work", "meetings"]
}]
},
"raw_data": {
"source": "my-meeting-app",
"original_transcript": "..."
},
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:00Z"
}
Proposal Types
| Type | Description |
|---|---|
CREATE_NODE | Create new nodes |
UPDATE_NODE | Modify existing nodes |
DELETE_NODE | Remove nodes |
MERGE_NODES | Combine multiple nodes into one |
CREATE_RELATIONSHIP | Add connections between nodes |
CREATE_TAG | Create new tags |
Creating Proposals
Create Node Proposal
proposal = client.proposals.create(
user_id="user-123",
title="Weekly Review",
type="CREATE_NODE",
canonical_data={
"entities": [{
"title": "Weekly Review",
"value": "This week I accomplished...",
"node_type": "EXPERIENCE",
"meaning_level": "CONTEXT",
"graph_view": "identity",
"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",
title="Create contacts + meeting note",
type="CREATE_NODE",
canonical_data={
"entities": [
{"title": "Contact: John", "node_type": "ENTITY"},
{"title": "Contact: Jane", "node_type": "ENTITY"},
{"title": "Team meeting", "node_type": "EXPERIENCE"}
]
}
)
Create Relationship Proposal
proposal = client.proposals.create(
user_id="user-123",
title="Link two nodes",
type="CREATE_RELATIONSHIP",
canonical_data={
"relationships": [{
"from_node_id": "node_a",
"to_node_id": "node_b",
"relationship_type": "references"
}]
}
)
Merge Nodes Proposal
proposal = client.proposals.create(
user_id="user-123",
title="Merge duplicate notes",
type="MERGE_NODES",
canonical_data={
"source_node_ids": ["node_1", "node_2", "node_3"],
"merged_node": {
"title": "Combined experiences",
"value": "Merged content from multiple nodes...",
"node_type": "EXPERIENCE"
},
"merge_reason": "These notes cover the same topic"
}
)
Proposal Status
| Status | Description |
|---|---|
PENDING | Awaiting user review |
READY_FOR_APPROVAL | Enriched and ready to approve |
APPROVED | User approved |
REJECTED | User declined |
APPLIED | Changes applied |
FAILED | Apply failed |
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['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": "EXPERIENCE"
}]
}
)
Canonical Data vs Raw Data
| Field | Purpose |
|---|---|
canonical_data | Structured data that will be created (required) |
raw_data | Original source data for reference (optional) |
Example:
proposal = client.proposals.create(
user_id="user-123",
title="Meeting Summary",
type="CREATE_NODE",
canonical_data={
"entities": [{
"title": "Meeting Summary",
"value": "Key points: 1. Launch date set for March...",
"node_type": "EXPERIENCE",
"graph_view": "identity"
}]
},
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.
Webhooks / Events 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"]
}
}
3. Batch Related Items
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
- Webhooks - Get real-time proposal updates
- API Reference: Proposals - Full endpoint docs