Examples
This section provides practical examples for both the n8n Substack node and the underlying API client.
n8n Workflow Examples
Basic Note Creation
Simple workflow to create a Substack note:
{
"meta": {
"instanceId": "your-instance-id"
},
"nodes": [
{
"parameters": {
"resource": "note",
"operation": "create",
"title": "My First Note",
"body": "This is the content of my note."
},
"id": "substack-node",
"name": "Substack",
"type": "n8n-nodes-substack.substack",
"typeVersion": 1,
"position": [250, 300],
"credentials": {
"substackApi": {
"id": "your-credential-id",
"name": "Substack API"
}
}
}
],
"connections": {}
}
Basic Post Retrieval
Simple workflow to retrieve posts from your publication:
{
"meta": {
"instanceId": "your-instance-id"
},
"nodes": [
{
"parameters": {
"resource": "post",
"operation": "getAll",
"limit": 10,
"offset": 0
},
"id": "substack-posts",
"name": "Get Substack Posts",
"type": "n8n-nodes-substack.substack",
"typeVersion": 1,
"position": [250, 300],
"credentials": {
"substackApi": {
"id": "your-credential-id",
"name": "Substack API"
}
}
}
],
"connections": {}
}
Post Analytics Workflow
Retrieve posts and analyze engagement patterns:
{
"meta": {
"instanceId": "your-instance-id"
},
"nodes": [
{
"parameters": {
"resource": "post",
"operation": "getAll",
"limit": 50,
"offset": 0
},
"id": "get-posts",
"name": "Get Recent Posts",
"type": "n8n-nodes-substack.substack",
"typeVersion": 1,
"position": [100, 300],
"credentials": {
"substackApi": {
"id": "your-credential-id",
"name": "Substack API"
}
}
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"leftValue": "={{ $json.type }}",
"rightValue": "newsletter",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
}
},
"id": "filter-newsletters",
"name": "Filter Newsletters",
"type": "n8n-nodes-base.filter",
"typeVersion": 2,
"position": [400, 300]
},
{
"parameters": {
"resource": "note",
"operation": "create",
"title": "Publication Stats",
"body": "=Recently published: {{ $json.title }} on {{ $json.post_date.split('T')[0] }}"
},
"id": "create-summary",
"name": "Create Summary Note",
"type": "n8n-nodes-substack.substack",
"typeVersion": 1,
"position": [700, 300],
"credentials": {
"substackApi": {
"id": "your-credential-id",
"name": "Substack API"
}
}
}
],
"connections": {
"Get Recent Posts": {
"main": [
[
{
"node": "Filter Newsletters",
"type": "main",
"index": 0
}
]
]
},
"Filter Newsletters": {
"main": [
[
{
"node": "Create Summary Note",
"type": "main",
"index": 0
}
]
]
}
}
}
Automated Content Publishing
Workflow that creates a note when a webhook is triggered:
{
"meta": {
"instanceId": "your-instance-id"
},
"nodes": [
{
"parameters": {
"path": "publish-note",
"options": {}
},
"id": "webhook-trigger",
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1,
"position": [100, 300],
"webhookId": "your-webhook-id"
},
{
"parameters": {
"resource": "note",
"operation": "create",
"title": "={{$json.title}}",
"body": "={{$json.content}}"
},
"id": "substack-node",
"name": "Substack",
"type": "n8n-nodes-substack.substack",
"typeVersion": 1,
"position": [400, 300],
"credentials": {
"substackApi": {
"id": "your-credential-id",
"name": "Substack API"
}
}
}
],
"connections": {
"Webhook": {
"main": [
[
{
"node": "Substack",
"type": "main",
"index": 0
}
]
]
}
}
}
Content Scheduling with Cron
Schedule regular content publication:
{
"meta": {
"instanceId": "your-instance-id"
},
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"triggerAtHour": 9,
"triggerAtMinute": 0,
"weekdays": [1, 3, 5]
}
]
}
},
"id": "cron-trigger",
"name": "Cron",
"type": "n8n-nodes-base.cron",
"typeVersion": 1,
"position": [100, 300]
},
{
"parameters": {
"resource": "post",
"operation": "getAll",
"limit": 1,
"offset": 0
},
"id": "get-latest-post",
"name": "Get Latest Post",
"type": "n8n-nodes-substack.substack",
"typeVersion": 1,
"position": [300, 300],
"credentials": {
"substackApi": {
"id": "your-credential-id",
"name": "Substack API"
}
}
},
{
"parameters": {
"resource": "note",
"operation": "create",
"title": "Weekly Update - {{$now.format('MMMM Do, YYYY')}}",
"body": "=Latest from the publication: {{ $json.title }}\nPublished: {{ $json.post_date.split('T')[0] }}\n\nCheck it out: {{ $json.canonical_url }}"
},
"id": "create-update-note",
"name": "Create Update Note",
"type": "n8n-nodes-substack.substack",
"typeVersion": 1,
"position": [500, 300],
"credentials": {
"substackApi": {
"id": "your-credential-id",
"name": "Substack API"
}
}
}
],
"connections": {
"Cron": {
"main": [
[
{
"node": "Get Latest Post",
"type": "main",
"index": 0
}
]
]
},
"Get Latest Post": {
"main": [
[
{
"node": "Create Update Note",
"type": "main",
"index": 0
}
]
]
}
}
}
Substack API Client Examples
Note: The examples below demonstrate usage of the underlying substack-api library directly. These are not n8n workflows but rather code examples for developers who want to use the library in their own applications.
The n8n Substack node uses this library internally and provides a simplified interface for common operations. For advanced use cases not covered by the n8n node, you can use the library directly in Function nodes or custom applications.
API Client Examples
Initialize the Client
import { Substack } from 'substack-api';
// Initialize with API key (required)
const client = new Substack({
hostname: 'example.substack.com',
apiKey: 'your-api-key-here'
});
// Using default hostname (substack.com)
const defaultClient = new Substack({
apiKey: 'your-api-key-here'
});
Publication Examples
Post Management Examples
List Recent Posts
async function listRecentPosts() {
try {
const posts = await client.getPosts({
limit: 10
});
posts.forEach(post => {
console.log(`${post.title} - ${new Date(post.post_date).toLocaleDateString()}`);
console.log(`Type: ${post.type}, Paywalled: ${post.paywalled ? 'Yes' : 'No'}`);
if (post.subtitle) {
console.log(`Subtitle: ${post.subtitle}`);
}
if (post.cover_image) {
console.log(`Cover: ${post.cover_image}`);
}
console.log('---');
});
} catch (error) {
console.error('Error:', error.message);
}
}
Search Posts by Topic and Date Range
async function searchPostsByTopicAndDate() {
try {
const results = await client.searchPosts({
query: 'TypeScript development',
type: 'newsletter',
published_after: '2023-01-01',
published_before: '2023-12-31',
limit: 20
});
console.log(`Found ${results.total} posts about TypeScript`);
results.results.forEach(post => {
console.log(`${post.title} - ${new Date(post.post_date).toLocaleDateString()}`);
console.log(`URL: ${post.canonical_url}`);
});
} catch (error) {
console.error('Error:', error.message);
}
}
Get Post with Comments
async function getPostWithComments(slug: string) {
try {
// Get the post
const post = await client.getPost(slug);
console.log(`Post: ${post.title}`);
console.log(`Published: ${new Date(post.post_date).toLocaleDateString()}`);
console.log(`URL: ${post.canonical_url}`);
// Get comments for the post
const comments = await client.getComments(post.id, {
limit: 10
});
console.log(`\nRecent comments (${comments.length}):`);
comments.forEach(comment => {
const adminFlag = comment.author.is_admin ? ' [ADMIN]' : '';
console.log(`${comment.author.name}${adminFlag}:`);
console.log(` ${comment.body.substring(0, 100)}...`);
console.log(` Posted: ${new Date(comment.created_at).toLocaleDateString()}`);
});
} catch (error) {
console.error('Error:', error.message);
}
}
Notes Examples
Get and Display Notes Feed
async function displayNotesFeed() {
try {
const notes = await client.getNotes({ limit: 10 });
console.log(`Loaded ${notes.items.length} notes`);
notes.items.forEach(note => {
if (note.comment) {
const user = note.context.users[0];
console.log(`${user.name} (@${user.handle}):`);
console.log(` ${note.comment.body}`);
console.log(` Posted: ${new Date(note.comment.date).toLocaleDateString()}`);
if (note.comment.reactions) {
const totalReactions = Object.values(note.comment.reactions).reduce((a, b) => a + b, 0);
console.log(` Reactions: ${totalReactions}`);
}
console.log('---');
}
});
// Check if there are more notes
if (notes.hasMore()) {
console.log('There are more notes available...');
}
} catch (error) {
console.error('Error:', error.message);
}
}
Paginate Through All Notes
async function getAllNotes() {
const allNotes: SubstackNote[] = [];
try {
let currentBatch = await client.getNotes({ limit: 20 });
while (currentBatch) {
allNotes.push(...currentBatch.items);
console.log(`Loaded ${allNotes.length} notes so far...`);
if (!currentBatch.hasMore()) {
break;
}
currentBatch = await currentBatch.next();
}
console.log(`Total notes loaded: ${allNotes.length}`);
return allNotes;
} catch (error) {
console.error('Error:', error.message);
return allNotes;
}
}
Publish Simple and Formatted Notes
async function publishNotes() {
try {
// Publish a simple note
const simpleNote = await client.publishNote('Just discovered something amazing! 🚀');
console.log(`Simple note published: ${simpleNote.id}`);
// Publish a formatted note with rich text
const formattedNote = await client
.note('🎉 Exciting announcement!')
.note('I just released a new ')
.bold('TypeScript library')
.simple(' for developers.')
.note('Key features:')
.note('• ')
.bold('Type safety')
.simple(' throughout')
.note('• ')
.italic('Easy integration')
.simple(' with existing projects')
.note('• ')
.bold('Comprehensive documentation')
.note('Check it out and let me know what you think! 💪')
.publish();
console.log(`Formatted note published: ${formattedNote.id}`);
} catch (error) {
console.error('Error:', error.message);
}
}
User Profile Examples
Get User Profiles
async function getUserProfiles() {
try {
// Get public profile by handle
const publicProfile = await client.getPublicProfile('john-doe');
console.log(`Name: ${publicProfile.name}`);
console.log(`Handle: @${publicProfile.handle}`);
console.log(`Subscribers: ${publicProfile.subscriberCountString}`);
console.log(`Bio: ${publicProfile.bio}`);
// List user's publications
console.log('\nPublications:');
publicProfile.publicationUsers.forEach(pubUser => {
console.log(`- ${pubUser.publication.name} (${pubUser.role})`);
});
// Get full profile with activity feed
const fullProfile = await client.getFullProfileBySlug('john-doe');
if (fullProfile.userProfile) {
console.log(`\nActivity items: ${fullProfile.userProfile.items.length}`);
}
} catch (error) {
console.error('Error:', error.message);
}
}
Following Management
async function manageFollowing() {
try {
// Get list of followed user IDs
const followingIds = await client.getFollowingIds();
console.log(`You follow ${followingIds.length} users`);
// Get full profiles of followed users
const followingProfiles = await client.getFollowingProfiles();
console.log('\nYour following list:');
followingProfiles.forEach(profile => {
console.log(`${profile.name} (@${profile.handle})`);
console.log(` Subscribers: ${profile.subscriberCountString}`);
if (profile.primaryPublication) {
console.log(` Publication: ${profile.primaryPublication.name}`);
}
if (profile.bio) {
console.log(` Bio: ${profile.bio.substring(0, 100)}...`);
}
console.log('---');
});
} catch (error) {
console.error('Error:', error.message);
}
}
Advanced Examples
Paginated Post Search with Analytics
async function searchAllPostsWithAnalytics(query: string) {
const pageSize = 20;
let offset = 0;
const allResults: SubstackPost[] = [];
const analytics = {
totalPosts: 0,
newsletters: 0,
podcasts: 0,
threads: 0,
paywalled: 0,
dateRange: { earliest: '', latest: '' }
};
try {
while (true) {
const results = await client.searchPosts({
query,
offset,
limit: pageSize
});
allResults.push(...results.results);
// Update analytics
analytics.totalPosts = results.total;
results.results.forEach(post => {
analytics[post.type]++;
if (post.paywalled) analytics.paywalled++;
if (!analytics.dateRange.earliest || post.post_date < analytics.dateRange.earliest) {
analytics.dateRange.earliest = post.post_date;
}
if (!analytics.dateRange.latest || post.post_date > analytics.dateRange.latest) {
analytics.dateRange.latest = post.post_date;
}
});
if (results.results.length < pageSize) {
break; // No more results
}
offset += pageSize;
}
console.log('Search Analytics:');
console.log(`Total posts found: ${analytics.totalPosts}`);
console.log(`Newsletters: ${analytics.newsletters}`);
console.log(`Podcasts: ${analytics.podcasts}`);
console.log(`Threads: ${analytics.threads}`);
console.log(`Paywalled: ${analytics.paywalled}`);
console.log(`Date range: ${analytics.dateRange.earliest} to ${analytics.dateRange.latest}`);
return allResults;
} catch (error) {
console.error('Error:', error.message);
return allResults;
}
}
Comprehensive Error Handling
import { SubstackError } from 'substack-api';
async function robustAPICall<T>(operation: () => Promise<T>): Promise<T | null> {
try {
return await operation();
} catch (error) {
if (error instanceof SubstackError) {
switch (error.status) {
case 400:
console.error('Bad request - check your parameters');
break;
case 401:
console.error('Unauthorized - check your API key');
break;
case 403:
console.error('Forbidden - insufficient permissions');
break;
case 404:
console.error('Resource not found');
break;
case 429:
console.error('Rate limit exceeded. Try again later.');
break;
case 500:
console.error('Server error - try again later');
break;
default:
console.error(`API Error (${error.status}): ${error.message}`);
}
} else {
console.error('Unexpected error:', error);
}
return null;
}
}
// Usage example
async function safeGetPosts() {
return robustAPICall(() => client.getPosts({ limit: 10 }));
}
TypeScript Integration with Custom Types
import type {
SubstackPublication,
SubstackPost,
SubstackComment,
SubstackConfig,
SubstackNote,
SubstackFullProfile,
PaginationParams,
SearchParams
} from 'substack-api';
// Custom interface extending base types
interface ExtendedPost extends SubstackPost {
commentsCount?: number;
readingTime?: number;
}
// Type-safe configuration
const config: SubstackConfig = {
hostname: 'example.substack.com',
apiVersion: 'v1',
apiKey: process.env.SUBSTACK_API_KEY!
};
// Type-safe search parameters
const searchParams: SearchParams = {
query: 'typescript',
type: 'newsletter',
limit: 10
};
// Type-safe function implementations
async function getPostsWithCommentCounts(): Promise<ExtendedPost[]> {
const client = new Substack(config);
const posts = await client.getPosts({ limit: 10 });
const extendedPosts: ExtendedPost[] = await Promise.all(
posts.map(async (post) => {
const comments = await client.getComments(post.id, { limit: 1 }); // Just to get count
return {
...post,
commentsCount: comments.length,
readingTime: Math.ceil(post.title.length / 200) // Rough estimate
};
})
);
return extendedPosts;
}
// Type-safe utility function
function isPaywalledPost(post: SubstackPost): boolean {
return post.paywalled === true;
}
// Filter posts by type with type safety
function filterPostsByType(posts: SubstackPost[], type: SubstackPost['type']): SubstackPost[] {
return posts.filter(post => post.type === type);
}
Real-world Application Example
// Content management dashboard example
class SubstackDashboard {
private client: Substack;
constructor(apiKey: string, hostname?: string) {
this.client = new Substack({
apiKey,
hostname: hostname || 'substack.com'
});
}
async getDashboardData() {
try {
// Get recent posts with engagement metrics
const posts = await this.client.getPosts({ limit: 5 });
const postsWithComments = await Promise.all(
posts.map(async (post) => {
const comments = await this.client.getComments(post.id, { limit: 100 });
return {
...post,
commentCount: comments.length,
lastCommentDate: comments[0]?.created_at
};
})
);
// Get recent notes
const notes = await this.client.getNotes({ limit: 10 });
// Get following stats
const followingIds = await this.client.getFollowingIds();
return {
recentPosts: postsWithComments,
recentNotes: notes.items,
followingCount: followingIds.length,
stats: {
totalPosts: posts.length,
totalNotes: notes.items.length,
avgCommentsPerPost: postsWithComments.reduce((acc, p) => acc + p.commentCount, 0) / postsWithComments.length
}
};
} catch (error) {
console.error('Dashboard error:', error);
return null;
}
}
async publishDailyUpdate(content: string) {
try {
const response = await this.client
.note('📊 Daily Update')
.note(content)
.note('Generated automatically by my dashboard 🤖')
.publish();
console.log(`Daily update published: ${response.id}`);
return response;
} catch (error) {
console.error('Failed to publish daily update:', error);
return null;
}
}
}
// Usage
const dashboard = new SubstackDashboard(process.env.SUBSTACK_API_KEY!, 'mysite.substack.com');
const data = await dashboard.getDashboardData();
if (data) {
console.log(`Publication: ${data.publication.name}`);
console.log(`Recent posts: ${data.recentPosts.length}`);
console.log(`Following: ${data.followingCount} users`);
}
These examples demonstrate common usage patterns and advanced techniques when working with the Substack API client. For more detailed information about specific methods and types, refer to the API Reference section.